Suricata源码分析-SMTP协议解析
注册协议解析器
SMTP模块的初始化和其他的应用层协议一样,是由 AppLayerSetup -> AppLayerParserRegisterProtocolParsers 调用 RegisterSMTPParsers 函数来注册SMTP协议解析器。截取其中一组比较重要的函数(解析入口)
RegisterSMTPParsers 函数完成了SMTP协议识别和解析所需要的一切函数的注册,整个协议解析模块都是以这样的方式实现,先在初始化模块注册,后面通过回调的方式调用到对应的处理函数。
# 注册了两个方向的解析函数,利用回调的方式调用
# SMTPParseClientRecord 处理客户端发送到服务端的消息(请求)
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_SMTP, STREAM_TOSERVER,
SMTPParseClientRecord);
# SMTPParseServerRecord 处理服务端到客户端的消息(响应)
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_SMTP, STREAM_TOCLIENT,
SMTPParseServerRecord);
这两个回调函数本身没有做什么事,都只是在内部调用了 SMTPParse,唯一的区别是第一个参数,传入方向不同。SMTPParse 函数原型如下:
static AppLayerResult SMTPParse(int direction, Flow *f, SMTPState *state,
AppLayerParserState *pstate, const uint8_t *input,
uint32_t input_len, SMTPThreadCtx *thread_data)
结构体介绍
协议解析模块涉及到几个重要的结构体(持续补充中……)
状态结构体-SMTPState
typedef struct SMTPState_ {
SMTPTransaction *curr_tx;
TAILQ_HEAD(, SMTPTransaction_) tx_list; /**< transaction list */
uint64_t tx_cnt;
uint64_t toserver_data_count;
uint64_t toserver_last_data_stamp;
// 正在解析的输入
const uint8_t *input;
int32_t input_len;
uint8_t direction;
/* --parser details-- */
// 解析器从对SMTPGetline()的调用中提取的当前行
const uint8_t *current_line;
// current_line的行长度,不包括分隔符\r\n
int32_t current_line_len;
uint8_t current_line_delimiter_len;
// 用于指示current_line缓冲区是否为已分配的缓冲区。
// 如果一行是分段的,我们就使用已分配的缓冲区。
uint8_t *tc_db;
int32_t tc_db_len;
uint8_t tc_current_line_db;
// 我们可以看到当前解析的行的LF标志
uint8_t tc_current_line_lf_seen;
// 用于指示current_line缓冲区是否为已分配的缓冲区。
// 如果一行是分段的,我们就使用已分配的缓冲区。
uint8_t *ts_db;
int32_t ts_db_len;
uint8_t ts_current_line_db;
// 我们可以看到当前解析的行的LF标志
uint8_t ts_current_line_lf_seen;
// 表示解析器状态的var
uint8_t parser_state;
// 当前命令正在进行中
uint8_t current_command;
// bdat块长度
uint32_t bdat_chunk_len;
// bdat块索引
uint32_t bdat_chunk_idx;
// 请求命令存储在这里,回复处理程序使用这些存储在缓冲区的命令来匹配回复的命令。
// 命令缓冲区
uint8_t *cmds;
// 缓冲区长度
uint16_t cmds_buffer_len;
// 储存在上述缓冲区的命令数量
uint16_t cmds_cnt;
// 缓冲区中命令的索引,目前正在被回复处理程序检查。
uint16_t cmds_idx;
// HELO消息内容
uint16_t helo_len;
uint8_t *helo;
// SMTP Mime解码和文件提取
// 发送给服务器的文件列表
FileContainer *files_ts;
uint32_t file_track_id;
} SMTPState;
事务结构体-SMTPTransaction
SMTPTransaction 结构体用来记录一次SMTP解析事务,可以理解为和邮箱服务器的一次交互。SMTP解析事务会被依次记录在链表中,而这个链表则被保存在上面的状态结构体中。
typedef struct SMTPTransaction_ {
// 事务id,从0开始,事务链表中的唯一标识
uint64_t tx_id;
AppLayerTxData tx_data;
// 事务解析完成时,设置为1
int done;
// 会话中包含的的第一个信息
MimeDecEntity *msg_head;
// 会话中包含的最后一个信息
MimeDecEntity *msg_tail;
// MIME解码解析器状态,存储了解析过程中的所有数据和状态
// 包括信件实体的解析状态、头部数据(以键值对的方式)
MimeDecParseState *mime_state;
// 解析遇到错误时设置响应的解析数据异常事件标志位,detect模块用到
AppLayerDecoderEvents *decoder_events; /**< per tx events */
DetectEngineState *de_state;
// mail from 参数
uint8_t *mail_from;
uint16_t mail_from_len;
// rcpt to的字符串列表
TAILQ_HEAD(, SMTPString_) rcpt_to_list;
// 事务链表的next指针,用来形成链表
TAILQ_ENTRY(SMTPTransaction_) next;
} SMTPTransaction;
MIME解码器状态-MimeDecParseState
// 包含MIME解析器当前状态的结构体
typedef struct MimeDecParseState {
// 信件的第一个信件体指针,是一个链表,包含子信件体链表(child指针链接而成)
// 和兄弟信件体链表(next指针链接而成),通过msg指针可以遍历到所有的子信件和兄弟信件
MimeDecEntity *msg; /**< Pointer to the top-level message entity */
/**
* 这个堆栈的栈顶top指针始终指向正在解析的数据的信件体指针,正在解析的头部字段和body
* 都存储在这个top指向的信件体指针(MimeDecEntity*),设置这个堆栈是方便代码书写,
* 压栈是将一个信件体加入到top指针指向的child子信件体链表,出栈则是将top指针指向top
* 指针指向信件体的父信件体指针,这个操作是在解析遇到boundary时进行的,遇到boundary
* 意味着上一个信件体的结束和下一个信件体的开始,所以要切换top指针,之后,接下来的数据
* 解析都属于这个信件体。
*/
MimeDecStack *stack; /**< Pointer to the top of the entity stack */
// 解析信件体头部字段时临时存储字段名称,解析完一个完整的头部字段和其数据时,
// 将其存储到MimeDecEntity的MimeDecField类型的链表中
uint8_t *hname; /**< Copy of the last known header name */
uint32_t hlen; /**< Length of the last known header name */
uint32_t hvlen; /**< Total length of value list */
// 头部字段的值的结构体的节点类型,这是一个链表,hvalue是头指针,每个节点
// 存储头部字段的值的一部分(如果该值是多行传输的话),头部节点解析完成后,
// 遍历hvalue链表将说有数据片段合并成一个完整的值,作为name的值存储
DataValue *hvalue; /**< Pointer to the incomplete header value list */
uint8_t linerem[LINEREM_SIZE]; /**< Remainder from previous line (for URL extraction) */
uint16_t linerem_len; /**< Length of remainder from previous line */
uint8_t bvremain[B64_BLOCK]; /**< Remainder from base64-decoded line */
uint8_t bvr_len; /**< Length of remainder from base64-decoded line */
uint8_t data_chunk[DATA_CHUNK_SIZE]; /**< Buffer holding data chunk */
#ifdef HAVE_NSS
HASHContext *md5_ctx;
uint8_t md5[MD5_LENGTH];
#endif
// 解析状态。解析头部开始状态HEADER_STARTED,遇到boundary分界线后就是该信件体的头部字段,
// 于是设置准备解析头部状态HEADER_READY,头部字段解析完成状态HEADER_DONE,body数据开始
// 解析状态BODY_STARTED,遇到结尾boundary分界线字符串后设置状态BODY_END_BOUND。
// 根据这个状态决定下一步做什么操作解析头部,还是解析body数据,还是处理boundary等。
uint8_t state_flag; /**< Flag representing current state of parser */
uint32_t data_chunk_len; /**< Length of data chunk */
/**
* 这个变量表示:解析数据过程中遇到boundary分界线,即本信件体包含子信件体,
* 再次遇到该boundary时表示信件体是一个子信件体,把该变量置1,解析完头部
* 节点后遇到boundary时,变量如果为1,则分配MimeDecEntity结构体,并压栈
*(将top指向该结构体指针),后续的头部节点和数据都存储到这个结构体
*/
int found_child; /**< Flag indicating a child entity was found */
/**
* 解析过程中的一个标志变量,两个情况设置该变量:
* 1. 在解析头部字段数据过程中,如果不是头部字段(没有找到冒号)则认为这是body数据,
* 设置为1,然后当作body数据解析,解析完成后设置为0
* 2. 在解析完头部字段后,找不到冒号则认为后续数据为body数据,然后解析是不是boundary,
* 如果不是则认为是body数据,设置为1,之后存解码存储,解析完成后设置为0
*/
int body_begin; /**< Currently at beginning of body */
// 解析过程中的一个标志变量,表示解析的body数据结束
int body_end; /**< Currently at end of body */
// 分隔符长度,即"\r\n"==2
uint8_t current_line_delimiter_len; /**< Length of line delimiter */
void *data; /**< Pointer to data specific to the caller */
// 自定义数据处理函数,当该结构体变量data_chunk存储满时在数据处理函数中调用该函数
// suricata的这个函数中主要是做了附件方面的处理
int (*DataChunkProcessorFunc) (const uint8_t *chunk, uint32_t len,
struct MimeDecParseState *state); /**< Data chunk processing function callback */
} MimeDecParseState;
协议解析主函数-SMTPParse
-
在进行正式的解析工作之前,先设置了一些状态标志位,主要是用来标识方向;以及将输入的数据保存到state结构体中,按行分割进行解析。
-
SMTPGetLine 行处理函数:在收到的数据中查找分隔符\r\n,按行进行分割,
state->current_line指向每行首字符,state->current_line_len记录每行数据的长度,不包含\r\n,其他状态信息也会被保存在结构体中。 -
根据方向不同,调用 SMTPProcessRequest 和 SMTPProcessReply 按行处理请求和响应的数据。
/* toserver */
if (direction == 0) {
while (SMTPGetLine(state) >= 0) {
if (SMTPProcessRequest(state, f, pstate) == -1)
SCReturnStruct(APP_LAYER_ERROR);
}
/* toclient */
} else {
while (SMTPGetLine(state) >= 0) {
if (SMTPProcessReply(state, f, pstate, thread_data) == -1)
SCReturnStruct(APP_LAYER_ERROR);
}
}
处理请求消息-SMTPProcessRequest
该函数主要分为3个部分
- 第一次解析时,需要创建事务结构体并进行初始化
// state->curr_tx始终指向当前的tmtp事务结构体指针
SMTPTransaction *tx = state->curr_tx;
//第一次解析则生成事务结构体SMTPTransaction
if (state->curr_tx == NULL || (state->curr_tx->done && !NoNewTx(state))) {
tx = SMTPTransactionCreate();
if (tx == NULL)
return -1;
state->curr_tx = tx;
TAILQ_INSERT_TAIL(&state->tx_list, tx, next);
tx->tx_id = state->tx_cnt++; // 事务ID自增
// 追踪tx的起点
state->toserver_last_data_stamp = state->toserver_data_count;
StreamTcpReassemblySetMinInspectDepth(f->protoctx, STREAM_TOSERVER,
smtp_config.content_inspect_min_size);
}
- 通过字符串匹配请求命令,设置
state->current_command保存当前命令状态,以及调用每个命令的解析函数,解析的主要命令如下图:(其中比较重要的是关于data命令的解析,后面单独进行讲解)

