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 添加解析器注册函数

问题记录

img

解决步骤:

  1. autoscan产生一个configure.in的原型,执行autoscan后会产生一个configure.scan的文件,可以用它作为configure.in的蓝本

  2. 执行aclocalautoconf,分别会产生aclocal.m4configure两个文件

  3. 然后执行automake --add-missing

  4. 最后执行./configure等继续其他步骤即可

2. 修改协议解析器的实现

对应文件 app-layer-pop3.happ-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

还可在配置文件中添加多个端口,如下(需要注意的是,每一项必须严格用空格对齐,端口之前用空格间隔)

img

协议解析

模板中会创建 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中保存的命令字符串,设置当前命令枚举。如果是userpass等命令(有用信息在请求消息中的),还需要保存整条命令

    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.houtput-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类型

  1. output-json-alert.c 文件 AlertAddAppLayer 函数中,增加pop3分支(能够在alert事件中增加pop3信息)
  2. 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。该函数在解析时,分为三种情况进行处理:

  1. 如果当前消息行是.,表示消息接收完毕,结束解析任务;
// 设置解析状态为完成
Pop3SetResponseState(tx, 0, POP3_COMMAND_PARSE_DONE);
return 0;
  1. 解析消息第一行+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;
  1. 解析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);
posted @ 2022-04-22 14:25  6c696e  阅读(1040)  评论(2)    收藏  举报