Published on

使用Zeek解析POP3协议(2)

Authors
  • avatar
    Name
    Morphy Chan
    Twitter

我们知道,POP3协议请求获取邮件的的命令是RETR,而根据使用Zeek解析POP3协议(1)对Zeek API的测试结果,可以初步归纳一个解析邮件内容的pattern:

  1. 邮件起始标志:
    1. client发出RETR命令,请求获取某封邮件;
    2. server响应OK
  2. 邮件内容:多行字符串
  3. 邮件结束标志:遇到一个新的命令

其中起始标志的2项判断条件都要满足:RETR请求获取邮件,OK代表响应请求成功,而在遇到下一个请求命令之前,我们认为之间的多行字符串都是邮件内容。因此解析邮件实质上是在API输出的多行sting中逐行跟踪分析以上pattern。

首先创建一个用于保存单封邮件信息的record Msg

type Msg: record {
    ts:                 time;
    uid:                string;
    id:                 conn_id;
    flag_retr_succ:     bool;       # 标识retr请求是否获取邮件内容成功
    request:            string;			# 请求命令
    req_arg:            string;			# 请求命令参数
    reply:              string;			# 响应
    retr_data_linenum:  int;				# 邮件内容的字符串行数
    retr_data:          vector of string;	# 保存邮件内容
};

其中flag_retr_succ用于标识邮件起始标志2项条件是否符合。

1. 分析邮件起始

pop3_request追踪retr命令:

event  pop3_request(c: connection, is_orig: bool, command: string, arg: string)
{
		...
    if(command == "RETR") {
        local retr_msg: Msg = [$ts = network_time(),
                                    $id = c$id,
                                    $uid = c$uid,
                                    $flag_retr_succ = F,
                                    $request = "RETR",
                                    $req_arg = arg,
                                    $reply = "",
                                    $retr_data_linenum = 0,
                                    $retr_data = vector()];
        g_retr_msg = retr_msg;
    }
  ...
}

监测到retr命令后,初始化一个全局的Msg

相应地,在pop3_reply分析服务器是否响应OK

event pop3_reply(c: connection, is_orig: bool, cmd: string, msg: string)
{
    ...
    if(cmd == "OK" && g_retr_msg?$flag_retr_succ) { # g_retr_msg exists
        if(g_retr_msg$request == "RETR" && g_retr_msg$reply == "") {
            g_retr_msg$flag_retr_succ = T;
            g_retr_msg$reply = "OK";
        }
    }
}

如果发现:

  1. reply命令为OK
  2. 全局msg中记录的request命令是retr,标志flag_retr_succ 为False,且reply为空

说明此reply响应的是一个retr的请求命令,获取邮件成功。至此邮件起始的2项条件满足:

zeek_pop3_mail_b

2. 保存邮件内容

邮件起始之后的多行字符串默认为邮件内容,使用pop3_data保存邮件内容:

event  pop3_data(c: connection, is_orig: bool, data: string)
{
    if(g_retr_msg?$flag_retr_succ && g_retr_msg$flag_retr_succ == T) {
        if(g_retr_msg$retr_data_linenum < g_retr_msg_max_line) {
            g_retr_msg$retr_data += data;
            g_retr_msg$retr_data_linenum += 1;
        }
    }
}

监测到flag_retr_succ为True,即此时data的内容都是获取的邮件信息,保存到字符串vector中再逐行解析。

分析邮件获取结束并解析邮件内容

同样使用pop3_request追踪邮件结束:

event pop3_reply(c: connection, is_orig: bool, cmd: string, msg: string)
{
    ...
    pop3_proc_g_retr_msg();	# 监测邮件结束标志并解析
    if(cmd == "OK" && g_retr_msg?$flag_retr_succ) { # g_retr_msg exists
        ...
    }
    ...
}

其中pop3_proc_g_retr_msg:

function pop3_proc_g_retr_msg()
{
    if(g_retr_msg?$flag_retr_succ && g_retr_msg$flag_retr_succ == T) {
        # 更新pop3 info信息
        local rec: POP3::Info = [$ts = g_retr_msg$ts,
                                 $uid = g_retr_msg$uid,
                                 $id = g_retr_msg$id];
        g_pop3_rec = rec;
        ...

		# 解析邮件内容
        for(idx in g_retr_msg$retr_data) {
            # print g_retr_msg$retr_data[idx];
            local data:string = g_retr_msg$retr_data[idx];
            local key: string = "";
            local val: string = "";
            local len: int;
            if(data != "") {
                # 匹配to字段
                if(/^[tT][oO]:/ in data) {
                    key = "to";
                    val = data[3:];
                }
                else if(/^[fF][rR][oO][mM]:/ in data) {
                    key = "from";
                    val = data[6:];
                }
				...
            }
            if(key != "" && val != "")
                pop3_update_g_rec_data(key, val);
        }
        # 写入pop3日志
        Log::write(POP3::LOG, g_pop3_rec);

        # 解析一封邮件结束,重新初始化全局的msg
        ...
    }
}

该函数在pop3_requrest被调用,并且判断全局msg中flag_retr_succ是否为T,如果T说明遇到了一个新的命令,即邮件获取完成:

zeek_pop3_mail_e

之后解析保存在msg中的邮件内容,并更新pop3的info信息(参考Zeek默认的smtp解析脚本,POP3脚本也创建了一个类似的info record,用于将解析结果写入日志)。

解析保存的邮件内容字符串,同样参考Zeek smtp的解析脚本,使用正则匹配邮件关键字段,例如from, to等。

3. 脚本解析结果

用以上思路编写解析脚本,在测试邮件上的解析结果:

$ cat pop3.log | jq
{
  "ts": 1615003258.432899,
  "uid": "CeJci2byiawb4zZlk",
  "id.orig_h": "192.168.153.18",
  "id.orig_p": 39118,
  "id.resp_h": "192.168.153.19",
  "id.resp_p": 110,
  "command": [
    "RETR",
    "OK"
  ],
  "arg": [
    "2"
  ],
  "date": " Fri, 5 Mar 2021 23:00:37 -0500",
  "from": "lisi <lisi@localdomain.com>",
  "to": [
    " zhangsan@localdomain.com"
  ],
  "msg_id": " <7ea7b5a3-3e76-ceee-2a49-a9ab81d5cc4c@localdomain.com>",
  "subject": " This is a test mail",
  "user_agent": " Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101"
}

可以看到,对比使用Zeek解析POP3协议(1)中测试邮件的内容,该脚本解析了邮件内容的相应字段信息。如果需要解析更多字段,增加类似的正则匹配即可。

该脚本只是基于一个比较粗糙的pattern,一些细节可能尚未考虑到。同时也有一些问题:

  1. 有的邮件字段信息,例如测试邮件样例中的UA跨行了,即保存在多行的字符串中,应如何处理。
  2. 如何解析邮件附件等。