- 根据
state->current_command中记录的命令,做进一步的数据处理
switch (state->current_command) {
case SMTP_COMMAND_STARTTLS:
return SMTPProcessCommandSTARTTLS(state, f, pstate);
case SMTP_COMMAND_DATA:
return SMTPProcessCommandDATA(state, f, pstate);
case SMTP_COMMAND_BDAT:
return SMTPProcessCommandBDAT(state, f, pstate);
default:
/* we have nothing to do with any other command at this instant.
* Just let it go through */
SCReturnInt(0);
"data"命令分支
- 匹配到"data"命令,首先会设置当前命令和解析状态,在下一步解析的时候会用到
state->current_command = SMTP_COMMAND_DATA;
- 验证是否开启了
raw_extraction配置选项,这项配置开启后,会将邮件正文信息保存在名为“rawmsg”的文件中
// 开启了raw_extraction配置选项
if (smtp_config.raw_extraction) {
const char *msgname = "rawmsg"; /* XXX have a better name */
// 如果指针为空,申请一个文件容器
if (state->files_ts == NULL)
state->files_ts = FileContainerAlloc();
// 再次检查,失败退出函数
if (state->files_ts == NULL) {
return -1;
}
if (state->tx_cnt > 1 && !state->curr_tx->done) {
// we did not close the previous tx, set error
// 没有关闭前一个tx,设置错误
SMTPSetEvent(state, SMTP_DECODER_EVENT_UNPARSABLE_CONTENT);
FileCloseFile(state->files_ts, NULL, 0, FILE_TRUNCATED);
tx = SMTPTransactionCreate();
if (tx == NULL)
return -1;
state->curr_tx = tx;
TAILQ_INSERT_TAIL(&state->tx_list, tx, next);
tx->tx_id = state->tx_cnt++;
}
// 打开或创建文件
if (FileOpenFileWithId(state->files_ts, &smtp_config.sbcfg,
state->file_track_id++,
(uint8_t*) msgname, strlen(msgname), NULL, 0,
FILE_NOMD5|FILE_NOMAGIC|FILE_USE_DETECT) == 0) {
SMTPNewFile(state->curr_tx, state->files_ts->tail);
}
- 另一条分支是验证是否开启
decode_mime配置选项,表明是否需要进行mime解码,这项配置和上面的raw_extraction是互斥的。这里主要是对mime解析器进行了一些初始化操作
else if (smtp_config.decode_mime) {
if (tx->mime_state) {
// 有两封连锁邮件,没有检测到第一封邮件的结束。所以开始一个新的
tx->mime_state->state_flag = PARSE_ERROR;
SMTPSetEvent(state, SMTP_DECODER_EVENT_UNPARSABLE_CONTENT);
tx = SMTPTransactionCreate();
if (tx == NULL)
return -1;
state->curr_tx = tx;
TAILQ_INSERT_TAIL(&state->tx_list, tx, next);
tx->tx_id = state->tx_cnt++;
}
// 通过为状态和top-level实体分配内存来启动解析器
tx->mime_state = MimeDecInitParser(f, SMTPProcessDataChunk);
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;
}
解析data命令
对data命令后的数据进行解析,先判断命令是否是.,如果是说明数据接收完毕,客户端不再发送数据,则设置解析状态和解析标志tx->done=1。如果不是,则调用 MimeDecParseLine 函数,完成数据行的解析。
MimeDecParseLine 是个包裹函数,没有做实质性的工作,只是给state->current_line_delimiter_len分隔符长度赋值,然后调用 ProcessMimeEntity 函数,进行实际的解析工作。
处理MIME信件体-ProcessMimeEntity
函数根据输入行和分析器的当前状态来处理MIME实体
- 判断解析过程中是否出现异常,以及数据长度合法性
// 判断解析过程中是否遇到错误
if (state->state_flag == PARSE_ERROR) {
SCLogDebug("START FLAG: PARSE_ERROR, bail");
return MIME_DEC_ERR_STATE;
}
// 每行数据不能超过998个字符,超过则设置异常标志
/* Track long line */
if (len > MAX_LINE_LEN) {
state->stack->top->data->anomaly_flags |= ANOM_LONG_LINE;
state->msg->anomaly_flags |= ANOM_LONG_LINE;
SCLogDebug("Error: Max input line length exceeded %u > %u", len,
MAX_LINE_LEN);
}
- 判断解析标志,如果是
HEADER_STARTED注① 或HEADER_READY注②,则调用函数 ProcessMimeHeaders 解析信件体的头部;否则调用 ProcessMimeBody 函数解析信件体的body数据。
/* Looking for headers */
if (state->state_flag == HEADER_READY ||
state->state_flag == HEADER_STARTED) {
SCLogDebug("Processing Headers");
/* Process message headers */
ret = ProcessMimeHeaders(buf, len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessMimeHeaders() function failed: %d",
ret);
return ret;
}
} else {
/* Processing body */
SCLogDebug("Processing Body of: %p", state->stack->top);
ret = ProcessMimeBody(buf, len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessMimeBody() function failed: %d",
ret);
return ret;
}
}
注①:HEADER_STARTED在初始化时设置,第一次解析时会走这条分支。在解析头部字段时,找到冒号成功解析头部字段名称时也会设置,表示需要继续提取该字段的对应内容。
注②:HEADER_READY在遇到boundary分界线时设置,表示将要开始解析信件体。
解析消息头部-ProcessMimeHeaders
(一)调用 FindMimeHeader,查找并保存所有的头部字段
int ret = MIME_DEC_OK;
MimeDecField *field;
uint8_t *bptr = NULL, *rptr = NULL;
uint32_t blen = 0;
MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data;
// 根据冒号查找头部字段的name和value,并保存在MimeDecParseState结构体
// 如果解析到name,则设置HEADER_START标志,继续解析value,解析出的字段会
// 存储在MimeDecField类型的链表中。解析完所有字段后设置HEADER_DONE标志,
// 做后续的分析工作
/* Look for mime header in current line */
ret = FindMimeHeader(buf, len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: FindMimeHeader() function failed: %d", ret);
return ret;
}
关于 FindMimeHeader 函数:主要逻辑就是在一行数据中查找冒号, 冒号前为字段name, 冒号后为字段value。其中value可能分为多行传输, 所以value的多行作为多个数据片段存放在一个链表中。
然后根据解析状态设置解析标志HEADER_DONE,HEADER_STARTED,BODY_STARTED
// 根据当前状态,在当前行找到完整的头部name和value字段
static int FindMimeHeader(const uint8_t *buf, uint32_t blen,
MimeDecParseState *state)
{
int ret = MIME_DEC_OK;
uint8_t *hname, *hval = NULL;
DataValue *dv;
uint32_t hlen, vlen;
// 临时变量,解析过程中使用:name结束时设置finish_header,
// 遇到新的name设置new_header
int finish_header = 0, new_header = 0;
// 从配置文件里读取,与MIME解析相关的配置选项
MimeDecConfig *mdcfg = MimeDecGetConfig();
// 在行数据中查找冒号并解析出冒号前的字段name
hname = FindMimeHeaderStart(buf, blen, &hlen);
if (hname != NULL) {
// 找到name,并且是第一次解析,则分配内存,保存name和value
// 检查字段名称长度(包含冒号最大76),长度不合法时设置异常标志
// 警示和跟踪,但还没有做任何事情
if (hlen > MAX_HEADER_NAME) {
state->stack->top->data->anomaly_flags |= ANOM_LONG_HEADER_NAME;
state->msg->anomaly_flags |= ANOM_LONG_HEADER_NAME;
SCLogDebug("Error: Header name exceeds limit (%u > %u)",
hlen, MAX_HEADER_NAME);
}
// 根据name和长度,解析出冒号后的value
hval = hname + hlen + 1;
if (hval - buf >= (int)blen) {
SCLogDebug("No Header value found");
hval = NULL;
} else {
while (hval[0] == ' ') {
// 如果是界线结束前的最后一个字符,则设置为NULL
if (hval - buf >= (int)blen - 1) {
SCLogDebug("No Header value found");
hval = NULL;
break;
}
hval++;
}
}
// 如果设置了HEADER_STARTED标志,则表示后续数据应该出现value,
// 假如本次解析出name,则表示上次的name结束,设置标志
if (state->state_flag == HEADER_STARTED) {
finish_header = 1;
}
// 新name开始的标志,表示下面数据是新name相关的
new_header = 1;
// 必须等待下一行以确定是否完成
state->state_flag = HEADER_STARTED;
} else if (blen == 0) {
// 如果数据长度为0,则表示空行,意味着头部字段结束(头部字段和
// 数据之间以空行分隔)
// 发现body,没有匹配的头部字段
state->state_flag = HEADER_DONE;
// 头部字段解析结束,但上次的name和value还没有保存,因为
// 只有出现新name的时候,才能知道上次的name信息结束
// 所以在这里设置该标志,以保存上次的头部字段信息
finish_header = 1;
SCLogDebug("All Header processing finished");
} else if (state->state_flag == HEADER_STARTED) {
// 如果没有找到name,但是设置了HEADER_STARTED标志,那说明
// 本次数据是上次name的value的一部分,是分多行传输的,需要
// 分配数据节点,拷贝数据
// 发现多行value(即接收头),如果超过了最大的value,则标记它
vlen = blen;
if ((mdcfg != NULL) && (state->hvlen + vlen > mdcfg->header_value_depth)) {
SCLogDebug("Error: Header value of length (%u) is too long",
state->hvlen + vlen);
vlen = mdcfg->header_value_depth - state->hvlen;
state->stack->top->data->anomaly_flags |= ANOM_LONG_HEADER_VALUE;
state->msg->anomaly_flags |= ANOM_LONG_HEADER_VALUE;
}
if (vlen > 0) {
// 分配数据节点,拷贝数据,因为是上次value的一部分,后续会合并一起存储
dv = AddDataValue(state->hvalue);
if (dv == NULL) {
SCLogError(SC_ERR_MEM_ALLOC, "AddDataValue() function failed");
return MIME_DEC_ERR_MEM;
}
if (state->hvalue == NULL) {
state->hvalue = dv;
}
dv->value = SCMalloc(vlen);
if (unlikely(dv->value == NULL)) {
SCLogError(SC_ERR_MEM_ALLOC, "Memory allocation failed");
return MIME_DEC_ERR_MEM;
}
memcpy(dv->value, buf, vlen);
dv->value_len = vlen;
state->hvlen += vlen;
}
} else {
// 如果在本次数据中没有找到name,解析标志不是HEADER_STARTED,
// 此时的解析标志只能是HEADER_READY,因为只有这两个标志才能进入本函数。
// 重要说明:
// HEADER_READY在解析遇到boundary时才会设置,也就是说,本来遇到boundary了,
// 后续数据应该可以解析出name,但是数据中并没有冒号分割的name,那么这个数据
// 就认为是没有头部字段的body数据。
// 于是设置解析标志为BODY_STARTED,并调用ProcessBodyLine处理本次的body数据。
// 可能是一个没有头部的body数据
SCLogDebug("No headers found");
state->state_flag = BODY_STARTED;
// body开始的标志
state->body_begin = 1;
state->body_end = 0;
ret = ProcessBodyLine(buf, blen, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessBodyLine() function failed");
return ret;
}
}
// 如果我们需要完成一个头部信息,那么就在下面做这些工作,然后再进行清理
if (finish_header) {
// 存储value,保存到链表中
ret = StoreMimeHeader(state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: StoreMimeHeader() function failed");
return ret;
}
}
// 当找到下一个标题时,我们总是创建一个新的标题
if (new_header) {
// 将name和value复制到状态
state->hname = SCMalloc(hlen);
if (unlikely(state->hname == NULL)) {
SCLogError(SC_ERR_MEM_ALLOC, "Memory allocation failed");
return MIME_DEC_ERR_MEM;
}
memcpy(state->hname, hname, hlen);
state->hlen = hlen;
if (state->hvalue != NULL) {
SCLogDebug("Error: Parser failed due to unexpected header "
"value");
return MIME_DEC_ERR_DATA;
}
// 如果解析到了name对应value,则将value存储到state->hvalue临时链表中。
// 因为hvalue只存放解析过程中头部字段的name和value,当遇到新的name或者
// 没有头部的body数据时,说明一个name的value已经完整了,就会将其合并后
// 存储到当前信件体的头部字段链表中,并销毁state->hvalue链表,以供下次
// 使用
if (hval != NULL) {
// 如果超过了最大标题值,则标记它
vlen = blen - (hval - buf);
if ((mdcfg != NULL) && (state->hvlen + vlen > mdcfg->header_value_depth)) {
SCLogDebug("Error: Header value of length (%u) is too long",
state->hvlen + vlen);
vlen = mdcfg->header_value_depth - state->hvlen;
state->stack->top->data->anomaly_flags |= ANOM_LONG_HEADER_VALUE;
state->msg->anomaly_flags |= ANOM_LONG_HEADER_VALUE;
}
if (vlen > 0) {
state->hvalue = AddDataValue(NULL);
if (state->hvalue == NULL) {
SCLogError(SC_ERR_MEM_ALLOC, "AddDataValue() function failed");
return MIME_DEC_ERR_MEM;
}
state->hvalue->value = SCMalloc(vlen);
if (unlikely(state->hvalue->value == NULL)) {
SCLogError(SC_ERR_MEM_ALLOC, "Memory allocation failed");
return MIME_DEC_ERR_MEM;
}
memcpy(state->hvalue->value, hval, vlen);
state->hvalue->value_len = vlen;
state->hvlen += vlen;
}
}
}
return ret;
}
(二)对找到的头部字段进行分析处理
如果状态标志为HEADER_DONE,则表示头部字段查找完成。开始分析头部重要字段如:content-type, content-transfer-encoding,content-dispositon,查找关键字符串如:"message/","boundary"等。
- 首先通过查看
content-transfer-encoding字段来确定编码
field = MimeDecFindField(entity, CTNT_TRAN_STR);
if (field != NULL) {
/* Look for base64 */
if (FindBuffer(field->value, field->value_len, (const uint8_t *)BASE64_STR, strlen(BASE64_STR))) {
SCLogDebug("Base64 encoding found");
entity->ctnt_flags |= CTNT_IS_BASE64;
} else if (FindBuffer(field->value, field->value_len, (const uint8_t *)QP_STR, strlen(QP_STR))) {
/* Look for quoted-printable */
SCLogDebug("quoted-printable encoding found");
entity->ctnt_flags |= CTNT_IS_QP;
}
}
- 检查内容中是否有附件,通过
content-dispositon字段
field = MimeDecFindField(entity, CTNT_DISP_STR);
if (field != NULL) {
bool truncated_name = false;
bptr = FindMimeHeaderTokenRestrict(field, "filename=", TOK_END_STR, &blen, NAME_MAX, &truncated_name);
if (bptr != NULL) {
SCLogDebug("File attachment found in disposition");
entity->ctnt_flags |= CTNT_IS_ATTACHMENT;
// 使用动态内存进行复制
entity->filename = SCMalloc(blen);
if (unlikely(entity->filename == NULL)) {
SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed");
return MIME_DEC_ERR_MEM;
}
memcpy(entity->filename, bptr, blen);
entity->filename_len = blen;
if (truncated_name) {
state->stack->top->data->anomaly_flags |= ANOM_LONG_FILENAME;
state->msg->anomaly_flags |= ANOM_LONG_FILENAME;
}
}
}
- 检查boundary、message和文件名等信息,其中boundary对于解析嵌套的信件体非常重要
/* Check for boundary, encapsulated message, and file name in Content-Type */
// 检查边界、封装的信息和Content-Type中的文件名
field = MimeDecFindField(entity, CTNT_TYPE_STR);
if (field != NULL) {
// 如果找到boundary字符串,表示当前信件体包含子信件体,设置变量found_child为1
// 解析body数据时会判断这个变量
bptr = FindMimeHeaderToken(field, BND_START_STR, TOK_END_STR, &blen);
if (bptr != NULL) {
state->found_child = 1;
// 设置content标志,说明信件体有多个部分组成
entity->ctnt_flags |= CTNT_IS_MULTIPART;
// boundary字符串长度检查
if (blen > (BOUNDARY_BUF - 2)) {
state->stack->top->data->anomaly_flags |= ANOM_LONG_BOUNDARY;
return MIME_DEC_ERR_PARSE;
}
// 存储boundary字符串到本信件体,后续解析body数据时在其中查找boundary字符串
state->stack->top->bdef = SCMalloc(blen);
if (unlikely(state->stack->top->bdef == NULL)) {
SCLogError(SC_ERR_MEM_ALLOC, "Memory allocation failed");
return MIME_DEC_ERR_MEM;
}
memcpy(state->stack->top->bdef, bptr, blen);
state->stack->top->bdef_len = blen;
}
// 判断是否有附件,有的话提取附件名称
if (!(entity->ctnt_flags & CTNT_IS_ATTACHMENT)) {
bool truncated_name = false;
bptr = FindMimeHeaderTokenRestrict(field, "name=", TOK_END_STR, &blen, NAME_MAX, &truncated_name);
if (bptr != NULL) {
SCLogDebug("File attachment found");
entity->ctnt_flags |= CTNT_IS_ATTACHMENT;
// 使用动态内存进行复制
entity->filename = SCMalloc(blen);
if (unlikely(entity->filename == NULL)) {
SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed");
return MIME_DEC_ERR_MEM;
}
memcpy(entity->filename, bptr, blen);
entity->filename_len = blen;
if (truncated_name) {
state->stack->top->data->anomaly_flags |= ANOM_LONG_FILENAME;
state->msg->anomaly_flags |= ANOM_LONG_FILENAME;
}
}
}
// 拉出简短的内容类型
entity->ctnt_type = GetToken(field->value, field->value_len, " \r\n;",
&rptr, &entity->ctnt_type_len);
if (entity->ctnt_type != NULL) {
// 这里在查找头字段中的值类型字符串"message/"
if (FindBuffer(entity->ctnt_type, entity->ctnt_type_len,
(const uint8_t *)MSG_STR, strlen(MSG_STR)))
{
SCLogDebug("Found encapsulated message entity");
entity->ctnt_flags |= CTNT_IS_ENV;
// 创建并推送子节点到堆栈
MimeDecEntity *child = MimeDecAddEntity(entity);
if (child == NULL)
return MIME_DEC_ERR_MEM;
child->ctnt_flags |= (CTNT_IS_ENCAP | CTNT_IS_MSG);
PushStack(state->stack);
state->stack->top->data = child;
// 标记为封装的子节点
state->stack->top->is_encap = 1;
/* Ready to parse headers */
state->state_flag = HEADER_READY;
} else if (FindBuffer(entity->ctnt_type, entity->ctnt_type_len,
(const uint8_t *)MULTIPART_STR, strlen(MULTIPART_STR)))
{
/* Check for multipart */
SCLogDebug("Found multipart entity");
entity->ctnt_flags |= CTNT_IS_MULTIPART;
} else if (FindBuffer(entity->ctnt_type, entity->ctnt_type_len,
(const uint8_t *)TXT_STR, strlen(TXT_STR)))
{
/* Check for plain text */
SCLogDebug("Found plain text entity");
entity->ctnt_flags |= CTNT_IS_TEXT;
} else if (FindBuffer(entity->ctnt_type, entity->ctnt_type_len,
(const uint8_t *)HTML_STR, strlen(HTML_STR)))
{
/* Check for html */
SCLogDebug("Found html entity");
entity->ctnt_flags |= CTNT_IS_HTML;
}
}
}
- 查找是否存在
message-id字段,最后解析完成后,设置消息体开始的标志
// 存储指向Message-ID的指针
field = MimeDecFindField(entity, MSG_ID_STR);
if (field != NULL) {
entity->msg_id = field->value;
entity->msg_id_len = field->value_len;
}
// 因为头部字段解析完成了,所以后续的数据就是body数据了
state->body_begin = 1; // 消息体开始的标志
state->body_end = 0;
解析消息体-ProcessMimeBody
函数首先是查找boundary分界线,根据查找结果函数 ProcessMimeBoundary 进一步处理boundary,如果没有找到boundary,则认为数据为body,调用函数 ProcessBodyLine 进行body数据的处理。
- 首先尝试寻找boundary
/* First look for boundary */
MimeDecStackNode *node = state->stack->top;
if (node == NULL) {
SCLogDebug("Error: Invalid stack state");
return MIME_DEC_ERR_PARSE;
}
// 上次的数据如果是结尾boundary字符串,则在解析时设置标志BODY_END_BOUND,
// 此处判断如果解析标志为BODY_END_BOUND,说明一个完整的信件体解析结束。
//
// 于是需要确定本次的boundary字符串,使用其父信件体定义的boundary,父信件体
// 的content-type中包含了这个boundary,如果没有则继续向上查找,直到找到。
// 如果最后没有找到boundary,则说明后续没有子信件体了,是普通的body数据,不
// 需要再用boundary查找,直接将数据存储到结构体中即可。
/* Traverse through stack to find a boundary definition */
if (state->state_flag == BODY_END_BOUND || node->bdef == NULL) {
/* If not found, then use parent's boundary
node = node->next;
while (node != NULL && node->bdef == NULL) {
SCLogDebug("Traversing through stack for node with boundary");
node = node->next;
}
}
- 如果没有找到任何boundary,则意味着是在正文中;如果找到了,则查找boundary的起点,做进一步处理
/* This means no boundary / parent w/boundary was found so we are in the body */
if (node == NULL) {
body_found = 1;
} else {
// 查找boundary字符串,比较前边两个横线即“--”
/* Now look for start of boundary */
if (len > 1 && buf[0] == '-' && buf[1] == '-') {
tlen = node->bdef_len + 2;
if (tlen > BOUNDARY_BUF) {
if (state->stack->top->data)
state->stack->top->data->anomaly_flags |= ANOM_LONG_BOUNDARY;
return MIME_DEC_ERR_PARSE;
}
memcpy(temp, "--", 2);
memcpy(temp + 2, node->bdef, node->bdef_len);
// 寻找下一个边界或结束边界
bstart = FindBuffer((const uint8_t *)buf, len, temp, tlen);
if (bstart != NULL) {
ret = ProcessMimeBoundary(buf, len, node->bdef_len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessMimeBoundary() function "
"failed");
return ret;
}
} else {
/* Otherwise add value to body */
body_found = 1;
}
} else {
/* Otherwise add value to body */
body_found = 1;
}
}
- 判断
body_found变量,调用函数处理body数据
/* Process body line */
if (body_found) {
// 设置解析标志
state->state_flag = BODY_STARTED;
ret = ProcessBodyLine(buf, len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessBodyLine() function failed");
return ret;
}
}
Mime分界线处理函数-ProcessMimeBoundary
-
首先对上次的数据处理结果进行保存,判断state_flag是不是
BODY_END_BOUND标志,这个标志是在遇到结尾boundary分界线时设置的。 -
- 如果不是,则说明上次的数据不是结束边界,那么对信件体做结束处理。
/* If previous line was not an end boundary, then we process the body as
* completed */
if (state->state_flag != BODY_END_BOUND) {
/* First lets complete the body */
ret = ProcessBodyComplete(state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessBodyComplete() function failed");
return ret;
}
-
- 如果state_flag是
BODY_END_BOUND标志,则说明上次的数据是结束边界,那么这个信件体的子信件体结束,设置HEADER_READY标志,准备进行下一次解析。
- 如果state_flag是
-
对当前数据(即boundary)进行分析
-
- 遇到了结尾boundary字符串,当前信件体结束,调整top指针,使其指向父信件体
// 现在检查嵌套边界的末端
if (len - (rptr - buf) > 1 && rptr[0] == DASH && rptr[1] == DASH) {
SCLogDebug("FOUND END BOUNDARY, POPPING: %p=%p",
state->stack->top, state->stack->top->data);
//如果本次数据是结尾boundary分界线,意味着一个信件体的所有子信件体
//结束了并且解析完成,于是调用函数popstack将mime_state->stack-top
//指向其父信件体结构体指针,这个堆栈的next指针指向的是其父信件体的
//结构体指针,并这只解析标志BODY_END_BOUND,这个意思就是遇到结尾
//boundary分界线了,这个信件体的所有子信件体就解析完成了,后续数据
//应该存储到其父信件体了。
// 如果找到边界的末尾,将子对象从堆栈中弹出
PopStack(state->stack);
if (state->stack->top == NULL) {
SCLogDebug("Error: Message is malformed");
return MIME_DEC_ERR_DATA;
}
// 如果当前是带有边界定义的封装消息,那么也可以弹出他
if (state->stack->top->is_encap && state->stack->top->bdef_len != 0) {
SCLogDebug("FOUND END BOUNDARY AND ENCAP, POPPING: %p=%p",
state->stack->top, state->stack->top->data);
PopStack(state->stack);
if (state->stack->top == NULL) {
SCLogDebug("Error: Message is malformed");
return MIME_DEC_ERR_DATA;
}
}
//设置结尾boundary分界线标志,设置后,如果下次遇到boundary分界线,就会执行函数
//开始的判断,设置state->state_flag = HEADER_READY
state->state_flag = BODY_END_BOUND;
-
- 如果
state->found_child为1
- 如果
else if (state->found_child) {
// 如果本次数据是boundary分界线,且mime_state->found_child为1,
// 则认为后续数据是一个子信件体,因为这个变量是在解析头部字段完成后,
// 分析content-type且找到boundary时设置的,说明本信件体包含一个
// 或多个子信件体。于是此处生成一个信件体结构体并链入其父信件体的child
// 链表中,并将top->data指向它,后续数据会存储到这里。
/* Otherwise process new child */
SCLogDebug("Child entity created");
/* Create and push child to stack */
child = MimeDecAddEntity(state->stack->top->data);
if (child == NULL)
return MIME_DEC_ERR_MEM;
child->ctnt_flags |= CTNT_IS_BODYPART;
PushStack(state->stack);
state->stack->top->data = child;
/* Reset flag */
state->found_child = 0;
-
- 如果
state->found_child不为1
- 如果
else {
/* Otherwise process sibling */
if (state->stack->top->next == NULL) {
SCLogDebug("Error: Missing parent entity from stack");
return MIME_DEC_ERR_DATA;
}
SCLogDebug("SIBLING CREATED, POPPING PARENT: %p=%p",
state->stack->top, state->stack->top->data);
// 如果本次数据不是结尾boundary,且mime_state->child_found不为1
// (不是子信件体),说明这个分界线后续的信件体是上个信件体的兄弟信件体,
// 于是找出父信件体,生成新信件体,将新信件体链入父信件体的child链表中。
// 首先弹出当前节点,以获得对父节点的访问权
PopStack(state->stack); //将top指针指向其父信件体
if (state->stack->top == NULL) {
SCLogDebug("Error: Message is malformed");
return MIME_DEC_ERR_DATA;
}
/* Create and push child to stack */
child = MimeDecAddEntity(state->stack->top->data);
if (child == NULL)
return MIME_DEC_ERR_MEM;
child->ctnt_flags |= CTNT_IS_BODYPART;
//将top指针指向新生成的信件体,后续数据都保存到这个信件体里
PushStack(state->stack);
state->stack->top->data = child;
- 在经过上述的分支之后,判断状态标志。如果不是结尾boundary分界线,则设置解析标志为
HEADER_READY,说明后续数据是头部字段,下次将判断此字段进入头部字段解析函数
/* After boundary look for headers */
if (state->state_flag != BODY_END_BOUND) {
state->state_flag = HEADER_READY;
}
Body行数据处理函数-ProcessBodyLine
- 根据头部字段设置编码标志对数据进行解码,主要有base64和quoted-printable两种,如果数据未编码则无需解码,直接存储
// 判断编码标志,如果是base64则解码
MimeDecConfig *mdcfg = MimeDecGetConfig();
if (mdcfg != NULL && mdcfg->decode_base64 &&
(entity->ctnt_flags & CTNT_IS_BASE64)) {
ret = ProcessBase64BodyLine(buf, len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessBase64BodyLine() function failed");
}
} else if (mdcfg != NULL && mdcfg->decode_quoted_printable &&
(entity->ctnt_flags & CTNT_IS_QP)) {
// 判断编码标志,如果是quoted-printable则解码
ret = ProcessQuotedPrintableBodyLine(buf, len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessQuotedPrintableBodyLine() function "
"failed");
}
} else {
// 未编码数据,直接复制到data_chunk变量中
remaining = len;
offset = 0;
- 将数据存储到
data_chunk中,如果空间满了则调用 ProcessDecodedDataChunk 函数进行处理,之后将data_chunk_len置0,下次数据从头存储
while (remaining > 0) {
// 计划在每一行的末尾加上CRLF(\r\n)
avail = DATA_CHUNK_SIZE - state->data_chunk_len;
tobuf = avail > remaining + EOL_LEN ? remaining : avail - EOL_LEN;
// 拷贝到缓冲区
memcpy(state->data_chunk + state->data_chunk_len, buf + offset, tobuf);
state->data_chunk_len += tobuf;
// 现在在末尾添加CRLF(\r\n)
if (tobuf == remaining) {
memcpy(state->data_chunk + state->data_chunk_len, CRLF, EOL_LEN);
state->data_chunk_len += EOL_LEN;
}
if ((int) (DATA_CHUNK_SIZE - state->data_chunk_len) < 0) {
SCLogDebug("Error: Invalid Chunk length: %u",
state->data_chunk_len);
ret = MIME_DEC_ERR_PARSE;
break;
}
// data_chunk数据满了,调用函数ProcessDecodedDataChunk进行处理,这个函数
// 主要提取url并存储,最后调用了MIME解析器初始化时设置的函数,SMTP模块注册的
// 函数主要是对附件进行处理
/* If buffer full, then invoke callback */
if (DATA_CHUNK_SIZE - state->data_chunk_len < EOL_LEN + 1) {
// 调用预处理程序和回调
ret = ProcessDecodedDataChunk(state->data_chunk,
state->data_chunk_len, state);
if (ret != MIME_DEC_OK) {
SCLogDebug("Error: ProcessDecodedDataChunk() function "
"failed");
}
}
remaining -= tobuf;
offset += tobuf;
}
附件处理函数-SMTPProcessDataChunk
SMTPProcessDataChunk 函数是在初始化MIME解析器时,由 SMTPProcessRequest -> MimeDecInitParser 函数注册,然后通过 ProcessDecodedDataChunk (见上文)调用。实现对信件body数据块的处理,SMTP模块的函数中主要是完成对附件的处理。详见app-layer-smtp.c文件
处理响应消息-SMTPProcessReply
主要解析服务器的响应消息,根据SMTPState中的命令数组对响应码进行判断,也涉及到一些多模匹配的算法。由于SMTP协议的数据主要是在请求部分,所以响应部分不做详细分析。
本文参考:https://blog.csdn.net/ljq32/article/details/120225475 smtp协议解析系列文章

浙公网安备 33010602011771号