Suricata新增POP3协议解析
1. 生成新协议的解析模板
scripts文件夹下 setup-app-layer.py 文件适用于python3, setup-app-layer-python2.py 适用于python2
cd suricata-6.0.3
python scripts/setup-app-layer.py Pop3
执行脚本后新增文件:
| src/app-layer-pop3.c | pop3协议的应用层检测器和解析器 |
|---|---|
| src/app-layer-pop3.h | |
| src/output-json-pop3.c | pop3协议的JSON应用层事务记录器 |
| src/output-json-pop3.h |
修改的文件:
| src/Makefile.am | 添加解析器和输出模块源码文件 |
|---|---|
| suricata.yaml.in | 添加协议解析配置,添加输出输出配置 |
| src/suricata-common.h | 添加输出模块号枚举 |
| src/output.c | 添加输出模块注册函数 |
| src/util-profiling.c | 新增输出模块号与其等价字符串映射 |
| src/app-layer-protos.h | 添加协议号枚举 |
| src/app-layer-protos.c | 新增协议名与协议号映射 |
| src/app-layer-detect-proto.c | 新增协议名与协议号映射 |
| src/app-layer-parser.c | 添加解析器注册函数 |
问题记录

解决步骤:
-
用
autoscan产生一个configure.in的原型,执行autoscan后会产生一个configure.scan的文件,可以用它作为configure.in的蓝本 -
执行
aclocal和autoconf,分别会产生aclocal.m4和configure两个文件 -
然后执行
automake --add-missing -
最后执行
./configure等继续其他步骤即可
2. 修改协议解析器的实现
对应文件 app-layer-pop3.h 和 app-layer-pop3.c
协议识别
(1)字符串检索
添加 Pop3RegisterPatternsForProtocolDetection 函数为Pop3协议注册关键字"USER","PASS"和"APOP",在注册解析器时(RegisterPop3Parsers)调用 ,关键字注册函数 AppLayerProtoDetectPMRegisterPatternCI
(2)端口检测
修改 AppLayerProtoDetectPPRegister 端口注册函数,默认端口改为110
/* The default port to probe for echo traffic if not provided in the
* configuration file. */
#define POP3_DEFAULT_PORT "110"
/* The minimum size for a message. For some protocols this might
* be the size of a header. */
#define POP3_MIN_FRAME_LEN 1
还可在配置文件中添加多个端口,如下(需要注意的是,每一项必须严格用空格对齐,端口之前用空格间隔)

