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

  1. 在进行正式的解析工作之前,先设置了一些状态标志位,主要是用来标识方向;以及将输入的数据保存到state结构体中,按行分割进行解析。

  2. SMTPGetLine 行处理函数:在收到的数据中查找分隔符\r\n,按行进行分割,state->current_line指向每行首字符,state->current_line_len记录每行数据的长度,不包含\r\n,其他状态信息也会被保存在结构体中。

  3. 根据方向不同,调用 SMTPProcessRequestSMTPProcessReply 按行处理请求和响应的数据。

/* 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个部分

  1. 第一次解析时,需要创建事务结构体并进行初始化
// 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);
}
  1. 通过字符串匹配请求命令,设置state->current_command保存当前命令状态,以及调用每个命令的解析函数,解析的主要命令如下图:(其中比较重要的是关于data命令的解析,后面单独进行讲解)

img

  1. 根据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"命令分支

  1. 匹配到"data"命令,首先会设置当前命令和解析状态,在下一步解析的时候会用到
state->current_command = SMTP_COMMAND_DATA;
  1. 验证是否开启了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);
    }
  1. 另一条分支是验证是否开启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实体

  1. 判断解析过程中是否出现异常,以及数据长度合法性
// 判断解析过程中是否遇到错误
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);
}
  1. 判断解析标志,如果是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"等。

  1. 首先通过查看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;
    }
}
  1. 检查内容中是否有附件,通过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;
        }
    }
}
  1. 检查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;
        }
    }
}
  1. 查找是否存在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数据的处理。

  1. 首先尝试寻找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;
    }
}
  1. 如果没有找到任何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;
    }
}
  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
  1. 首先对上次的数据处理结果进行保存,判断state_flag是不是BODY_END_BOUND标志,这个标志是在遇到结尾boundary分界线时设置的。

    1. 如果不是,则说明上次的数据不是结束边界,那么对信件体做结束处理。
/* 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;
    }
    1. 如果state_flag是BODY_END_BOUND标志,则说明上次的数据是结束边界,那么这个信件体的子信件体结束,设置HEADER_READY标志,准备进行下一次解析。
  1. 对当前数据(即boundary)进行分析

    1. 遇到了结尾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;
    1. 如果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;
    1. 如果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;
  1. 在经过上述的分支之后,判断状态标志。如果不是结尾boundary分界线,则设置解析标志为HEADER_READY,说明后续数据是头部字段,下次将判断此字段进入头部字段解析函数
/* After boundary look for headers */
if (state->state_flag != BODY_END_BOUND) {
    state->state_flag = HEADER_READY;
}
Body行数据处理函数-ProcessBodyLine
  1. 根据头部字段设置编码标志对数据进行解码,主要有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;
  1. 将数据存储到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协议解析系列文章

posted @ 2022-04-22 14:21  6c696e  阅读(560)  评论(0)    收藏  举报