协议解析
模板中会创建 RegisterPop3Parsers 函数,pop3模块所用到的函数,都是在该函数中完成注册的(这个过程会在模块初始化中进行,详见《协议解析模块分析》)。
我们可以根据功能的需要,在这个函数中增加注册语句,以使对应的功能起到作用。比如,上文提到的注册协议识别关键字函数 Pop3RegisterPatternsForProtocolDetection
解析请求消息
解析请求的主入口函数为 Pop3ParseRequest ,pop3因为是用来接收邮件的协议,于smtp不同,所以请求消息都比较简单,很多命令都没有什么解析的价值,除个别命令外,其他的只需要记录当前命令分类即可
// pop3命令枚举
enum {
POP3_COMMAND_USER = 1,
POP3_COMMAND_PASS,
POP3_COMMAND_APOP,
POP3_COMMAND_STAT,
/* --这部分命令不需要解析-- */
POP3_COMMAND_DELE, // 给邮件设置删除标记
POP3_COMMAND_RSET, // 用于清除所有删除标记
POP3_COMMAND_NOOP, // 检查服务器连接状态
POP3_COMMAND_QUIT, // 断开连接
POP3_COMMAND_CAPA, // 获取服务器功能列表
/* --------------------- */
POP3_COMMAND_UIDL,
POP3_COMMAND_LIST,
POP3_COMMAND_TOP,
// retr 邮件接收命令
POP3_COMMAND_RETR,
};
Pop3ParseRequest 函数的前面部分是在做一些检测和信息的获取,从下面这部分代码开始,是正式的开始解析工作。首先,尝试获取当前解析事务,如果为空或当前事务已经解析结束,那么就需要申请一个事务结构体,并且重新赋值。
Pop3Transaction* tx = state->curr_tx;
if (tx == NULL || tx->response_done & POP3_COMMAND_PARSE_DONE)
{
tx = Pop3TxAlloc(state);
if (unlikely(tx == NULL)) {
SCLogInfo("Failed to allocate new Pop3 tx.");
goto end;
}
SCLogInfo("Allocated Pop3 tx %"PRIu64".", tx->tx_id);
state->curr_tx = tx;
/* keep track of the start of the tx */
state->toclient_last_data_stamp = state->toclient_data_count;
StreamTcpReassemblySetMinInspectDepth(f->protoctx, STREAM_TOSERVER,
smtp_config.content_inspect_min_size);
}
然后,比较map中保存的命令字符串,设置当前命令枚举。如果是user、pass等命令(有用信息在请求消息中的),还需要保存整条命令
unsigned int i = 0;
while (pop3_command_map[i].enum_name)
{
if (SCMemcmpLowercase(pop3_command_map[i].enum_name, input,
strlen(pop3_command_map[i].enum_name)) == 0)
{
// 根据请求命令设置对应状态
state->current_command = pop3_command_map[i].enum_value;
if (state->current_command == POP3_COMMAND_USER ||
state->current_command == POP3_COMMAND_PASS ||
state->current_command == POP3_COMMAND_APOP)
{
// 需要解析的数据在请求命令中,0=>toserver
state->direction = 0;
state->input = input;
state->input_len = input_len;
} else {
// 需要解析的数据在响应消息中,1=>toclient
state->direction = 1;
}
break;
}
i++;
}
解析响应消息
解析响应的主函数是 Pop3ParseResponse ,与请求类似,也需要先做检测和获取当前事务,但这里不会有申请新事务的操作。接下来会调用 Pop3GetLine 函数将消息按行分开,因为响应消息会存在多行,甚至可能一次请求都没法全部发完。根据状态结构体中保存的方向变量direction判断,要解析的数据是响应还是请求(user、pass等),如果是请求消息,还需判断消息是否以“+ok”开头(表示请求的内容是正确的);响应消息直接进入下一层 Pop3ResponseMsgHandler 进行判断
Pop3ResponseMsgHandler
首先,该函数也需要检测消息的正确性,如果是第一次解析的消息,并且不是以“+ok”开头,则说明消息错误,没有解析价值,直接丢弃即可。
如果消息正确,则判断当前出于哪个命令中,并选择对应的分支进一步的解析
// 分析响应头
if (state->current_line_len >= 3 &&
SCMemcmpLowercase("+ok", state->current_line, 3) == 0)
{
switch (state->current_command)
{
case POP3_COMMAND_STAT: { // 获取邮件数量和总大小
int r1 = Pop3ParseCommandWithInteger(
state, 3, &tx->mail_count, ' ');
int r2 = Pop3ParseCommandWithInteger(
state, 3 + 1 + CalcDigitsForInteger(tx->mail_count),
&tx->mail_total_size, ' ');
return (r1 <= r2) ? r1 : r2;
}
case POP3_COMMAND_UIDL: // 获取邮件唯一标识符
return Pop3ProcessCommandUIDL(state, tx);
case POP3_COMMAND_LIST: // 获取邮件列表
return Pop3ProcessCommandLIST(state, tx);
case POP3_COMMAND_TOP: { // 接收邮件前n行
state->current_command = POP3_COMMAND_RETR;
return Pop3ParseCommandRETR(state, tx, f);
}
case POP3_COMMAND_RETR: // 接收邮件
return Pop3ParseCommandRETR(state, tx, f);
default:
break;
}
}
如果消息是多行命令的后续行,则根据保存的状态,继续调用解析函数
// 进行后续数据分析
switch (tx->response_state)
{
case POP3_COMMAND_UIDL:
return Pop3ProcessCommandUIDL(state, tx);
case POP3_COMMAND_LIST:
return Pop3ProcessCommandLIST(state, tx);
case POP3_COMMAND_TOP:
case POP3_COMMAND_RETR:
return Pop3ParseCommandRETR(state, tx, f);
default:
break;
}
3. 修改输出模块的实现
对应文件 output-json-pop3.h 和 output-json-pop3.c
输出模块的入口函数是 JsonPop3Logger
(1)创建pop3通用头部信息
JsonBuilder *js = CreateEveHeaderWithTxId(
p, LOG_DIR_PACKET, "pop3", NULL, tx_id, thread->emaillog_ctx->eve_ctx);
if (unlikely(js == NULL)) {
return TM_ECODE_FAILED;
}
(2)输出pop3命令解析信息
调用 EvePop3DataLogger 函数
static void EvePop3DataLogger(const Flow* f, void* state, void* vtx, uint64_t tx_id, JsonBuilder* js)
{
Pop3Transaction* tx = vtx;
if (tx->username) {
jb_set_string(js, "user", (const char*)tx->username);
}
if (tx->password) {
jb_set_string(js, "pass", (const char*)tx->password);
}
if (tx->mail_count) {
jb_set_uint(js, "count", tx->mail_count);
}
if (tx->mail_total_size) {
jb_set_uint(js, "total_bytes", tx->mail_total_size);
}
Pop3MailInfo* info_item;
if (!TAILQ_EMPTY(&tx->info_list)) {
JsonBuilderMark mark = { 0, 0, 0 };
jb_get_mark(js, &mark);
jb_open_object(js, "list");
TAILQ_FOREACH(info_item, &tx->info_list, next) {
char str[10];
sprintf(str, "%d", info_item->mail_id);
jb_set_uint(js, (const char*)str, info_item->bytes);
}
jb_close(js);
}
Pop3MailUid* uid_item;
if (!TAILQ_EMPTY(&tx->uid_list)) {
JsonBuilderMark mark = { 0, 0, 0 };
jb_get_mark(js, &mark);
jb_open_object(js, "uidl");
TAILQ_FOREACH(uid_item, &tx->uid_list, next) {
char str[10];
sprintf(str, "%d", uid_item->mail_id);
jb_set_string(js, (const char*)str, (const char*)uid_item->uid);
}
jb_close(js);
}
}
(3)输出邮件正文解析信息
EveEmailLogJson
这个函数在 output-json-email-common.c 文件中,按道理来说这应该是通用的邮件输出模块,但目前的Suricata源码中,这部分是仅针对于smtp协议使用的,所以解析pop3的时候就会不兼容。这部分做的修改主要就是,将本函数以及调用的下层函数等,改为邮件协议通用的代码
/* JSON format logging */
TmEcode EveEmailLogJson(JsonEmailLogThread *aft, JsonBuilder *js, const Packet *p, Flow *f, void *state, void *vtx, uint64_t tx_id)
{
OutputJsonEmailCtx *email_ctx = aft->emaillog_ctx;
JsonBuilderMark mark = { 0, 0, 0 };
AppProto proto = FlowGetAppProtocol(f);
jb_get_mark(js, &mark);
jb_open_object(js, "email");
if (!EveEmailLogJsonData(proto, state, vtx, tx_id, js)) {
jb_restore_mark(js, &mark);
SCReturnInt(TM_ECODE_FAILED);
}
if ((email_ctx->flags & LOG_EMAIL_EXTENDED) || (email_ctx->fields != 0))
EveEmailLogJSONCustom(email_ctx, js, vtx, proto);
if (!g_disable_hashing) {
EveEmailLogJSONMd5(email_ctx, js, vtx, proto);
}
jb_close(js);
SCReturnInt(TM_ECODE_OK);
}
(4)在告警事件中增加pop3类型
- 在 output-json-alert.c 文件 AlertAddAppLayer 函数中,增加pop3分支(能够在alert事件中增加pop3信息)
- 在 output-json-file.c 文件 JsonBuildFileInfoRecord 函数中,增加pop3分支(在file事件中增加pop3信息)
case ALPROTO_POP3:
jb_get_mark(jb, &mark);
jb_open_object(jb, "pop3");
if (EvePop3AddMetadata(p->flow, tx_id, jb)) {
jb_close(jb);
} else {
jb_restore_mark(jb, &mark);
}
jb_get_mark(jb, &mark);
jb_open_object(jb, "email");
if (EveEmailAddMetadata(p->flow, tx_id, jb)) {
jb_close(jb);
} else {
jb_restore_mark(jb, &mark);
}
break;
4. 邮件正文解析
在所有命令当中,最重要的当然是邮件接收命令retr,对应的函数是 Pop3ParseCommandRETR。该函数在解析时,分为三种情况进行处理:
- 如果当前消息行是
.,表示消息接收完毕,结束解析任务;
// 设置解析状态为完成
Pop3SetResponseState(tx, 0, POP3_COMMAND_PARSE_DONE);
return 0;
- 解析消息第一行
+ok,因为本行格式与下文不同,用来标识邮件的字节数。这条分支还需要做一些初始化的操作,初始化Mime解析器,注册数据处理函数等,为解析正文做好准备
// TODO:将截取到的邮件序号和字节数存入info_list
unsigned int i = 0;
int r = Pop3ParseCommandWithInteger(state, 3, &i, ' ');
// 初始化Mime解析器,注册数据处理函数(主要负责处理附件)
tx->mime_state = MimeDecInitParser(f, Pop3ProcessDataChunk);
if (tx->mime_state == NULL) {
SCLogError(SC_ERR_MEM_ALLOC, "MimeDecInitParser() failed to "
"allocate data");
return MIME_DEC_ERR_MEM;
}
// 添加新的mime消息到列表末尾
if (tx->msg_head == NULL) {
tx->msg_head = tx->mime_state->msg;
tx->msg_tail = tx->mime_state->msg;
}
else {
tx->msg_tail->next = tx->mime_state->msg;
tx->msg_tail = tx->mime_state->msg;
}
// 设置解析状态为开始
Pop3SetResponseState(tx, POP3_COMMAND_RETR, 0);
return r;
- 解析Mime格式的消息正文 MimeDecParseLine,详见 util-decode-mimie.c 文件。因为邮件协议通用使用Mime格式传输,所以这部分直接使用了smtp模块的实现,具体分析请查看《SMTP协议解析模块分析报告》
5. 邮件附件还原
Suricata本身为能传输文件的协议,提供了一些处理模板。要想实现pop3附件还原的功能,只需再做一些修改。
detect-filename.c 文件的 DetectFilenameRegister 函数中,增加一条pop3协议注册语句;然后在protos_tc数组中加上pop3的协议枚举号
DetectAppLayerInspectEngineRegister2(
"files", ALPROTO_POP3, SIG_FLAG_TOCLIENT, 0, DetectFileInspectGeneric, NULL);

浙公网安备 33010602011771号