From: David Abarbanel Date: Tue, 6 Nov 2012 14:45:36 +0000 (-0500) Subject: SMTP MIME Email Message decoder X-Git-Tag: suricata-2.1beta2~43 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=c2dc6867425ec5abb964e1625f7dfaa6fcce3d1b;p=thirdparty%2Fsuricata.git SMTP MIME Email Message decoder --- diff --git a/rules/files.rules b/rules/files.rules index 0f770902f8..467735ee04 100644 --- a/rules/files.rules +++ b/rules/files.rules @@ -43,3 +43,5 @@ # Alert and store pdf attachment but not pdf file #alert http any any -> any any (msg:"FILE pdf claimed, but not pdf"; flow:established,to_client; fileext:"pdf"; filemagic:!"PDF document"; filestore; sid:22; rev:1;) +# Alert and store files over SMTP +#alert smtp any any -> any any (msg:"File Found over SMTP and stored"; filestore; sid:27; rev:1;) diff --git a/rules/smtp-events.rules b/rules/smtp-events.rules index a20c6155a1..52b4647f96 100644 --- a/rules/smtp-events.rules +++ b/rules/smtp-events.rules @@ -16,3 +16,12 @@ alert smtp any any -> any any (msg:"SURICATA SMTP no server welcome message"; fl alert smtp any any -> any any (msg:"SURICATA SMTP tls rejected"; flow:established; app-layer-event:smtp.tls_rejected; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220007; rev:1;) alert smtp any any -> any any (msg:"SURICATA SMTP data command rejected"; flow:established,to_client; app-layer-event:smtp.data_command_rejected; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220008; rev:1;) +# SMTP MIME events +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime parser failed"; flow:established; app-layer-event:smtp.mime_parse_failed; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220009; rev:1;) +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime malformed message found"; flow:established; app-layer-event:smtp.mime_malformed_msg; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220010; rev:1;) +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime base64-decoding failed"; flow:established; app-layer-event:smtp.mime_invalid_base64; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220011; rev:1;) +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime header name len exceeded"; flow:established; app-layer-event:smtp.mime_long_header_name; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220012; rev:1;) +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime header value len exceeded"; flow:established; app-layer-event:smtp.mime_long_header_value; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220013; rev:1;) +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime quoted-printable-decoding failed"; flow:established; app-layer-event:smtp.mime_invalid_qp; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220014; rev:1;) +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime line len exceeded"; flow:established; app-layer-event:smtp.mime_long_line; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220015; rev:1;) +#alert smtp any any -> any any (msg:"SURICATA SMTP Mime encoded line len exceeded"; flow:established; app-layer-event:smtp.mime_long_enc_line; flowint:smtp.anomaly.count,+,1; classtype:protocol-command-decode; sid:2220016; rev:1;) diff --git a/src/Makefile.am b/src/Makefile.am index ee9b654e23..6d6be901fa 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -214,6 +214,7 @@ log-httplog.c log-httplog.h \ log-pcap.c log-pcap.h \ log-tcp-data.c log-tcp-data.h \ log-tlslog.c log-tlslog.h \ +mime-decode.c mime-decode.h \ output.c output.h \ output-file.c output-file.h \ output-filedata.c output-filedata.h \ @@ -283,6 +284,7 @@ tm-threads.c tm-threads.h tm-threads-common.h \ unix-manager.c unix-manager.h \ util-action.c util-action.h \ util-atomic.c util-atomic.h \ +util-base64.c util-base64.h \ util-bloomfilter-counting.c util-bloomfilter-counting.h \ util-bloomfilter.c util-bloomfilter.h \ util-buffer.c util-buffer.h \ diff --git a/src/app-layer-smtp.c b/src/app-layer-smtp.c index fc3b9f7dad..0d5ef040e8 100644 --- a/src/app-layer-smtp.c +++ b/src/app-layer-smtp.c @@ -113,6 +113,25 @@ SCEnumCharMap smtp_decoder_event_table[ ] = { SMTP_DECODER_EVENT_TLS_REJECTED }, { "DATA_COMMAND_REJECTED", SMTP_DECODER_EVENT_DATA_COMMAND_REJECTED }, + + /* MIME Events */ + { "MIME_PARSE_FAILED", + SMTP_DECODER_EVENT_MIME_PARSE_FAILED }, + { "MIME_MALFORMED_MSG", + SMTP_DECODER_EVENT_MIME_MALFORMED_MSG }, + { "MIME_INVALID_BASE64", + SMTP_DECODER_EVENT_MIME_INVALID_BASE64 }, + { "MIME_INVALID_QP", + SMTP_DECODER_EVENT_MIME_INVALID_QP }, + { "MIME_LONG_LINE", + SMTP_DECODER_EVENT_MIME_LONG_LINE }, + { "MIME_LONG_ENC_LINE", + SMTP_DECODER_EVENT_MIME_LONG_ENC_LINE }, + { "MIME_LONG_HEADER_NAME", + SMTP_DECODER_EVENT_MIME_LONG_HEADER_NAME }, + { "MIME_LONG_HEADER_VALUE", + SMTP_DECODER_EVENT_MIME_LONG_HEADER_VALUE }, + { NULL, -1 }, }; @@ -183,10 +202,185 @@ SCEnumCharMap smtp_reply_map[ ] = { { "555", SMTP_REPLY_555 }, { NULL, -1 }, }; -//static void SMTPParserReset(void) -//{ -// return; -//} + +typedef struct SMTPConfig { + + int decode_mime; + MimeDecConfig mime_config; + +} SMTPConfig; + +/* Create SMTP config structure */ +static SMTPConfig smtp_config = { 0, { 0, 0, 0, 0 } }; + +/** + * \brief Configure SMTP Mime Decoder by parsing out 'smtp-mime' section of YAML + * config file + * + * \return none + */ +static void SMTPConfigure(void) { + + SCEnter(); + int ret = 0, val; + intmax_t imval; + + ConfNode *config = ConfGetNode("smtp-mime"); + if (config != NULL) { + + ret = ConfGetChildValueBool(config, "decode-mime", &val); + if (ret) { + smtp_config.decode_mime = val; + } + + ret = ConfGetChildValueBool(config, "decode-base64", &val); + if (ret) { + smtp_config.mime_config.decode_base64 = val; + } + + ret = ConfGetChildValueBool(config, "decode-quoted-printable", &val); + if (ret) { + smtp_config.mime_config.decode_quoted_printable = val; + } + + ret = ConfGetChildValueInt(config, "header-value-depth", &imval); + if (ret) { + smtp_config.mime_config.header_value_depth = (uint32_t) imval; + } + + ret = ConfGetChildValueBool(config, "extract-urls", &val); + if (ret) { + smtp_config.mime_config.extract_urls = val; + } + } + + /* Pass mime config data to MimeDec API */ + MimeDecSetConfig(&smtp_config.mime_config); + + SCReturn; +} + +static int ProcessDataChunk(const uint8_t *chunk, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + Flow *flow = (Flow *) state->data; + SMTPState *smtp_state = (SMTPState *) flow->alstate; + MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; + FileContainer *files = NULL; + uint16_t flags = 0; + + /* Set flags */ + if (flow->flags & FLOW_FILE_NO_STORE_TS) { + flags |= FILE_NOSTORE; + } + + if (flow->flags & FLOW_FILE_NO_MAGIC_TS) { + flags |= FILE_NOMAGIC; + } + + if (flow->flags & FLOW_FILE_NO_MD5_TS) { + flags |= FILE_NOMD5; + } + + /* Determine whether to process files */ + if ((flags & (FILE_NOSTORE | FILE_NOMAGIC | FILE_NOMD5)) == + (FILE_NOSTORE | FILE_NOMAGIC | FILE_NOMD5)) { + SCLogDebug("File content ignored"); + return 0; + } + + /* Find file */ + if (entity->ctnt_flags & CTNT_IS_ATTACHMENT) { + + /* Make sure file container allocated */ + if (smtp_state->files_ts == NULL) { + smtp_state->files_ts = FileContainerAlloc(); + if (smtp_state->files_ts == NULL) { + ret = MIME_DEC_ERR_MEM; + SCLogError(SC_ERR_MEM_ALLOC, "Could not create file container"); + goto end; + } + } + files = smtp_state->files_ts; + + /* Open file if necessary */ + if (state->body_begin) { + + if (SCLogDebugEnabled()) { + SCLogDebug("Opening file...%u bytes", len); + printf("File - "); + for (uint32_t i = 0; i < entity->filename_len; i++) { + printf("%c", entity->filename[i]); + } + printf("\n"); + } + + /* Set storage flag if applicable since only the first file in the + * flow seems to be processed by the 'filestore' detector */ + if (files->head != NULL && (files->head->flags & FILE_STORE)) { + flags |= FILE_STORE; + } + + if (FileOpenFile(files, (uint8_t *) entity->filename, entity->filename_len, + (uint8_t *) chunk, len, flags) == NULL) { + ret = MIME_DEC_ERR_DATA; + SCLogDebug("FileOpenFile() failed"); + } + + /* If close in the same chunk, then pass in empty bytes */ + if (state->body_end) { + + SCLogDebug("Closing file...%u bytes", len); + + if (files->tail->state == FILE_STATE_OPENED) { + ret = FileCloseFile(files, (uint8_t *) NULL, 0, flags); + if (ret != 0) { + SCLogDebug("FileCloseFile() failed: %d", ret); + } + } else { + SCLogDebug("File already closed"); + } + } + } else if (state->body_end) { + /* Close file */ + SCLogDebug("Closing file...%u bytes", len); + + if (files->tail->state == FILE_STATE_OPENED) { + ret = FileCloseFile(files, (uint8_t *) chunk, len, flags); + if (ret != 0) { + SCLogDebug("FileCloseFile() failed: %d", ret); + } + } else { + SCLogDebug("File already closed"); + } + } else { + /* Append data chunk to file */ + SCLogDebug("Appending file...%u bytes", len); + + /* 0 is ok, -2 is not stored, -1 is error */ + ret = FileAppendData(files, (uint8_t *) chunk, len); + if (ret == -2) { + ret = 0; + SCLogDebug("FileAppendData() - file no longer being extracted"); + } else if (ret < 0) { + SCLogDebug("FileAppendData() failed: %d", ret); + } + } + + if (ret == MIME_DEC_OK) { + SCLogDebug("Successfully processed file data!"); + } + } else { + SCLogDebug("Body not a Ctnt_attachment"); + } + + if (files != NULL) { + FilePrune(files); + } +end: + SCReturnInt(ret); +} /** * \internal @@ -496,6 +690,53 @@ static int SMTPProcessCommandDATA(SMTPState *state, Flow *f, * the command buffer to be used by the reply handler to match * the reply received */ SMTPInsertCommandIntoCommandBuffer(SMTP_COMMAND_DATA_MODE, state, f); + + if (smtp_config.decode_mime) { + /* Complete parsing task */ + int ret = MimeDecParseComplete(state->mime_state); + if (ret != MIME_DEC_OK) { + + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_PARSE_FAILED); + SCLogDebug("MimeDecParseComplete() function failed"); + } + + /* Generate decoder events */ + MimeDecEntity *msg = state->mime_state->msg; + if (msg->anomaly_flags & ANOM_INVALID_BASE64) { + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_INVALID_BASE64); + } + if (msg->anomaly_flags & ANOM_INVALID_QP) { + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_INVALID_QP); + } + if (msg->anomaly_flags & ANOM_LONG_LINE) { + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_LONG_LINE); + } + if (msg->anomaly_flags & ANOM_LONG_ENC_LINE) { + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_LONG_ENC_LINE); + } + if (msg->anomaly_flags & ANOM_LONG_HEADER_NAME) { + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_LONG_HEADER_NAME); + } + if (msg->anomaly_flags & ANOM_LONG_HEADER_VALUE) { + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_LONG_HEADER_VALUE); + } + if (msg->anomaly_flags & ANOM_MALFORMED_MSG) { + AppLayerDecoderEventsSetEvent(f, SMTP_DECODER_EVENT_MIME_MALFORMED_MSG); + } + } + } + + /* If DATA, then parse out a MIME message */ + if (state->current_command == SMTP_COMMAND_DATA && + (state->parser_state & SMTP_PARSER_STATE_COMMAND_DATA_MODE)) { + + if (smtp_config.decode_mime) { + int ret = MimeDecParseLine((const char *) state->current_line, + state->current_line_len, state->mime_state); + if (ret != MIME_DEC_OK) { + SCLogDebug("MimeDecParseLine() function returned an error code: %d", ret); + } + } } return 0; @@ -665,6 +906,30 @@ static int SMTPProcessRequest(SMTPState *state, Flow *f, } else if (state->current_line_len >= 4 && SCMemcmpLowercase("data", state->current_line, 4) == 0) { state->current_command = SMTP_COMMAND_DATA; + + if (smtp_config.decode_mime) { + /* Re-init the MIME parser */ + if (state->mime_state != NULL) { + MimeDecDeInitParser(state->mime_state); + } + state->mime_state = MimeDecInitParser(f, ProcessDataChunk); + if (state->mime_state == NULL) { + SCLogError(SC_ERR_MEM_ALLOC, "MimeDecInitParser() failed to " + "allocate data"); + return MIME_DEC_ERR_MEM; + } + + /* Add new MIME message to end of list */ + if (state->msg_head == NULL) { + state->msg_head = state->mime_state->msg; + state->msg_tail = state->mime_state->msg; + } + else { + state->msg_tail->next = state->mime_state->msg; + state->msg_tail = state->mime_state->msg; + } + } + } else if (state->current_line_len >= 4 && SCMemcmpLowercase("bdat", state->current_line, 4) == 0) { r = SMTPParseCommandBDAT(state); @@ -821,6 +1086,16 @@ static void SMTPStateFree(void *p) SCFree(smtp_state->tc_db); } + FileContainerFree(smtp_state->files_ts); + + /* Free MIME parser */ + if (smtp_state->mime_state != NULL) { + MimeDecDeInitParser(smtp_state->mime_state); + } + + /* Free list of MIME message recursively */ + MimeDecFreeEntity(smtp_state->msg_head); + SCFree(smtp_state); return; @@ -927,6 +1202,8 @@ void RegisterSMTPParsers(void) SMTPSetMpmState(); + SMTPConfigure(); + #ifdef UNITTESTS AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_SMTP, SMTPParserRegisterTests); #endif @@ -3614,6 +3891,677 @@ end: return result; } +/** + * \test Test DATA command w/MIME message. + */ +int SMTPParserTest14(void) +{ + int result = 0; + Flow f; + int r = 0; + + /* 220 mx.google.com ESMTP d15sm986283wfl.6 */ + uint8_t welcome_reply[] = { + 0x32, 0x32, 0x30, 0x20, 0x6d, 0x78, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, + 0x6d, 0x20, 0x45, 0x53, 0x4d, 0x54, 0x50, 0x20, + 0x64, 0x31, 0x35, 0x73, 0x6d, 0x39, 0x38, 0x36, + 0x32, 0x38, 0x33, 0x77, 0x66, 0x6c, 0x2e, 0x36, + 0x0d, 0x0a + }; + uint32_t welcome_reply_len = sizeof(welcome_reply); + + /* EHLO boo.com */ + uint8_t request1[] = { + 0x45, 0x48, 0x4c, 0x4f, 0x20, 0x62, 0x6f, 0x6f, + 0x2e, 0x63, 0x6f, 0x6d, 0x0d, 0x0a + }; + uint32_t request1_len = sizeof(request1); + /* 250-mx.google.com at your service, [117.198.115.50] + * 250-SIZE 35882577 + * 250-8BITMIME + * 250-STARTTLS + * 250 ENHANCEDSTATUSCODES + */ + uint8_t reply1[] = { + 0x32, 0x35, 0x30, 0x2d, 0x70, 0x6f, 0x6f, 0x6e, + 0x61, 0x5f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x5f, + 0x76, 0x6d, 0x31, 0x2e, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x0d, + 0x0a, 0x32, 0x35, 0x30, 0x2d, 0x50, 0x49, 0x50, + 0x45, 0x4c, 0x49, 0x4e, 0x49, 0x4e, 0x47, 0x0d, + 0x0a, 0x32, 0x35, 0x30, 0x2d, 0x53, 0x49, 0x5a, + 0x45, 0x20, 0x31, 0x30, 0x32, 0x34, 0x30, 0x30, + 0x30, 0x30, 0x0d, 0x0a, 0x32, 0x35, 0x30, 0x2d, + 0x56, 0x52, 0x46, 0x59, 0x0d, 0x0a, 0x32, 0x35, + 0x30, 0x2d, 0x45, 0x54, 0x52, 0x4e, 0x0d, 0x0a, + 0x32, 0x35, 0x30, 0x2d, 0x45, 0x4e, 0x48, 0x41, + 0x4e, 0x43, 0x45, 0x44, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x43, 0x4f, 0x44, 0x45, 0x53, 0x0d, + 0x0a, 0x32, 0x35, 0x30, 0x2d, 0x38, 0x42, 0x49, + 0x54, 0x4d, 0x49, 0x4d, 0x45, 0x0d, 0x0a, 0x32, + 0x35, 0x30, 0x20, 0x44, 0x53, 0x4e, 0x0d, 0x0a + }; + uint32_t reply1_len = sizeof(reply1); + + /* MAIL FROM:asdff@asdf.com */ + uint8_t request2[] = { + 0x4d, 0x41, 0x49, 0x4c, 0x20, 0x46, 0x52, 0x4f, + 0x4d, 0x3a, 0x61, 0x73, 0x64, 0x66, 0x66, 0x40, + 0x61, 0x73, 0x64, 0x66, 0x2e, 0x63, 0x6f, 0x6d, + 0x0d, 0x0a + }; + uint32_t request2_len = sizeof(request2); + /* 250 2.1.0 Ok */ + uint8_t reply2[] = { + 0x32, 0x35, 0x30, 0x20, 0x32, 0x2e, 0x31, 0x2e, + 0x30, 0x20, 0x4f, 0x6b, 0x0d, 0x0a + }; + uint32_t reply2_len = sizeof(reply2); + + /* RCPT TO:bimbs@gmail.com */ + uint8_t request3[] = { + 0x52, 0x43, 0x50, 0x54, 0x20, 0x54, 0x4f, 0x3a, + 0x62, 0x69, 0x6d, 0x62, 0x73, 0x40, 0x67, 0x6d, + 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x0d, + 0x0a + }; + uint32_t request3_len = sizeof(request3); + /* 250 2.1.5 Ok */ + uint8_t reply3[] = { + 0x32, 0x35, 0x30, 0x20, 0x32, 0x2e, 0x31, 0x2e, + 0x35, 0x20, 0x4f, 0x6b, 0x0d, 0x0a + }; + uint32_t reply3_len = sizeof(reply3); + + /* DATA */ + uint8_t request4[] = { + 0x44, 0x41, 0x54, 0x41, 0x0d, 0x0a + }; + uint32_t request4_len = sizeof(request4); + /* 354 End data with .|| */ + uint8_t reply4[] = { + 0x33, 0x35, 0x34, 0x20, 0x45, 0x6e, 0x64, 0x20, + 0x64, 0x61, 0x74, 0x61, 0x20, 0x77, 0x69, 0x74, + 0x68, 0x20, 0x3c, 0x43, 0x52, 0x3e, 0x3c, 0x4c, + 0x46, 0x3e, 0x2e, 0x3c, 0x43, 0x52, 0x3e, 0x3c, + 0x4c, 0x46, 0x3e, 0x0d, 0x0a + }; + uint32_t reply4_len = sizeof(reply4); + + /* MIME_MSG */ + uint64_t filesize = 133; + uint8_t request4_msg[] = { + 0x4D, 0x49, 0x4D, 0x45, 0x2D, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x31, 0x2E, + 0x30, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, + 0x6E, 0x74, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x3A, + 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6F, 0x63, 0x74, + 0x65, 0x74, 0x2D, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6D, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, + 0x6E, 0x74, 0x2D, 0x54, 0x72, 0x61, 0x6E, 0x73, + 0x66, 0x65, 0x72, 0x2D, 0x45, 0x6E, 0x63, 0x6F, + 0x64, 0x69, 0x6E, 0x67, 0x3A, 0x20, 0x62, 0x61, + 0x73, 0x65, 0x36, 0x34, 0x0D, 0x0A, 0x43, 0x6F, + 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x2D, 0x44, 0x69, + 0x73, 0x70, 0x6F, 0x73, 0x69, 0x74, 0x69, 0x6F, + 0x6E, 0x3A, 0x20, 0x61, 0x74, 0x74, 0x61, 0x63, + 0x68, 0x6D, 0x65, 0x6E, 0x74, 0x3B, 0x20, 0x66, + 0x69, 0x6C, 0x65, 0x6E, 0x61, 0x6D, 0x65, 0x3D, + 0x22, 0x74, 0x65, 0x73, 0x74, 0x2E, 0x65, 0x78, + 0x65, 0x22, 0x3B, 0x0D, 0x0A, 0x0D, 0x0A, 0x54, + 0x56, 0x6F, 0x41, 0x41, 0x46, 0x42, 0x46, 0x41, + 0x41, 0x42, 0x4D, 0x41, 0x51, 0x45, 0x41, 0x61, + 0x69, 0x70, 0x59, 0x77, 0x77, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x42, + 0x41, 0x41, 0x44, 0x41, 0x51, 0x73, 0x42, 0x43, + 0x41, 0x41, 0x42, 0x41, 0x41, 0x43, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x48, 0x6B, 0x41, 0x41, + 0x41, 0x41, 0x4D, 0x41, 0x41, 0x41, 0x41, 0x65, + 0x51, 0x41, 0x41, 0x41, 0x41, 0x77, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x45, 0x41, 0x41, 0x42, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x51, 0x41, 0x41, + 0x41, 0x42, 0x30, 0x41, 0x41, 0x41, 0x41, 0x49, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x51, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x42, + 0x41, 0x45, 0x41, 0x41, 0x49, 0x67, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x67, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x42, 0x63, 0x58, 0x44, 0x59, 0x32, 0x4C, + 0x6A, 0x6B, 0x7A, 0x4C, 0x6A, 0x59, 0x34, 0x4C, + 0x6A, 0x5A, 0x63, 0x65, 0x67, 0x41, 0x41, 0x4F, + 0x41, 0x3D, 0x3D, 0x0D,0x0A }; + uint32_t request4_msg_len = sizeof(request4_msg); + + /* DATA COMPLETED */ + uint8_t request4_end[] = { + 0x0d, 0x0a, 0x2e, 0x0d, 0x0a + }; + uint32_t request4_end_len = sizeof(request4_end); + /* 250 2.0.0 Ok: queued as 6A1AF20BF2 */ + uint8_t reply4_end[] = { + 0x32, 0x35, 0x30, 0x20, 0x32, 0x2e, 0x30, 0x2e, + 0x30, 0x20, 0x4f, 0x6b, 0x3a, 0x20, 0x71, 0x75, + 0x65, 0x75, 0x65, 0x64, 0x20, 0x61, 0x73, 0x20, + 0x36, 0x41, 0x31, 0x41, 0x46, 0x32, 0x30, 0x42, + 0x46, 0x32, 0x0d, 0x0a + }; + uint32_t reply4_end_len = sizeof(reply4_end); + + /* QUIT */ + uint8_t request5[] = { + 0x51, 0x55, 0x49, 0x54, 0x0d, 0x0a + }; + uint32_t request5_len = sizeof(request5); + /* 221 2.0.0 Bye */ + uint8_t reply5[] = { + 0x32, 0x32, 0x31, 0x20, 0x32, 0x2e, 0x30, 0x2e, + 0x30, 0x20, 0x42, 0x79, 0x65, 0x0d, 0x0a + }; + uint32_t reply5_len = sizeof(reply5); + + TcpSession ssn; + + memset(&f, 0, sizeof(f)); + memset(&ssn, 0, sizeof(ssn)); + + FLOW_INITIALIZE(&f); + f.protoctx = (void *)&ssn; + + StreamTcpInitConfig(TRUE); + void *thread_local_data = SMTPLocalStorageAlloc(); + + /* EHLO Request */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOSERVER, + request1, request1_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + SMTPState *smtp_state = f.alstate; + if (smtp_state == NULL) { + printf("no smtp state: "); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 1 || + smtp_state->cmds_idx != 0 || + smtp_state->cmds[0] != SMTP_COMMAND_OTHER_CMD || + smtp_state->parser_state != 0) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* Welcome reply */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOCLIENT, + welcome_reply, welcome_reply_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 1 || + smtp_state->cmds_idx != 0 || + smtp_state->cmds[0] != SMTP_COMMAND_OTHER_CMD || + smtp_state->parser_state != SMTP_PARSER_STATE_FIRST_REPLY_SEEN) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* EHLO Reply */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOCLIENT, + reply1, reply1_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 0 || + smtp_state->cmds_idx != 0 || + smtp_state->parser_state != SMTP_PARSER_STATE_FIRST_REPLY_SEEN) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* MAIL FROM Request */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOSERVER, + request2, request2_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 1 || + smtp_state->cmds_idx != 0 || + smtp_state->cmds[0] != SMTP_COMMAND_OTHER_CMD || + smtp_state->parser_state != SMTP_PARSER_STATE_FIRST_REPLY_SEEN) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* MAIL FROM Reply */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOCLIENT, + reply2, reply2_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 0 || + smtp_state->cmds_idx != 0 || + smtp_state->parser_state != (SMTP_PARSER_STATE_FIRST_REPLY_SEEN)) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* RCPT TO Request */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOSERVER, + request3, request3_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 1 || + smtp_state->cmds_idx != 0 || + smtp_state->cmds[0] != SMTP_COMMAND_OTHER_CMD || + smtp_state->parser_state != SMTP_PARSER_STATE_FIRST_REPLY_SEEN) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* RCPT TO Reply */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOCLIENT, + reply3, reply3_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 0 || + smtp_state->cmds_idx != 0 || + smtp_state->parser_state != (SMTP_PARSER_STATE_FIRST_REPLY_SEEN)) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* Enable mime decoding */ + smtp_config.decode_mime = 1; + smtp_config.mime_config.decode_base64 = 1; + smtp_config.mime_config.decode_quoted_printable = 1; + MimeDecSetConfig(&smtp_config.mime_config); + + /* DATA request */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOSERVER, + request4, request4_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 1 || + smtp_state->cmds_idx != 0 || + smtp_state->cmds[0] != SMTP_COMMAND_DATA || + smtp_state->parser_state != SMTP_PARSER_STATE_FIRST_REPLY_SEEN) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* Data reply */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOCLIENT, + reply4, reply4_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 0 || + smtp_state->cmds_idx != 0 || + smtp_state->parser_state != (SMTP_PARSER_STATE_FIRST_REPLY_SEEN | + SMTP_PARSER_STATE_COMMAND_DATA_MODE)) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* DATA message */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOSERVER, + request4_msg, request4_msg_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 0 || + smtp_state->cmds_idx != 0 || + smtp_state->mime_state == NULL || smtp_state->msg_head == NULL || /* MIME data structures */ + smtp_state->parser_state != (SMTP_PARSER_STATE_FIRST_REPLY_SEEN | + SMTP_PARSER_STATE_COMMAND_DATA_MODE)) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* DATA . request */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOSERVER, + request4_end, request4_end_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 1 || + smtp_state->cmds_idx != 0 || + smtp_state->cmds[0] != SMTP_COMMAND_DATA_MODE || + smtp_state->mime_state == NULL || smtp_state->msg_head == NULL || /* MIME data structures */ + smtp_state->parser_state != (SMTP_PARSER_STATE_FIRST_REPLY_SEEN)) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + SMTPState *state = (SMTPState *) f.alstate; + FileContainer *files = state->files_ts; + if (files != NULL && files->head != NULL) { + File *file = files->head; + + if(strncmp((const char *)file->name, "test.exe", 8) != 0){ + printf("smtp-mime file name is incorrect"); + goto end; + } + if(file->size != filesize){ + printf("smtp-mime file size %lu is incorrect", file->size); + goto end; + } + uint8_t org_binary[] = {0x4D, 0x5A, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, + 0x4C, 0x01, 0x01, 0x00, 0x6A, 0x2A, 0x58, 0xC3, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x03, 0x01, 0x0B, 0x01, 0x08, 0x00, + 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x79, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x79, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x5C, 0x5C, 0x36, 0x36, + 0x2E, 0x39, 0x33, 0x2E, 0x36, 0x38, 0x2E, 0x36, + 0x5C, 0x7A, 0x00, 0x00, 0x38,}; + uint64_t z; + for (z=0; z <= filesize; z++){ + if(org_binary[z] != file->chunks_head->data[z]){ + printf("smtp-mime file data incorrect\n"); + goto end; + } + } + } + + /* DATA . reply */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOCLIENT, + reply4_end, reply4_end_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 0 || + smtp_state->cmds_idx != 0 || + smtp_state->parser_state != (SMTP_PARSER_STATE_FIRST_REPLY_SEEN)) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* QUIT Request */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOSERVER, + request5, request5_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 1 || + smtp_state->cmds_idx != 0 || + smtp_state->cmds[0] != SMTP_COMMAND_OTHER_CMD || + smtp_state->parser_state != SMTP_PARSER_STATE_FIRST_REPLY_SEEN) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + /* QUIT Reply */ + r = AppLayerParse(thread_local_data, &f, ALPROTO_SMTP, STREAM_TOCLIENT, + reply5, reply5_len); + if (r != 0) { + printf("smtp check returned %" PRId32 ", expected 0: ", r); + goto end; + } + if (smtp_state->input_len != 0 || + smtp_state->cmds_cnt != 0 || + smtp_state->cmds_idx != 0 || + smtp_state->parser_state != (SMTP_PARSER_STATE_FIRST_REPLY_SEEN)) { + printf("smtp parser in inconsistent state\n"); + goto end; + } + + result = 1; + end: + StreamTcpFreeConfig(TRUE); + FLOW_DESTROY(&f); + SMTPLocalStorageFree(thread_local_data); + return result; +} + +int SMTPProcessDataChunkTest01(void){ + Flow f; + FLOW_INITIALIZE(&f); + f.flags = FLOW_FILE_NO_STORE_TS; + MimeDecParseState *state = MimeDecInitParser(&f, NULL); + int ret; + ret = ProcessDataChunk(NULL, 0, state); + + return ret; +} + + +int SMTPProcessDataChunkTest02(void){ + char mimemsg[] = {0x4D, 0x49, 0x4D, 0x45, 0x2D, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x31, 0x2E, + 0x30, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, + 0x6E, 0x74, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x3A, + 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6F, 0x63, 0x74, + 0x65, 0x74, 0x2D, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6D, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, + 0x6E, 0x74, 0x2D, 0x54, 0x72, 0x61, 0x6E, 0x73, + 0x66, 0x65, 0x72, 0x2D, 0x45, 0x6E, 0x63, 0x6F, + 0x64, 0x69, 0x6E, 0x67, 0x3A, 0x20, 0x62, 0x61, + 0x73, 0x65, 0x36, 0x34, 0x0D, 0x0A, 0x43, 0x6F, + 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x2D, 0x44, 0x69, + 0x73, 0x70, 0x6F, 0x73, 0x69, 0x74, 0x69, 0x6F, + 0x6E, 0x3A, 0x20, 0x61, 0x74, 0x74, 0x61, 0x63, + 0x68, 0x6D, 0x65, 0x6E, 0x74, 0x3B, 0x20, 0x66, + 0x69, 0x6C, 0x65, 0x6E, 0x61, 0x6D, 0x65, 0x3D, + 0x22, 0x74, 0x65, 0x73, 0x74, 0x2E, 0x65, 0x78, + 0x65, 0x22, 0x3B, 0x0D, 0x0A, 0x0D, 0x0A, 0x54, + 0x56, 0x6F, 0x41, 0x41, 0x46, 0x42, 0x46, 0x41, + 0x41, 0x42, 0x4D, 0x41, 0x51, 0x45, 0x41, 0x61, + 0x69, 0x70, 0x59, 0x77, 0x77, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x42, + 0x41, 0x41, 0x44, 0x41, 0x51, 0x73, 0x42, 0x43, + 0x41, 0x41, 0x42, 0x41, 0x41, 0x43, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x48, 0x6B, 0x41, 0x41, + 0x41, 0x41, 0x4D, 0x41, 0x41, 0x41, 0x41, 0x65, + 0x51, 0x41, 0x41, 0x41, 0x41, 0x77, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x45, 0x41, 0x41, 0x42, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x51, 0x41, 0x41, + 0x41, 0x42, 0x30, 0x41, 0x41, 0x41, 0x41, 0x49, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x51, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x42, + 0x41, 0x45, 0x41, 0x41, 0x49, 0x67, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x67, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x42, 0x63, 0x58, 0x44, 0x59, 0x32, 0x4C, + 0x6A, 0x6B, 0x7A, 0x4C, 0x6A, 0x59, 0x34, 0x4C, + 0x6A, 0x5A, 0x63, 0x65, 0x67, 0x41, 0x41, 0x4F, + 0x41, 0x3D, 0x3D, 0x0D, 0x0A,}; + + Flow f; + FLOW_INITIALIZE(&f); + f.alstate = SMTPStateAlloc(); + MimeDecParseState *state = MimeDecInitParser(&f, NULL); + ((MimeDecEntity *)state->stack->top->data)->ctnt_flags = CTNT_IS_ATTACHMENT; + state->body_begin = 1; + int ret; + ret = ProcessDataChunk((uint8_t *)mimemsg, sizeof(mimemsg), state); + + + return ret; +} + + + +int SMTPProcessDataChunkTest03(void){ + char mimemsg[] = {0x4D, 0x49, 0x4D, 0x45, 0x2D, 0x56, 0x65, 0x72, }; + char mimemsg2[] = {0x73, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x31, 0x2E, }; + char mimemsg3[] = {0x30, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, }; + char mimemsg4[] = {0x6E, 0x74, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x3A, }; + char mimemsg5[] = {0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, }; + char mimemsg6[] = {0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6F, 0x63, 0x74, }; + char mimemsg7[] = {0x65, 0x74, 0x2D, 0x73, 0x74, 0x72, 0x65, 0x61, }; + char mimemsg8[] = {0x6D, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, }; + char mimemsg9[] = {0x6E, 0x74, 0x2D, 0x54, 0x72, 0x61, 0x6E, 0x73, }; + char mimemsg10[] = {0x66, 0x65, 0x72, 0x2D, 0x45, 0x6E, 0x63, 0x6F, }; + char mimemsg11[] = {0x64, 0x69, 0x6E, 0x67, 0x3A, 0x20, 0x62, 0x61, }; + char mimemsg12[] = {0x73, 0x65, 0x36, 0x34, 0x0D, 0x0A, 0x43, 0x6F, }; + + Flow f; + FLOW_INITIALIZE(&f); + f.alstate = SMTPStateAlloc(); + MimeDecParseState *state = MimeDecInitParser(&f, NULL); + ((MimeDecEntity *)state->stack->top->data)->ctnt_flags = CTNT_IS_ATTACHMENT; + int ret; + + state->body_begin = 1; + ret = ProcessDataChunk((uint8_t *)mimemsg, sizeof(mimemsg), state); + if(ret) goto end; + state->body_begin = 0; + ret = ProcessDataChunk((uint8_t *)mimemsg2, sizeof(mimemsg2), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg3, sizeof(mimemsg3), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg4, sizeof(mimemsg4), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg5, sizeof(mimemsg5), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg6, sizeof(mimemsg6), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg7, sizeof(mimemsg7), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg8, sizeof(mimemsg8), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg9, sizeof(mimemsg9), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg10, sizeof(mimemsg10), state); + if(ret) goto end; + ret = ProcessDataChunk((uint8_t *)mimemsg11, sizeof(mimemsg11), state); + if(ret) goto end; + state->body_end = 1; + ret = ProcessDataChunk((uint8_t *)mimemsg12, sizeof(mimemsg12), state); + if(ret) goto end; + + end: + return ret; +} + + +int SMTPProcessDataChunkTest04(void){ + char mimemsg[] = {0x4D, 0x49, 0x4D, 0x45, 0x2D, 0x56, 0x65, 0x72, }; + char mimemsg2[] = {0x73, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x31, 0x2E, }; + char mimemsg3[] = {0x30, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, }; + char mimemsg4[] = {0x6E, 0x74, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x3A, }; + char mimemsg5[] = {0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, }; + char mimemsg6[] = {0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6F, 0x63, 0x74, }; + char mimemsg7[] = {0x65, 0x74, 0x2D, 0x73, 0x74, 0x72, 0x65, 0x61, }; + char mimemsg8[] = {0x6D, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, }; + char mimemsg9[] = {0x6E, 0x74, 0x2D, 0x54, 0x72, 0x61, 0x6E, 0x73, }; + char mimemsg10[] = {0x66, 0x65, 0x72, 0x2D, 0x45, 0x6E, 0x63, 0x6F, }; + char mimemsg11[] = {0x64, 0x69, 0x6E, 0x67, 0x3A, 0x20, 0x62, 0x61, }; + + Flow f; + FLOW_INITIALIZE(&f); + f.alstate = SMTPStateAlloc(); + MimeDecParseState *state = MimeDecInitParser(&f, NULL); + ((MimeDecEntity *)state->stack->top->data)->ctnt_flags = CTNT_IS_ATTACHMENT; + int ret = MIME_DEC_OK; + + state->body_begin = 1; + if(ProcessDataChunk((uint8_t *)mimemsg, sizeof(mimemsg), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg2, sizeof(mimemsg2), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg3, sizeof(mimemsg3), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg4, sizeof(mimemsg4), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg5, sizeof(mimemsg5), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg6, sizeof(mimemsg6), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg7, sizeof(mimemsg7), state) != 0) goto end; + state->body_begin = 0; + state->body_end = 1; + if(ProcessDataChunk((uint8_t *)mimemsg8, sizeof(mimemsg8), state) != 0) goto end; + state->body_end = 0; + if(ProcessDataChunk((uint8_t *)mimemsg9, sizeof(mimemsg9), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg10, sizeof(mimemsg10), state) != 0) goto end; + if(ProcessDataChunk((uint8_t *)mimemsg11, sizeof(mimemsg11), state) != 0) goto end; + + end: + return ret; +} + +int SMTPProcessDataChunkTest05(void){ + char mimemsg[] = {0x4D, 0x49, 0x4D, 0x45, 0x2D, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x31, 0x2E, + 0x30, 0x0D, 0x0A, 0x43, 0x6F, 0x6E, 0x74, 0x65, + 0x6E, 0x74, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x3A, + 0x6A, 0x6B, 0x7A, 0x4C, 0x6A, 0x59, 0x34, 0x4C, + 0x6A, 0x5A, 0x63, 0x65, 0x67, 0x41, 0x41, 0x4F, + 0x41, 0x3D, 0x3D, 0x0D, 0x0A,}; + + Flow f; + FLOW_INITIALIZE(&f); + f.alstate = SMTPStateAlloc(); + MimeDecParseState *state = MimeDecInitParser(&f, NULL); + ((MimeDecEntity *)state->stack->top->data)->ctnt_flags = CTNT_IS_ATTACHMENT; + state->body_begin = 1; + int ret; + uint64_t file_size = 0; + ret = ProcessDataChunk((uint8_t *)mimemsg, sizeof(mimemsg), state); + state->body_begin = 0; + if(ret){goto end;} + SMTPState *smtp_state = (SMTPState *)((Flow *)state->data)->alstate; + FileContainer *files = smtp_state->files_ts; + File *file = files->head; + file_size = file->size; + + FileDisableStoring(&f, STREAM_TOSERVER); + FileDisableMagic(&f, STREAM_TOSERVER); + FileDisableMd5(&f, STREAM_TOSERVER); + ret = ProcessDataChunk((uint8_t *)mimemsg, sizeof(mimemsg), state); + if(ret){goto end;} + printf("%u\t%u\n", (uint32_t) file->size, (uint32_t) file_size); + if(file->size == file_size){ + return 0; + }else{ + return 1; + } + + end: + return ret; +} + #endif /* UNITTESTS */ void SMTPParserRegisterTests(void) @@ -3632,6 +4580,12 @@ void SMTPParserRegisterTests(void) UtRegisterTest("SMTPParserTest11", SMTPParserTest11, 1); UtRegisterTest("SMTPParserTest12", SMTPParserTest12, 1); UtRegisterTest("SMTPParserTest13", SMTPParserTest13, 1); + UtRegisterTest("SMTPParserTest14", SMTPParserTest14, 1); + UtRegisterTest("SMTPProcessDataChunkTest01", SMTPProcessDataChunkTest01, 0); + UtRegisterTest("SMTPProcessDataChunkTest02", SMTPProcessDataChunkTest02, 0); + UtRegisterTest("SMTPProcessDataChunkTest03", SMTPProcessDataChunkTest03, 0); + UtRegisterTest("SMTPProcessDataChunkTest04", SMTPProcessDataChunkTest04, 0); + UtRegisterTest("SMTPProcessDataChunkTest05", SMTPProcessDataChunkTest05, 0); #endif /* UNITTESTS */ return; diff --git a/src/app-layer-smtp.h b/src/app-layer-smtp.h index 0a29a35b37..94d8eb8404 100644 --- a/src/app-layer-smtp.h +++ b/src/app-layer-smtp.h @@ -25,6 +25,7 @@ #define __APP_LAYER_SMTP_H__ #include "decode-events.h" +#include "mime-decode.h" enum { SMTP_DECODER_EVENT_INVALID_REPLY, @@ -36,6 +37,16 @@ enum { SMTP_DECODER_EVENT_NO_SERVER_WELCOME_MESSAGE, SMTP_DECODER_EVENT_TLS_REJECTED, SMTP_DECODER_EVENT_DATA_COMMAND_REJECTED, + + /* MIME Events */ + SMTP_DECODER_EVENT_MIME_PARSE_FAILED, + SMTP_DECODER_EVENT_MIME_MALFORMED_MSG, + SMTP_DECODER_EVENT_MIME_INVALID_BASE64, + SMTP_DECODER_EVENT_MIME_INVALID_QP, + SMTP_DECODER_EVENT_MIME_LONG_LINE, + SMTP_DECODER_EVENT_MIME_LONG_ENC_LINE, + SMTP_DECODER_EVENT_MIME_LONG_HEADER_NAME, + SMTP_DECODER_EVENT_MIME_LONG_HEADER_VALUE, }; typedef struct SMTPState_ { @@ -89,6 +100,16 @@ typedef struct SMTPState_ { * handler */ uint16_t cmds_idx; + /* SMTP Mime decoding and file extraction */ + /** the list of files sent to the server */ + FileContainer *files_ts; + /** the first message contained in the session */ + MimeDecEntity *msg_head; + /** the last message contained in the session */ + MimeDecEntity *msg_tail; + /** the mime decoding parser state */ + MimeDecParseState *mime_state; + } SMTPState; void RegisterSMTPParsers(void); diff --git a/src/detect-engine-file.c b/src/detect-engine-file.c index ca502c9b75..655adffcaa 100644 --- a/src/detect-engine-file.c +++ b/src/detect-engine-file.c @@ -51,6 +51,7 @@ #include "app-layer-smb.h" #include "app-layer-dcerpc-common.h" #include "app-layer-dcerpc.h" +#include "app-layer-smtp.h" #include "util-unittest.h" #include "util-unittest-helper.h" @@ -240,3 +241,48 @@ int DetectFileInspectHttp(ThreadVars *tv, return r; } + +/** + * \brief Inspect the file inspecting keywords against the SMTP transactions. + * + * \param tv thread vars + * \param det_ctx detection engine thread ctx + * \param f flow + * \param s signature to inspect + * \param alstate state + * \param flags direction flag + * + * \retval 0 no match + * \retval 1 match + * \retval 2 can't match + * \retval 3 can't match filestore signature + * + * \note flow is not locked at this time + */ +int DetectFileInspectSmtp(ThreadVars *tv, + DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx, + Signature *s, Flow *f, uint8_t flags, void *alstate, + void *tx, uint64_t tx_id) +{ + SCEnter(); + + int r = 0; + SMTPState *smtp_state = NULL; + FileContainer *ffc; + + smtp_state = (SMTPState *)alstate; + if (smtp_state == NULL) { + SCLogDebug("no SMTP state"); + goto end; + } + + if (flags & STREAM_TOSERVER) + ffc = smtp_state->files_ts; + else + goto end; + + r = DetectFileInspect(tv, det_ctx, f, s, flags, ffc); + +end: + SCReturnInt(r); +} diff --git a/src/detect-engine-file.h b/src/detect-engine-file.h index 91e2f21ebf..365ed8a99f 100644 --- a/src/detect-engine-file.h +++ b/src/detect-engine-file.h @@ -28,4 +28,10 @@ int DetectFileInspectHttp(ThreadVars *tv, DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx, Signature *s, Flow *f, uint8_t flags, void *alstate, void *tx, uint64_t tx_id); + +int DetectFileInspectSmtp(ThreadVars *tv, DetectEngineCtx *de_ctx, + DetectEngineThreadCtx *det_ctx, Signature *s, + Flow *f, uint8_t flags, void *alstate, + void *tx, uint64_t tx_id); + #endif /* __DETECT_ENGINE_FILE_H__ */ diff --git a/src/detect-fileext.c b/src/detect-fileext.c index 98b8bcb90f..7b5e9103d4 100644 --- a/src/detect-fileext.c +++ b/src/detect-fileext.c @@ -211,14 +211,15 @@ static int DetectFileextSetup (DetectEngineCtx *de_ctx, Signature *s, char *str) SigMatchAppendSMToList(s, sm, DETECT_SM_LIST_FILEMATCH); - - if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_HTTP) { + if (s->alproto != ALPROTO_HTTP && s->alproto != ALPROTO_SMTP) { SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting keywords."); goto error; } - AppLayerHtpNeedFileInspection(); - s->alproto = ALPROTO_HTTP; + if (s->alproto == ALPROTO_HTTP) { + AppLayerHtpNeedFileInspection(); + } + s->file_flags |= (FILE_SIG_NEED_FILE|FILE_SIG_NEED_FILENAME); return 0; diff --git a/src/detect-filemagic.c b/src/detect-filemagic.c index 9edecd2cfc..5dd28ec819 100644 --- a/src/detect-filemagic.c +++ b/src/detect-filemagic.c @@ -392,15 +392,14 @@ static int DetectFilemagicSetup (DetectEngineCtx *de_ctx, Signature *s, char *st SigMatchAppendSMToList(s, sm, DETECT_SM_LIST_FILEMATCH); - if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_HTTP) { + if (s->alproto != ALPROTO_HTTP && s->alproto != ALPROTO_SMTP) { SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting keywords."); goto error; } - AppLayerHtpNeedFileInspection(); - - /** \todo remove this once we support more than http */ - s->alproto = ALPROTO_HTTP; + if (s->alproto == ALPROTO_HTTP) { + AppLayerHtpNeedFileInspection(); + } s->file_flags |= (FILE_SIG_NEED_FILE|FILE_SIG_NEED_MAGIC); return 0; diff --git a/src/detect-filemd5.c b/src/detect-filemd5.c index a81ba25d6d..1bb0cf3ac8 100644 --- a/src/detect-filemd5.c +++ b/src/detect-filemd5.c @@ -324,15 +324,14 @@ static int DetectFileMd5Setup (DetectEngineCtx *de_ctx, Signature *s, char *str) SigMatchAppendSMToList(s, sm, DETECT_SM_LIST_FILEMATCH); - if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_HTTP) { + if (s->alproto != ALPROTO_HTTP && s->alproto != ALPROTO_SMTP) { SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting keywords."); goto error; } - AppLayerHtpNeedFileInspection(); - - /** \todo remove this once we support more than http */ - s->alproto = ALPROTO_HTTP; + if (s->alproto == ALPROTO_HTTP) { + AppLayerHtpNeedFileInspection(); + } s->file_flags |= (FILE_SIG_NEED_FILE|FILE_SIG_NEED_MD5); return 0; diff --git a/src/detect-filename.c b/src/detect-filename.c index c8cea7f359..06ccc70bd0 100644 --- a/src/detect-filename.c +++ b/src/detect-filename.c @@ -215,14 +215,14 @@ static int DetectFilenameSetup (DetectEngineCtx *de_ctx, Signature *s, char *str SigMatchAppendSMToList(s, sm, DETECT_SM_LIST_FILEMATCH); - if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_HTTP) { + if (s->alproto != ALPROTO_HTTP && s->alproto != ALPROTO_SMTP) { SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting keywords."); goto error; } - AppLayerHtpNeedFileInspection(); - - s->alproto = ALPROTO_HTTP; + if (s->alproto == ALPROTO_HTTP) { + AppLayerHtpNeedFileInspection(); + } s->file_flags |= (FILE_SIG_NEED_FILE|FILE_SIG_NEED_FILENAME); return 0; diff --git a/src/detect-filesize.c b/src/detect-filesize.c index d8c49bcf78..e826e3c9b5 100644 --- a/src/detect-filesize.c +++ b/src/detect-filesize.c @@ -310,15 +310,14 @@ static int DetectFilesizeSetup (DetectEngineCtx *de_ctx, Signature *s, char *str SigMatchAppendSMToList(s, sm, DETECT_SM_LIST_FILEMATCH); - if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_HTTP) { + if (s->alproto != ALPROTO_HTTP && s->alproto != ALPROTO_SMTP) { SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting keywords."); goto error; } - AppLayerHtpNeedFileInspection(); - - /** \todo remove this once we support more than http */ - s->alproto = ALPROTO_HTTP; + if (s->alproto == ALPROTO_HTTP) { + AppLayerHtpNeedFileInspection(); + } s->file_flags |= (FILE_SIG_NEED_FILE|FILE_SIG_NEED_SIZE); SCReturnInt(0); diff --git a/src/detect-filestore.c b/src/detect-filestore.c index f9bfdf55c2..e88c95eeb6 100644 --- a/src/detect-filestore.c +++ b/src/detect-filestore.c @@ -414,14 +414,14 @@ static int DetectFilestoreSetup (DetectEngineCtx *de_ctx, Signature *s, char *st SigMatchAppendSMToList(s, sm, DETECT_SM_LIST_FILEMATCH); s->filestore_sm = sm; - if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_HTTP) { + if (s->alproto != ALPROTO_HTTP && s->alproto != ALPROTO_SMTP) { SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting keywords."); goto error; } - AppLayerHtpNeedFileInspection(); - - s->alproto = ALPROTO_HTTP; + if (s->alproto == ALPROTO_HTTP) { + AppLayerHtpNeedFileInspection(); + } s->flags |= SIG_FLAG_FILESTORE; return 0; diff --git a/src/log-file.c b/src/log-file.c index 8cb7a662bb..92b066df54 100644 --- a/src/log-file.c +++ b/src/log-file.c @@ -55,6 +55,8 @@ #include "util-logopenfile.h" #include "app-layer-htp.h" +#include "app-layer-smtp.h" +#include "mime-decode.h" #include "util-memcmp.h" #include "stream-tcp-reassemble.h" @@ -144,6 +146,31 @@ static void LogFileMetaGetUserAgent(FILE *fp, const Packet *p, const File *ff) fprintf(fp, ""); } +static void LogFileMetaGetSmtp(FILE *fp, const Packet *p, const File *ff) { + + SMTPState *state = (SMTPState *) p->flow->alstate; + if (state != NULL && state->msg_tail != NULL) { + + /* Message Id */ + if (state->msg_tail->msg_id != NULL) { + + fprintf(fp, "\"message-id\": \""); + PrintRawJsonFp(fp, (uint8_t *) state->msg_tail->msg_id, + (int) state->msg_tail->msg_id_len); + fprintf(fp, "\", "); + } + + /* Sender */ + MimeDecField *field = MimeDecFindField(state->msg_tail, "From"); + if (field != NULL) { + fprintf(fp, "\"sender\": \""); + PrintRawJsonFp(fp, (uint8_t *) field->value, + (int) field->value_len); + fprintf(fp, "\", "); + } + } +} + /** * \internal * \brief Write meta data on a single line json record @@ -231,6 +258,9 @@ static void LogFileWriteJsonRecord(LogFileLogThread *aft, const Packet *p, const fprintf(fp, "\"http_user_agent\": \""); LogFileMetaGetUserAgent(fp, p, ff); fprintf(fp, "\", "); + } else if (p->flow->alproto == ALPROTO_SMTP) { + /* Only applicable to SMTP */ + LogFileMetaGetSmtp(fp, p, ff); } fprintf(fp, "\"filename\": \""); diff --git a/src/log-filestore.c b/src/log-filestore.c index 989b51dee5..771cc8105d 100644 --- a/src/log-filestore.c +++ b/src/log-filestore.c @@ -53,6 +53,8 @@ #include "util-logopenfile.h" #include "app-layer-htp.h" +#include "app-layer-smtp.h" +#include "mime-decode.h" #include "util-memcmp.h" #include "stream-tcp-reassemble.h" @@ -139,8 +141,29 @@ static void LogFilestoreMetaGetUserAgent(FILE *fp, const Packet *p, const File * fprintf(fp, ""); } -static void LogFilestoreLogCreateMetaFile(const Packet *p, const File *ff, const char *filename, int ipver) -{ +static void LogFilestoreMetaGetSmtp(FILE *fp, Packet *p, File *ff) { + + SMTPState *state = (SMTPState *) p->flow->alstate; + if (state != NULL && state->msg_tail != NULL) { + + /* Message Id */ + if (state->msg_tail->msg_id != NULL) { + fprintf(fp, "MESSAGE-ID: "); + PrintRawUriFp(fp, (uint8_t *) state->msg_tail->msg_id, state->msg_tail->msg_id_len); + fprintf(fp, "\n"); + } + + /* Sender */ + MimeDecField *field = MimeDecFindField(state->msg_tail, "From"); + if (field != NULL) { + fprintf(fp, "SENDER: "); + PrintRawUriFp(fp, (uint8_t *) field->value, field->value_len); + fprintf(fp, "\n"); + } + } +} + +static void LogFilestoreLogCreateMetaFile(const Packet *p, const File *ff, char *filename, int ipver) { char metafilename[PATH_MAX] = ""; snprintf(metafilename, sizeof(metafilename), "%s.meta", filename); FILE *fp = fopen(metafilename, "w+"); @@ -180,18 +203,26 @@ static void LogFilestoreLogCreateMetaFile(const Packet *p, const File *ff, const fprintf(fp, "SRC PORT: %" PRIu16 "\n", sp); fprintf(fp, "DST PORT: %" PRIu16 "\n", dp); } - fprintf(fp, "HTTP URI: "); - LogFilestoreMetaGetUri(fp, p, ff); - fprintf(fp, "\n"); - fprintf(fp, "HTTP HOST: "); - LogFilestoreMetaGetHost(fp, p, ff); - fprintf(fp, "\n"); - fprintf(fp, "HTTP REFERER: "); - LogFilestoreMetaGetReferer(fp, p, ff); - fprintf(fp, "\n"); - fprintf(fp, "HTTP USER AGENT: "); - LogFilestoreMetaGetUserAgent(fp, p, ff); - fprintf(fp, "\n"); + + /* Only applicable to HTTP traffic */ + if (ff->txid != 0) { + fprintf(fp, "HTTP URI: "); + LogFilestoreMetaGetUri(fp, p, ff); + fprintf(fp, "\n"); + fprintf(fp, "HTTP HOST: "); + LogFilestoreMetaGetHost(fp, p, ff); + fprintf(fp, "\n"); + fprintf(fp, "HTTP REFERER: "); + LogFilestoreMetaGetReferer(fp, p, ff); + fprintf(fp, "\n"); + fprintf(fp, "HTTP USER AGENT: "); + LogFilestoreMetaGetUserAgent(fp, p, ff); + fprintf(fp, "\n"); + } else if (p->flow->alproto == ALPROTO_SMTP) { + /* Only applicable to SMTP */ + LogFilestoreMetaGetSmtp(fp, p, ff); + } + fprintf(fp, "FILENAME: "); PrintRawUriFp(fp, ff->name, ff->name_len); fprintf(fp, "\n"); diff --git a/src/mime-decode.c b/src/mime-decode.c new file mode 100644 index 0000000000..fdca4f07bc --- /dev/null +++ b/src/mime-decode.c @@ -0,0 +1,2841 @@ +/* Copyright (C) 2012 BAE Systems + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author David Abarbanel + * + */ + +#include +#include +#include "mime-decode.h" + +#include "util-spm-bs.h" +#include "util-unittest.h" + +/* Character constants */ +#ifndef CR +#define CR 13 +#define LF 10 +#endif + +#define CRLF "\r\n" +#define COLON 58 +#define DASH 45 +#define PRINTABLE_START 33 +#define PRINTABLE_END 126 +#define UC_START 65 +#define UC_END 90 +#define LC_START 97 +#define LC_END 122 +#define UC_LC_DIFF 32 +#define EOL_LEN 2 + +/* Base-64 constants */ +#define BASE64_STR "Base64" + +/* Mime Constants */ +#define MAX_LINE_LEN 998 /* Def in RFC 2045, excluding CRLF sequence */ +#define MAX_ENC_LINE_LEN 76 /* Def in RFC 2045, excluding CRLF sequence */ +#define MAX_HEADER_NAME 75 /* 75 + ":" = 76 */ +#define MAX_HEADER_VALUE 2000 /* Default - arbitrary limit */ +#define BOUNDARY_BUF 256 +#define CTNT_TYPE_STR "Content-Type" +#define CTNT_DISP_STR "Content-Disposition" +#define CTNT_TRAN_STR "Content-Transfer-Encoding" +#define MSG_ID_STR "Message-ID" +#define BND_START_STR "boundary=\"" +#define TOK_END_STR "\"" +#define MSG_STR "message/" +#define MULTIPART_STR "multipart/" +#define QP_STR "quoted-printable" +#define TXT_STR "text/plain" +#define HTML_STR "text/html" +#define URL_STR "http://" + +/* Memory Usage Constants */ +#define STACK_FREE_NODES 10 + +/* Other Constants */ +#define MAX_IP4_CHARS 15 +#define MAX_IP6_CHARS 39 + +/* Globally hold configuration data */ +static MimeDecConfig mime_dec_config = { 1, 1, 1, MAX_HEADER_VALUE }; + +/* Mime Parser String translation */ +static const char *StateFlags[] = { "NONE", + "HEADER_READY", + "HEADER_STARTED", + "HEADER_DONE", + "BODY_STARTED", + "BODY_DONE", + "BODY_END_BOUND", + "PARSE_DONE", + "PARSE_ERROR", + NULL }; + +/* URL executable file extensions */ +static const char *UrlExeExts[] = { ".exe", + ".vbs", + ".bin", + ".cmd", + ".bat", + ".jar", + ".js", + NULL }; + +/** + * \brief Function used to print character strings that are not null-terminated + * + * \param log_level The logging level in which to print + * \param label A label for the string to print + * \param src The source string + * \param len The length of the string + * + * \return none + */ +void PrintChars(int log_level, char *label, char *src, uint32_t len) { + + if (log_level <= sc_log_global_log_level) { + + static uint32_t max_len = 100; + char tempbuf[max_len]; + + if (len == 0) { + SCLog(log_level, "PrintChars() - length is 0\n"); + } else { + uint32_t llen = strlen(label); + + /* Add label and source */ + int num = snprintf(tempbuf, llen +4 < max_len ? llen + 4 : max_len, "[%s] ", label); + strncpy(tempbuf + num, src, len + num < max_len ? len + num : max_len - num - 1); + + SCLog(log_level, "%s", tempbuf); + } + } +} + +/** + * \brief Set global config policy + * + * \param config Config policy to set + * \return none + */ +void MimeDecSetConfig(MimeDecConfig *config) { + + if (config != NULL) { + mime_dec_config = *config; + + /* Set to default */ + if (mime_dec_config.header_value_depth == 0) { + mime_dec_config.header_value_depth = MAX_HEADER_VALUE; + } + } else { + SCLogWarning(SC_ERR_MISSING_CONFIG_PARAM, "Invalid null configuration parameters"); + } +} + +/** + * \brief Get global config policy + * + * \return config data structure + */ +MimeDecConfig * MimeDecGetConfig(void) { + return &mime_dec_config; +} + +/** + * \brief Recursively frees a mime entity tree + * + * \param entity The root entity + * + * \return none + * + */ +void MimeDecFreeEntity (MimeDecEntity *entity) { + + if (entity != NULL) { + + MimeDecFreeField(entity->field_list); + MimeDecFreeUrl(entity->url_list); + SCFree(entity->filename); + + /* Use recursion */ + MimeDecFreeEntity(entity->child); + MimeDecFreeEntity(entity->next); + + SCFree(entity); + } +} + +/** + * \brief Iteratively frees a header field entry list + * + * \param field The header field + * + * \return none + * + */ +void MimeDecFreeField(MimeDecField *field) { + + MimeDecField *temp, *curr; + + if (field != NULL) { + + curr = field; + while (curr != NULL) { + temp = curr; + curr = curr->next; + + /* Free contents of node */ + SCFree(temp->name); + SCFree(temp->value); + + /* Now free node data */ + SCFree(temp); + } + } +} + +/** + * \brief Iteratively frees a URL entry list + * + * \param url The url entry + * + * \return none + * + */ +void MimeDecFreeUrl(MimeDecUrl *url) { + + MimeDecUrl *temp, *curr; + + if (url != NULL) { + + curr = url; + while (curr != NULL) { + temp = curr; + curr = curr->next; + + /* Now free node data */ + SCFree(temp->url); + SCFree(temp); + } + } +} + +/** + * \brief Creates and adds a header field entry to an entity + * + * The entity is optional. If NULL is specified, than a new stand-alone field + * is created. + * + * \param entity The parent entity + * + * \return The field object, or NULL if the operation fails + * + */ +MimeDecField * MimeDecAddField(MimeDecEntity *entity) { + + MimeDecField *node = SCMalloc(sizeof(MimeDecField)); + if (unlikely(node == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + memset(node, 0x00, sizeof(MimeDecField)); + + if (entity != NULL) { + + /* If list is empty, then set as head of list */ + if (entity->field_list == NULL) { + entity->field_list = node; + } else { + /* Otherwise add to beginning of list since these are out-of-order in + * the message */ + node->next = entity->field_list; + entity->field_list = node; + } + } + + return node; +} + +/** + * \brief Searches for a header field with the specified name (case insensitive) + * + * \param entity The entity to search + * \param name The header name (case insensitive) + * + * \return The field object, or NULL if not found + * + */ +MimeDecField * MimeDecFindField(const MimeDecEntity *entity, const char *name) { + MimeDecField *curr = entity->field_list; + + while (curr != NULL) { + + if (strncasecmp(curr->name, name, curr->name_len) == 0) { + break; + } + curr = curr->next; + } + + return curr; +} + +/** + * \brief Creates and adds a URL entry to the specified entity + * + * The entity is optional and if NULL is specified, then a new list will be created. + * + * \param entity The entity + * + * \return URL entry or NULL if the operation fails + * + */ +MimeDecUrl * MimeDecAddUrl(MimeDecEntity *entity) { + + MimeDecUrl *node = SCMalloc(sizeof(MimeDecUrl)); + if (unlikely(node == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + memset(node, 0x00, sizeof(MimeDecUrl)); + + if (entity != NULL) { + + /* If list is empty, then set as head of list */ + if (entity->url_list == NULL) { + entity->url_list = node; + } else { + /* Otherwise add to beginning of list since these are out-of-order in + * the message */ + node->next = entity->url_list; + entity->url_list = node; + } + } + + return node; +} + +/** + * \brief Creates and adds a child entity to the specified parent entity + * + * \param parent The parent entity + * + * \return The child entity, or NULL if the operation fails + * + */ +MimeDecEntity * MimeDecAddEntity(MimeDecEntity *parent) { + + MimeDecEntity *curr, *node = SCMalloc(sizeof(MimeDecEntity)); + if (unlikely(node == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + memset(node, 0x00, sizeof(MimeDecEntity)); + + /* If parent is NULL then just return the new pointer */ + if (parent != NULL) { + if (parent->child == NULL) { + parent->child = node; + } else { + curr = parent->child; + while (curr->next != NULL) { + curr = curr->next; + } + curr->next = node; + } + } + + return node; +} + +/** + * \brief Creates a mime header field and fills in its values and adds it to the + * specified entity + * + * \param entity Entity in which to add the field + * \param name String containing the name + * \param nlen Length of the name + * \param value String containing the value + * \param vlen Length of the value + * \param copy_name_value Flag indicating whether to make copies of the name and + * value or just point to them + * + * \return The field or NULL if the operation fails + */ +MimeDecField * MimeDecFillField(MimeDecEntity *entity, const char *name, + uint32_t nlen, const char *value, uint32_t vlen, int copy_name_value) { + + MimeDecField *field = MimeDecAddField(entity); + if (unlikely(field == NULL)) { + return NULL; + } + + if (nlen > 0) { + if (copy_name_value) { + field->name = SCMalloc(nlen); + if (unlikely(field->name == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + SCFree(field); + return NULL; + } + strncpy(field->name, name, nlen); + } else { + field->name = (char *) name; + } + field->name_len = nlen; + } + + if (vlen > 0) { + if (copy_name_value) { + field->value = SCMalloc(vlen); + if (unlikely(field->value == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + SCFree(field); + return NULL; + } + strncpy(field->value, value, vlen); + } else { + field->value = (char *) value; + } + field->value_len = vlen; + } + + return field; +} + +/** + * \brief Pushes a node onto a stack and returns the new node. + * + * \param stack The top of the stack + * + * \return pointer to a new node, otherwise NULL if it fails + */ +static MimeDecStackNode * PushStack(MimeDecStack *stack) { + + /* Attempt to pull from free nodes list */ + MimeDecStackNode *node = stack->free_nodes; + if (node == NULL) { + node = SCMalloc(sizeof(MimeDecStackNode)); + if (unlikely(node == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + } else { + /* Move free nodes pointer over */ + stack->free_nodes = stack->free_nodes->next; + stack->free_nodes_cnt--; + } + memset(node, 0x00, sizeof(MimeDecStackNode)); + + /* Push to top of stack */ + node->next = stack->top; + stack->top = node; + + /* Return a pointer to the top of the stack */ + return node; +} + +/** + * \brief Pops the top node from the stack and returns the next node. + * + * \param stack The top of the stack + * + * \return pointer to the next node, otherwise NULL if no nodes remain + */ +static MimeDecStackNode * PopStack(MimeDecStack *stack) { + + /* Move stack pointer to next item */ + MimeDecStackNode *curr = stack->top; + if (curr != NULL) { + curr = curr->next; + } + + /* Always free alloc'd memory */ + SCFree(stack->top->bdef); + + /* Now move head to free nodes list */ + if (stack->free_nodes_cnt < STACK_FREE_NODES) { + stack->top->next = stack->free_nodes; + stack->free_nodes = stack->top; + stack->free_nodes_cnt++; + } else { + SCFree(stack->top); + } + stack->top = curr; + + /* Return a pointer to the top of the stack */ + return curr; +} + +/** + * \brief Frees the stack along with the free-nodes list + * + * \param stack The stack pointer + * + * \return none + */ +static void FreeMimeDecStack(MimeDecStack *stack) { + + MimeDecStackNode *temp, *curr; + + if (stack != NULL) { + + /* Top of stack */ + curr = stack->top; + while (curr != NULL) { + temp = curr; + curr = curr->next; + + /* Now free node */ + SCFree(temp->bdef); + SCFree(temp); + } + + /* Free nodes */ + curr = stack->free_nodes; + while (curr != NULL) { + temp = curr; + curr = curr->next; + + /* Now free node */ + SCFree(temp); + } + + SCFree(stack); + } +} + +/** + * \brief Adds a data value to the data values linked list + * + * \param dv The head of the linked list (NULL if new list) + * + * \return pointer to a new node, otherwise NULL if it fails + */ +static DataValue * AddDataValue(DataValue *dv) { + + DataValue *curr, *node = SCMalloc(sizeof(DataValue)); + if (unlikely(node == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + memset(node, 0x00, sizeof(DataValue)); + + if (dv != NULL) { + curr = dv; + while (curr->next != NULL) { + curr = curr->next; + } + + curr->next = node; + } + + return node; +} + +/** + * \brief Frees a linked list of data values starting at the head + * + * \param dv The head of the linked list + * + * \return none + */ +static void FreeDataValue(DataValue *dv) { + DataValue *temp, *curr; + + if (dv != NULL) { + curr = dv; + while (curr != NULL) { + temp = curr; + curr = curr->next; + + /* Now free node */ + SCFree(temp->value); + SCFree(temp); + } + } +} + +/** + * \brief Converts a list of data values into a single value (returns dynamically + * allocated memory) + * + * \param dv The head of the linked list (NULL if new list) + * \param len The output length of the single value + * + * \return pointer to a single value, otherwise NULL if it fails or is zero-length + */ +static char * GetFullValue(DataValue *dv, uint32_t *len) { + + DataValue *curr; + uint32_t offset = 0; + char *val = NULL; + + /* First calculate total length */ + *len = 0; + curr = dv; + while (curr != NULL) { + *len += curr->value_len; + + /* Add CRLF except on last one */ + if (curr->next != NULL) { + *len += 2; + } + curr = curr->next; + } + + /* Must have at least one character in the value */ + if (*len > 0) { + val = SCMalloc(*len); + if (unlikely(val == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + *len = 0; + return NULL; + } + + curr = dv; + while (curr != NULL) { + strncpy(val + offset, (char *) curr->value, curr->value_len); + offset += curr->value_len; + + /* Add CRLF except on last one */ + if (curr->next != NULL) { + strncpy(val + offset, CRLF, 2); + offset += 2; + } + curr = curr->next; + } + } + + return val; +} + +/** + * \brief Find a string while searching up to N characters within a source + * string + * + * \param src The source string (not null-terminated) + * \param len The length of the source string + * \param find The string to find (null-terminated) + * + * \return Pointer to the position it was found, otherwise NULL if not found + */ +static inline char * FindString(const char *src, uint32_t len, const char *find) { + + /* Use utility search function */ + return (char *) BasicSearchNocase((uint8_t *) src, len, (uint8_t *) find, strlen(find)); +} + +/** + * \brief Get a line (CRLF or just CR or LF) from a buffer (similar to GetToken) + * + * \param buf The input buffer (not null-terminated) + * \param blen The length of the input buffer + * \param remainPtr Pointer to remaining after tokenizing iteration + * \param tokLen Output token length (if non-null line) + * + * \return Pointer to line + */ +static char * GetLine(char *buf, uint32_t blen, char **remainPtr, + uint32_t *tokLen) { + + uint32_t i; + char *tok; + + /* So that it can be used just like strtok_r */ + if (buf == NULL) { + buf = *remainPtr; + } else { + *remainPtr = buf; + } + tok = buf; + + /* length must be specified */ + for (i = 0; i < blen && buf[i] != 0; i++) { + + /* Found delimiter */ + if (buf[i] == CR || buf[i] == LF) { + + /* Add another if we find either CRLF or LFCR */ + *remainPtr += (i + 1); + if ((i + 1 < blen) && buf[i] != buf[i + 1] && + (buf[i + 1] == CR || buf[i + 1] == LF)) { + (*remainPtr)++; + } + break; + } + } + + /* If no delimiter found, then point to end of buffer */ + if (buf == *remainPtr) { + (*remainPtr) += i; + } + + /* Calculate token length */ + *tokLen = (buf + i) - tok; + + return tok; +} + +/** + * \brief Get token from buffer and return pointer to it + * + * \param buf The input buffer (not null-terminated) + * \param blen The length of the input buffer + * \param delims Character delimiters (null-terminated) + * \param remainPtr Pointer to remaining after tokenizing iteration + * \param tokLen Output token length (if non-null line) + * + * \return Pointer to token, or NULL if not found + */ +static char * GetToken(char *buf, uint32_t blen, const char *delims, + char **remainPtr, uint32_t *tokenLen) { + + uint32_t i, j, delimFound = 0; + char *tok = NULL; + + /* So that it can be used just like strtok_r */ + if (buf == NULL) { + buf = *remainPtr; + } else { + *remainPtr = buf; + } + + /* Must specify length */ + for (i = 0; i < blen && buf[i] != 0; i++) { + + /* Look for delimiters */ + for (j = 0; delims[j] != 0; j++) { + if (buf[i] == delims[j]) { + /* Data must be found before delimiter matters */ + if (tok != NULL) { + (*remainPtr) += (i + 1); + } + delimFound = 1; + break; + } + } + + /* If at least one non-delimiter found, then a token is found */ + if (tok == NULL && !delimFound) { + tok = buf + i; + } else { + /* Reset delimiter */ + delimFound = 0; + } + + /* If delimiter found, then break out of loop */ + if (buf != *remainPtr) { + break; + } + } + + /* Make sure remaining points to end of buffer if delimiters not found */ + if (tok != NULL) { + if (buf == *remainPtr) { + (*remainPtr) += i; + } + + /* Calculate token length */ + *tokenLen = (buf + i) - tok; + } + + return tok; +} + +/** + * \brief Stores the final MIME header value into the current entity on the + * stack. + * + * \param state The parser state + * + * \return MIME_DEC_OK if stored, otherwise a negative number indicating error + */ +static int StoreMimeHeader(MimeDecParseState *state) { + + int ret = MIME_DEC_OK, stored = 0; + char *val; + uint32_t vlen; + + /* Lets save the most recent header */ + if (state->hname != NULL || state->hvalue != NULL) { + SCLogDebug("Storing last header"); + val = GetFullValue(state->hvalue, &vlen); + if (val != NULL) { + + if (state->hname == NULL) { + SCLogDebug("Error: Invalid parser state - header value without" + " name"); + ret = MIME_DEC_ERR_PARSE; + } else if (state->stack->top != NULL) { + /* Store each header name and value */ + if (MimeDecFillField(state->stack->top->data, state->hname, + state->hlen, val, vlen, 0) == NULL) { + SCLogError(SC_ERR_MEM_ALLOC, "MimeDecFillField() function failed"); + ret = MIME_DEC_ERR_MEM; + } else { + stored = 1; + } + } else { + SCLogDebug("Error: Stack pointer missing"); + ret = MIME_DEC_ERR_DATA; + } + } else if (state->hvalue != NULL) { + /* Memory allocation must have failed since val is NULL */ + SCLogError(SC_ERR_MEM_ALLOC, "GetFullValue() function failed"); + ret = MIME_DEC_ERR_MEM; + } + + /* Do cleanup here */ + if (!stored) { + SCFree(state->hname); + } + state->hname = NULL; + FreeDataValue(state->hvalue); + state->hvalue = NULL; + state->hvlen = 0; + } + + return ret; +} + +/** + * \brief Function determines whether a url string points to an executable + * based on file extension only. + * + * \param url The url string + * \param len The url string length + * + * \retval 1 The url points to an EXE + * \retval 0 The url does NOT point to an EXE + */ +static int IsExeUrl(char *url, uint32_t len) { + + int isExeUrl = 0; + uint32_t i, extLen; + char *ext; + + /* Now check for executable extensions and if not found, cut off at first '/' */ + for (i = 0; UrlExeExts[i] != NULL; i++) { + extLen = strlen(UrlExeExts[i]); + ext = FindString(url, len, UrlExeExts[i]); + if (ext != NULL && (ext + extLen - url == len || ext[extLen] == '?')) { + isExeUrl = 1; + break; + } + } + + return isExeUrl; +} + +/** + * \brief Function determines whether a host string is a numeric IP v4 address + * + * \param urlhost The host string + * \param len The host string length + * + * \retval 1 The host is a numeric IP + * \retval 0 The host is NOT a numeric IP + */ +static int IsIpv4Host(char *urlhost, uint32_t len) { + + struct sockaddr_in sa; + + char tempIp[MAX_IP4_CHARS + 1]; + + /* Cut off at '/' */ + uint32_t i = 0; + for (i = 0; i < len && urlhost[i] != 0; i++) { + + if (urlhost[i] == '/') { + break; + } + } + + /* Too many chars */ + if (i > MAX_IP4_CHARS) { + return 0; + } + + /* Create null-terminated string */ + strncpy(tempIp, urlhost, i); + tempIp[i] = 0; + + return inet_pton(AF_INET, tempIp, &(sa.sin_addr)); +} + +/** + * \brief Function determines whether a host string is a numeric IP v6 address + * + * \param urlhost The host string + * \param len The host string length + * + * \retval 1 The host is a numeric IP + * \retval 0 The host is NOT a numeric IP + */ +static int IsIpv6Host(char *urlhost, uint32_t len) { + + struct sockaddr_in sa; + + char tempIp[MAX_IP6_CHARS + 1]; + + /* Cut off at '/' */ + uint32_t i = 0; + for (i = 0; i < len && urlhost[i] != 0; i++) { + + if (urlhost[i] == '/') { + break; + } + } + + /* Too many chars */ + if (i > MAX_IP6_CHARS) { + return 0; + } + + /* Create null-terminated string */ + strncpy(tempIp, urlhost, i); + tempIp[i] = 0; + + return inet_pton(AF_INET6, tempIp, &(sa.sin_addr)); +} + +/** + * \brief Traverses through the list of URLs for an exact match of the specified + * string + * + * \param entity The MIME entity + * \param url The matching URL string + * \param url_len The matching URL string length + * + * \return URL object or NULL if not found + */ +static MimeDecUrl * FindExistingUrl(MimeDecEntity *entity, char *url, + uint32_t url_len) { + + uint32_t len; + MimeDecUrl *curr = entity->url_list; + + while (curr != NULL) { + + /* Get smaller length */ + len = curr->url_len < url_len ? curr->url_len : url_len; + if (strncasecmp(curr->url, url, len) == 0) { + break; + } + + curr = curr->next; + } + + return curr; +} + +/** + * \brief This function searches a text or html line for a URL string + * + * URLS are generally truncated to the 'host.domain' format because + * some email messages contain dozens or even hundreds of URLs with + * the same host, but with only small variations in path. + * + * The exception is that URLs with executable file extensions are stored + * with the full path. + * + * Numeric IPs, malformed numeric IPs, and URLs pointing to executables are + * also flagged as URLs of interest. + * + * \param line the line + * \param len the line length + * \param state The current parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int FindUrlStrings(const char *line, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; + char *fptr, *remptr, *tok = NULL, *tempUrl; + uint32_t tokLen, i, tempUrlLen; + uint8_t urlStrLen = 0, flags = 0; + + remptr = (char *) line; + do { + SCLogDebug("Looking for URL String starting with: %s", URL_STR); + + /* Check for token definition */ + fptr = FindString(remptr, len - (remptr - line), URL_STR); + if (fptr != NULL) { + + urlStrLen = strlen(URL_STR); + fptr += urlStrLen; /* Start at end of start string */ + tok = GetToken(fptr, len - (fptr - line), " \"\'<>]\t", &remptr, + &tokLen); + if (tok == fptr) { + SCLogDebug("Found url string"); + + /* First copy to temp URL string */ + tempUrl = SCMalloc(urlStrLen + tokLen); + if (unlikely(tempUrl == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "Memory allocation failed"); + return MIME_DEC_ERR_MEM; + } + + PrintChars(SC_LOG_DEBUG, "RAW URL", tok, tokLen); + + /* Copy over to temp URL while decoding */ + tempUrlLen = 0; + for (i = 0; i < tokLen && tok[i] != 0; i++) { + + // URL decoding would probably go here + + tempUrl[tempUrlLen] = tok[i]; + tempUrlLen++; + } + + /* Determine if URL points to an EXE */ + if (IsExeUrl(tempUrl, tempUrlLen)) { + flags |= URL_IS_EXE; + + PrintChars(SC_LOG_DEBUG, "EXE URL", tempUrl, tempUrlLen); + } else { + /* Not an EXE URL */ + /* Cut off length at first '/' */ + tok = FindString(tempUrl, tempUrlLen, "/"); + if (tok != NULL) { + tempUrlLen = tok - tempUrl; + } + } + + /* Make sure remaining URL exists */ + if (tempUrlLen > 0) { + + /* Now look for numeric IP */ + if (IsIpv4Host(tempUrl, tempUrlLen)) { + flags |= URL_IS_IP4; + + PrintChars(SC_LOG_DEBUG, "IP URL4", tempUrl, tempUrlLen); + } else if (IsIpv6Host(tempUrl, tempUrlLen)) { + flags |= URL_IS_IP6; + + PrintChars(SC_LOG_DEBUG, "IP URL6", tempUrl, tempUrlLen); + } + + /* Update URL list */ + MimeDecUrl *url = FindExistingUrl(entity, tempUrl, + tempUrlLen); + if (url == NULL) { + + /* Add URL list item */ + url = MimeDecAddUrl(entity); + url->url = tempUrl; + url->url_len = tempUrlLen; + url->url_flags |= flags; + } else { + SCFree(tempUrl); + } + + /* Increment counter */ + url->url_cnt++; + } else { + SCFree(tempUrl); + } + } + } + } while (fptr != NULL); + + return ret; +} + +/** + * \brief This function is a pre-processor for handling decoded data chunks that + * then invokes the caller's callback function for further processing + * + * \param chunk The decoded chunk + * \param len The decoded chunk length (varies) + * \param state The current parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessDecodedDataChunk(const uint8_t *chunk, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; + char *remainPtr, *tok; + uint32_t tokLen; + + MimeDecConfig *mdcfg = MimeDecGetConfig(); + if (mdcfg != NULL && mdcfg->extract_urls) { + /* If plain text or html, then look for URLs */ + if ((entity->ctnt_flags & CTNT_IS_TEXT) || + (entity->ctnt_flags & CTNT_IS_HTML)) { + + /* Remainder from previous line */ + if (state->linerem_len > 0) { + // TODO + } else { + /* No remainder from previous line */ + /* Parse each line one by one */ + remainPtr = (char *) chunk; + do { + tok = GetLine(remainPtr, len - (remainPtr - (char *) chunk), + &remainPtr, &tokLen); + if (tok != remainPtr) { + // DEBUG - ADDED + /* If last token found without CR/LF delimiter, then save + * and reconstruct with next chunk + */ + if (tok + tokLen - (char *) chunk == len) { + PrintChars(SC_LOG_DEBUG, "LAST CHUNK LINE - CUTOFF", + tok, tokLen); + SCLogDebug("\nCHUNK CUTOFF CHARS: %d delim %ld\n", tokLen, len - (tok + tokLen - (char *) chunk)); + } else { + /* Search line for URL */ + ret = FindUrlStrings(tok, tokLen, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: FindUrlStrings() function" + " failed: %d", ret); + break; + } + } + } + } while (tok != remainPtr && remainPtr - (char *) chunk < len); + } + } + } + + /* Now invoke callback */ + if (state->dataChunkProcessor != NULL) { + ret = state->dataChunkProcessor(chunk, len, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: state->dataChunkProcessor() callback function" + " failed"); + } + } + + /* Reset data chunk buffer */ + state->data_chunk_len = 0; + + /* Mark body / file as no longer at beginning */ + state->body_begin = 0; + + return ret; +} + +/** + * \brief Processes a remainder (line % 4 = remainder) from the previous line + * such that all base64 decoding attempts are divisible by 4 + * + * \param buf The current line + * \param len The length of the line + * \param state The current parser state + * \param force Flag indicating whether decoding should always occur + * + * \return Number of bytes pulled from the current buffer + */ +static uint8_t ProcessBase64Remainder(const char *buf, uint32_t len, + MimeDecParseState *state, int force) { + + uint32_t ret; + uint8_t remainder = 0, remdec = 0; + + SCLogDebug("Base64 line remainder found: %u", state->bvr_len); + + /* Fill in block with first few bytes of current line */ + remainder = B64_BLOCK - state->bvr_len; + remainder = remainder < len ? remainder : len; + strncpy(state->bvremain + state->bvr_len, buf, remainder); + state->bvr_len += remainder; + + /* If data chunk buffer will be full, then clear it now */ + if (DATA_CHUNK_SIZE - state->data_chunk_len < ASCII_BLOCK) { + + /* Invoke pre-processor and callback */ + ret = ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, + state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessDecodedDataChunk() function failed"); + } + } + + /* Only decode if divisible by 4 */ + if (state->bvr_len == B64_BLOCK || force) { + remdec = DecodeBase64(state->data_chunk + state->data_chunk_len, + state->bvremain, state->bvr_len); + if (remdec > 0) { + + /* Track decoded length */ + state->stack->top->data->decoded_body_len += remdec; + + /* Update length */ + state->data_chunk_len += remdec; + + /* If data chunk buffer is now full, then clear */ + if (DATA_CHUNK_SIZE - state->data_chunk_len < ASCII_BLOCK) { + + /* Invoke pre-processor and callback */ + ret = ProcessDecodedDataChunk(state->data_chunk, + state->data_chunk_len, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessDecodedDataChunk() function " + "failed"); + } + } + } else { + /* Track failed base64 */ + state->stack->top->data->anomaly_flags |= ANOM_INVALID_BASE64; + state->msg->anomaly_flags |= ANOM_INVALID_BASE64; + SCLogDebug("Error: DecodeBase64() function failed"); + PrintChars(SC_LOG_DEBUG, "Base64 failed string", state->bvremain, state->bvr_len); + } + + /* Reset remaining */ + state->bvr_len = 0; + } + + return remainder; +} + +/** + * \brief Processes a body line by base64-decoding and passing to the data chunk + * processing callback function when the buffer is read + * + * \param buf The current line + * \param len The length of the line + * \param state The current parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessBase64BodyLine(const char *buf, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + uint8_t rem1 = 0, rem2 = 0; + uint32_t numDecoded, remaining, offset, avail, tobuf; + + /* Track long line */ + if (len > MAX_ENC_LINE_LEN) { + state->stack->top->data->anomaly_flags |= ANOM_LONG_ENC_LINE; + state->msg->anomaly_flags |= ANOM_LONG_ENC_LINE; + SCLogDebug("Error: Max encoded input line length exceeded %u > %u", + len, MAX_ENC_LINE_LEN); + } + + /* First process remaining from previous line */ + if (state->bvr_len > 0) { + + SCLogDebug("Base64 line remainder found: %u", state->bvr_len); + + /* Process remainder and return number of bytes pulled from current buffer */ + rem1 = ProcessBase64Remainder(buf, len, state, 0); + } + + /* No error and at least some more data needs to be decoded */ + if ((int) (len - rem1) > 0) { + + /* Determine whether we need to save a remainder if not divisible by 4 */ + rem2 = (len - rem1) % B64_BLOCK; + if (rem2 > 0) { + + SCLogDebug("Base64 saving remainder: %u", rem2); + + strncpy(state->bvremain, buf + (len - rem2), rem2); + state->bvr_len = rem2; + } + + /* Process remaining in loop in case buffer fills up */ + remaining = len - rem1 - rem2; + offset = rem1; + while (remaining > 0) { + + /* Determine amount to add to buffer */ + avail = (DATA_CHUNK_SIZE - state->data_chunk_len) * B64_BLOCK / ASCII_BLOCK; + tobuf = avail > remaining ? remaining : avail; + while (tobuf % 4 != 0) { + tobuf--; + } + + if (tobuf < B64_BLOCK) { + SCLogDebug("Error: Invalid state for decoding base-64 block"); + return MIME_DEC_ERR_PARSE; + } + + SCLogDebug("Decoding: %u", len - rem1 - rem2); + + numDecoded = DecodeBase64(state->data_chunk + state->data_chunk_len, + buf + offset, tobuf); + if (numDecoded > 0) { + + /* Track decoded length */ + state->stack->top->data->decoded_body_len += numDecoded; + + /* Update length */ + state->data_chunk_len += numDecoded; + + 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; + } + + /* If buffer full, then invoke callback */ + if (DATA_CHUNK_SIZE - state->data_chunk_len < ASCII_BLOCK) { + + /* Invoke pre-processor and callback */ + ret = ProcessDecodedDataChunk(state->data_chunk, + state->data_chunk_len, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessDecodedDataChunk() " + "function failed"); + } + } + } else { + /* Track failed base64 */ + state->stack->top->data->anomaly_flags |= ANOM_INVALID_BASE64; + state->msg->anomaly_flags |= ANOM_INVALID_BASE64; + SCLogDebug("Error: DecodeBase64() function failed"); + PrintChars(SC_LOG_DEBUG, "Base64 failed string", (char *) (buf + offset), tobuf); + } + + /* Update counts */ + remaining -= tobuf; + offset += tobuf; + } + } + + return ret; +} + +/** + * \brief Decoded a hex character into its equivalent byte value for + * quoted-printable decoding + * + * \param h The hex char + * + * \return byte value on success, -1 if failed + **/ +static int16_t DecodeQPChar(char h) { + uint16_t res = 0; + + /* 0-9 */ + if (h >= 48 && h <= 57) { + res = h - 48; + } else if (h >= 65 && h <= 70) { + /* A-F */ + res = h - 55; + } else { + /* Invalid */ + res = -1; + } + + return res; + +} + +/** + * \brief Processes a quoted-printable encoded body line by decoding and passing + * to the data chunk processing callback function when the buffer is read + * + * \param buf The current line + * \param len The length of the line + * \param state The current parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessQuotedPrintableBodyLine(const char *buf, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + uint32_t remaining, offset; + MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; + char c, h1, h2, val; + int16_t res; + + /* Track long line */ + if (len > MAX_ENC_LINE_LEN) { + state->stack->top->data->anomaly_flags |= ANOM_LONG_ENC_LINE; + state->msg->anomaly_flags |= ANOM_LONG_ENC_LINE; + SCLogDebug("Error: Max encoded input line length exceeded %u > %u", + len, MAX_ENC_LINE_LEN); + } + + remaining = len; + offset = 0; + while (remaining > 0) { + + c = *(buf + offset); + + /* Copy over normal character */ + if (c != '=') { + state->data_chunk[state->data_chunk_len] = c; + state->data_chunk_len++; + entity->decoded_body_len += 1; + + /* Add CRLF sequence if end of line */ + if (remaining == 1) { + memcpy(state->data_chunk + state->data_chunk_len, CRLF, EOL_LEN); + state->data_chunk_len += EOL_LEN; + entity->decoded_body_len += EOL_LEN; + } + } else if (remaining > 1) { + /* If last character handle as soft line break by ignoring, + otherwise process as escaped '=' character */ + + /* Not enough characters */ + if (remaining < 3) { + entity->anomaly_flags |= ANOM_INVALID_QP; + state->msg->anomaly_flags |= ANOM_INVALID_QP; + SCLogDebug("Error: Quoted-printable decoding failed"); + } else { + h1 = *(buf + offset + 1); + res = DecodeQPChar(h1); + if (res < 0) { + entity->anomaly_flags |= ANOM_INVALID_QP; + state->msg->anomaly_flags |= ANOM_INVALID_QP; + SCLogDebug("Error: Quoted-printable decoding failed"); + } else { + val = (res << 4); /* Shift result left */ + h2 = *(buf + offset + 2); + res = DecodeQPChar(h2); + if (res < 0) { + entity->anomaly_flags |= ANOM_INVALID_QP; + state->msg->anomaly_flags |= ANOM_INVALID_QP; + SCLogDebug("Error: Quoted-printable decoding failed"); + } else { + /* Decoding sequence succeeded */ + val += res; + + state->data_chunk[state->data_chunk_len] = val; + state->data_chunk_len++; + entity->decoded_body_len++; + + /* Add CRLF sequence if end of line */ + if (remaining == 3) { + memcpy(state->data_chunk + state->data_chunk_len, + CRLF, EOL_LEN); + state->data_chunk_len += EOL_LEN; + entity->decoded_body_len += EOL_LEN; + } + + /* Account for extra 2 characters in 3-characted QP + * sequence */ + remaining -= 2; + offset += 2; + } + } + } + } + + /* Change by 1 */ + remaining--; + offset++; + + /* If buffer full, then invoke callback */ + if (DATA_CHUNK_SIZE - state->data_chunk_len < EOL_LEN + 1) { + + /* Invoke pre-processor and callback */ + ret = ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, + state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessDecodedDataChunk() function " + "failed"); + } + } + } + + return ret; +} + +/** + * \brief Processes a body line by base64-decoding (if applicable) and passing to + * the data chunk processing callback function + * + * \param buf The current line + * \param len The length of the line + * \param state The current parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessBodyLine(const char *buf, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + uint32_t remaining, offset, avail, tobuf; + MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; + + SCLogDebug("Processing body line"); + + /* Track length */ + entity->body_len += len + 2; /* With CRLF */ + + /* Process base-64 content if enabled */ + 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)) { + /* Process quoted-printable content if enabled */ + ret = ProcessQuotedPrintableBodyLine(buf, len, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessQuotedPrintableBodyLine() function " + "failed"); + } + } else { + /* Process non-decoded content */ + remaining = len; + offset = 0; + while (remaining > 0) { + + /* Plan to add CRLF to the end of each line */ + avail = DATA_CHUNK_SIZE - state->data_chunk_len; + tobuf = avail > remaining + EOL_LEN ? remaining : avail - EOL_LEN; + + /* Copy over to buffer */ + memcpy(state->data_chunk + state->data_chunk_len, buf + offset, tobuf); + state->data_chunk_len += tobuf; + + /* Now always add a CRLF to the end */ + 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; + } + + /* If buffer full, then invoke callback */ + if (DATA_CHUNK_SIZE - state->data_chunk_len < EOL_LEN + 1) { + + /* Invoke pre-processor and callback */ + ret = ProcessDecodedDataChunk(state->data_chunk, + state->data_chunk_len, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessDecodedDataChunk() function " + "failed"); + } + } + + remaining -= tobuf; + offset += tobuf; + } + } + + return ret; +} + +/** + * \brief Find the start of a header name on the current line + * + * \param buf The input line (not null-terminated) + * \param blen The length of the input line + * \param glen The output length of the header name + * + * \return Pointer to header name, or NULL if not found + */ +static char * FindMimeHeaderStart(const char *buf, uint32_t blen, uint32_t *hlen) { + + uint32_t i, valid = 0; + char *hname = NULL; + + /* Init */ + *hlen = 0; + + /* Look for sequence of printable characters followed by ':', or + CRLF then printable characters followed by ':' */ + for (i = 0; i < blen && buf[i] != 0; i++) { + + /* If ready for printable characters and found one, then increment */ + if (buf[i] != COLON && buf[i] >= PRINTABLE_START && + buf[i] <= PRINTABLE_END) { + valid++; + } else if (valid > 0 && buf[i] == COLON) { + /* If ready for printable characters, found some, and found colon + * delimiter, then a match is found */ + hname = (char *) buf + i - valid; + *hlen = valid; + break; + } else { + /* Otherwise reset and quit */ + break; + } + } + + return hname; +} + +/** + * \brief Find full header name and value on the current line based on the + * current state + * + * \param buf The current line (no CRLF) + * \param blen The length of the current line + * \param state The current state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int FindMimeHeader(const char *buf, uint32_t blen, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + char *hname, *hval = NULL; + DataValue *dv; + uint32_t hlen, vlen; + int finish_header = 0, new_header = 0; + MimeDecConfig *mdcfg = MimeDecGetConfig(); + + /* Find first header */ + hname = FindMimeHeaderStart(buf, blen, &hlen); + if (hname != NULL) { + + /* Warn and track but don't do anything yet */ + 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); + } + + /* Value starts after 'header:' (normalize spaces) */ + hval = hname + hlen + 1; + if (hval - buf >= blen) { + SCLogDebug("No Header value found"); + hval = NULL; + } else { + while (hval[0] == ' ') { + + /* If last character before end of bounds, set to NULL */ + if (hval - buf >= blen - 1) { + SCLogDebug("No Header value found"); + hval = NULL; + break; + } + + hval++; + } + } + + /* If new header found, then previous header is finished */ + if (state->state_flag == HEADER_STARTED) { + finish_header = 1; + } + + /* Now process new header */ + new_header = 1; + + /* Must wait for next line to determine if finished */ + state->state_flag = HEADER_STARTED; + } else if (blen == 0) { + /* Found body */ + /* No more headers */ + state->state_flag = HEADER_DONE; + + finish_header = 1; + + SCLogDebug("All Header processing finished"); + } else if (state->state_flag == HEADER_STARTED) { + /* Found multi-line value (ie. Received header) */ + /* If max header value exceeded, flag it */ + 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) { + 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; + } + strncpy(dv->value, (char *) buf, vlen); + dv->value_len = vlen; + state->hvlen += vlen; + } + } else { + /* Likely a body without headers */ + SCLogDebug("No headers found"); + + state->state_flag = BODY_STARTED; + + /* Flag beginning of 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 we need to finish a header, then do so below and then cleanup */ + if (finish_header) { + + /* Store the header value */ + ret = StoreMimeHeader(state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: StoreMimeHeader() function failed"); + return ret; + } + } + + /* When next header is found, we always create a new one */ + if (new_header) { + + /* Copy name and value to state */ + state->hname = SCMalloc(hlen); + if (unlikely(state->hname == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "Memory allocation failed"); + return MIME_DEC_ERR_MEM; + } + strncpy(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; + } + + if (hval != NULL) { + + /* If max header value exceeded, flag it */ + 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; + } + strncpy(state->hvalue->value, hval, vlen); + state->hvalue->value_len = vlen; + state->hvlen += vlen; + } + } + } + + return ret; +} + +/** + * \brief Finds a mime header token within the specified field + * + * \param field The current field + * \param search_start The start of the search (ie. boundary=\") + * \param search_end The end of the search (ie. \") + * \param tlen The output length of the token (if found) + * + * \return A pointer to the token if found, otherwise NULL if not found + */ +static char * FindMimeHeaderToken(MimeDecField *field, char *search_start, + char *search_end, uint32_t *tlen) { + + char *fptr, *tptr = NULL, *tok = NULL; + + SCLogDebug("Looking for token: %s", search_start); + + /* Check for token definition */ + fptr = FindString(field->value, field->value_len, search_start); + if (fptr != NULL) { + fptr += strlen(search_start); /* Start at end of start string */ + tok = GetToken(fptr, field->value_len - (fptr - field->value), search_end, + &tptr, tlen); + if (tok != NULL) { + SCLogDebug("Found mime token"); + } + } + + return tok; +} + +/** + * \brief Processes the current line for mime headers and also does post-processing + * when all headers found + * + * \param buf The current line + * \param len The length of the line + * \param state The current parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessMimeHeaders(const char *buf, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + MimeDecField *field; + char *bptr = NULL, *rptr = NULL; + uint32_t blen = 0; + MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; + + /* 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; + } + + /* Post-processing after all headers done */ + if (state->state_flag == HEADER_DONE) { + + /* First determine encoding by looking at Content-Transfer-Encoding */ + field = MimeDecFindField(entity, CTNT_TRAN_STR); + if (field != NULL) { + /* Look for base64 */ + if (FindString(field->value, field->value_len, BASE64_STR)) { + SCLogDebug("Base64 encoding found"); + entity->ctnt_flags |= CTNT_IS_BASE64; + } else if (FindString(field->value, field->value_len, QP_STR)) { + /* Look for quoted-printable */ + SCLogDebug("quoted-printable encoding found"); + entity->ctnt_flags |= CTNT_IS_QP; + } + } + + /* Check for file attachment in content disposition */ + field = MimeDecFindField(entity, CTNT_DISP_STR); + if (field != NULL) { + bptr = FindMimeHeaderToken(field, "filename=\"", TOK_END_STR, &blen); + if (bptr != NULL) { + SCLogDebug("File attachment found in disposition"); + entity->ctnt_flags |= CTNT_IS_ATTACHMENT; + + /* Copy over using dynamic memory */ + entity->filename = SCMalloc(blen); + if (unlikely(entity->filename == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return MIME_DEC_ERR_MEM; + } + strncpy(entity->filename, bptr, blen); + entity->filename_len = blen; + } + } + + /* Check for boundary, encapsulated message, and file name in Content-Type */ + field = MimeDecFindField(entity, CTNT_TYPE_STR); + if (field != NULL) { + + /* Check if child entity boundary definition found */ + bptr = FindMimeHeaderToken(field, BND_START_STR, TOK_END_STR, &blen); + if (bptr != NULL) { + state->found_child = 1; + + entity->ctnt_flags |= CTNT_IS_MULTIPART; + + /* Store boundary in parent node */ + 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; + } + strncpy(state->stack->top->bdef, bptr, blen); + state->stack->top->bdef_len = blen; + } + + /* Look for file name (if not already found) */ + if (!(entity->ctnt_flags & CTNT_IS_ATTACHMENT)) { + bptr = FindMimeHeaderToken(field, "name=\"", TOK_END_STR, &blen); + if (bptr != NULL) { + SCLogDebug("File attachment found"); + entity->ctnt_flags |= CTNT_IS_ATTACHMENT; + + /* Copy over using dynamic memory */ + entity->filename = SCMalloc(blen); + if (unlikely(entity->filename == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return MIME_DEC_ERR_MEM; + } + strncpy(entity->filename, bptr, blen); + entity->filename_len = blen; + } + } + + /* Pull out short-hand content type */ + entity->ctnt_type = GetToken(field->value, field->value_len, " \r\n;", + &rptr, &entity->ctnt_type_len); + if (entity->ctnt_type != NULL) { + + /* Check for encapsulated message */ + if (FindString(entity->ctnt_type, entity->ctnt_type_len, MSG_STR)) { + SCLogDebug("Found encapsulated message entity"); + + entity->ctnt_flags |= CTNT_IS_ENV; + + /* Create and push child to stack */ + MimeDecEntity *child = MimeDecAddEntity(entity); + child->ctnt_flags |= (CTNT_IS_ENCAP | CTNT_IS_MSG); + PushStack(state->stack); + state->stack->top->data = child; + + /* Mark as encapsulated child */ + state->stack->top->is_encap = 1; + + /* Ready to parse headers */ + state->state_flag = HEADER_READY; + } else if (FindString(entity->ctnt_type, entity->ctnt_type_len, + MULTIPART_STR)) { + /* Check for multipart */ + SCLogDebug("Found multipart entity"); + entity->ctnt_flags |= CTNT_IS_MULTIPART; + } else if (FindString(entity->ctnt_type, entity->ctnt_type_len, + TXT_STR)) { + /* Check for plain text */ + SCLogDebug("Found plain text entity"); + entity->ctnt_flags |= CTNT_IS_TEXT; + } else if (FindString(entity->ctnt_type, entity->ctnt_type_len, + HTML_STR)) { + /* Check for html */ + SCLogDebug("Found html entity"); + entity->ctnt_flags |= CTNT_IS_HTML; + } + } + } + + /* Store pointer to Message-ID */ + field = MimeDecFindField(entity, MSG_ID_STR); + if (field != NULL) { + entity->msg_id = field->value; + entity->msg_id_len = field->value_len; + } + + /* Flag beginning of body */ + state->body_begin = 1; + state->body_end = 0; + } + + return ret; +} + +/** + * \brief Indicates to the parser that the body of an entity has completed + * processing on the previous line + * + * \param state The current parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessBodyComplete(MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + + SCLogDebug("Process body complete called"); + + /* Mark the file as hitting the end */ + state->body_end = 1; + + if (state->bvr_len > 0) { + SCLogDebug("Found (%u) remaining base64 bytes not processed", + state->bvr_len); + + /* Process the remainder */ + ret = ProcessBase64Remainder(NULL, 0, state, 1); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessBase64BodyLine() function failed"); + } + } + + /* Invoke pre-processor and callback with remaining data */ + ret = ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessDecodedDataChunk() function failed"); + } + + /* Now reset */ + state->body_begin = 0; + state->body_end = 0; + + return ret; +} + +/** + * \brief When a mime boundary is found, look for end boundary and also do stack + * management + * + * \param buf The current line + * \param len The length of the line + * \param bdef_len The length of the current boundary + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessMimeBoundary(const char *buf, uint32_t len, uint32_t bdef_len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + char *rptr; + MimeDecEntity *child; + + SCLogDebug("PROCESSING BOUNDARY - START: %d", + state->state_flag); + + /* 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; + } + } else { + /* If last line was an end boundary, then now we are ready to parse + * headers again */ + state->state_flag = HEADER_READY; + } + + /* Update remaining buffer */ + rptr = (char *) buf + bdef_len + 2; + + /* If entity is encapsulated and current and parent didn't define the boundary, + * then pop out */ + if (state->stack->top->is_encap && state->stack->top->bdef_len == 0) { + + if (state->stack->top->next == NULL) { + SCLogDebug("Error: Missing parent entity from stack"); + return MIME_DEC_ERR_DATA; + } + + if (state->stack->top->next->bdef_len == 0) { + + SCLogDebug("POPPED ENCAPSULATED CHILD FROM STACK: %p=%p", + state->stack->top, state->stack->top->data); + + /* If end of boundary found, pop the child off the stack */ + PopStack(state->stack); + if (state->stack->top == NULL) { + SCLogDebug("Error: Message is malformed"); + return MIME_DEC_ERR_DATA; + } + } + } + + /* Now check for end of nested boundary */ + 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); + + /* If end of boundary found, pop the child off the stack */ + PopStack(state->stack); + if (state->stack->top == NULL) { + SCLogDebug("Error: Message is malformed"); + return MIME_DEC_ERR_DATA; + } + + /* If current is an encapsulated message with a boundary definition, + * then pop him as well */ + 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; + } + } + + state->state_flag = BODY_END_BOUND; + } else if (state->found_child) { + /* Otherwise process new child */ + SCLogDebug("Child entity created"); + + /* Create and push child to stack */ + child = MimeDecAddEntity(state->stack->top->data); + child->ctnt_flags |= CTNT_IS_BODYPART; + PushStack(state->stack); + state->stack->top->data = child; + + /* Reset flag */ + state->found_child = 0; + } 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); + + /* First pop current to get access to parent */ + PopStack(state->stack); + 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); + child->ctnt_flags |= CTNT_IS_BODYPART; + PushStack(state->stack); + state->stack->top->data = child; + } + + /* After boundary look for headers */ + if (state->state_flag != BODY_END_BOUND) { + state->state_flag = HEADER_READY; + } + + SCLogDebug("PROCESSING BOUNDARY - END: %d", state->state_flag); + + return ret; +} + +/** + * \brief Processes the MIME Entity body based on the input line and current + * state of the parser + * + * \param buf The current line + * \param len The length of the line + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessMimeBody(const char *buf, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + char temp[BOUNDARY_BUF]; + char *bstart; + int body_found = 0; + uint32_t tlen; + + /* Ignore empty lines */ + if (len == 0) { + return ret; + } + + /* First look for boundary */ + MimeDecStackNode *node = state->stack->top; + if (node == NULL) { + SCLogDebug("Error: Invalid stack state"); + return MIME_DEC_ERR_PARSE; + } + + /* 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; + } + } + + /* This means no boundary / parent w/boundary was found so we are in the body */ + if (node == NULL) { + body_found = 1; + } else { + + /* Now look for start of boundary */ + if (len > 1 && buf[0] == '-' && buf[1] == '-') { + + tlen = node->bdef_len + 2; + strncpy(temp, "--", 2); + strncpy(temp + 2, node->bdef, node->bdef_len); + temp[tlen] = 0; + + SCLogDebug("Boundary to search: [%s]", temp); + + /* Find either next boundary or end boundary */ + bstart = FindString(buf, len, temp); + 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; + } + } + + /* 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; + } + } + + return ret; +} + +/** + * \brief Processes the MIME Entity based on the input line and current state of + * the parser + * + * \param buf The current line + * \param len The length of the line + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +static int ProcessMimeEntity(const char *buf, uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + + SCLogDebug("START FLAG: %s", StateFlags[state->state_flag]); + + /* 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); + } + + /* 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; + } + } + + SCLogDebug("END FLAG: %s", StateFlags[state->state_flag]); + + return ret; +} + +/** + * \brief Init the parser by allocating memory for the state and top-level entity + * + * \param data A caller-specified pointer to data for access within the data chunk + * processor callback function + * \param dcpfunc The data chunk processor callback function + * + * \return A pointer to the state object, or NULL if the operation fails + */ +MimeDecParseState * MimeDecInitParser(void *data, + int (*dcpfunc)(const uint8_t *chunk, uint32_t len, + MimeDecParseState *state)) { + + MimeDecParseState *state; + MimeDecEntity *mimeMsg; + + state = SCMalloc(sizeof(MimeDecParseState)); + if (unlikely(state == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + memset(state, 0x00, sizeof(MimeDecParseState)); + + state->stack = SCMalloc(sizeof(MimeDecStack)); + if (unlikely(state->stack == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + memset(state->stack, 0x00, sizeof(MimeDecStack)); + + mimeMsg = SCMalloc(sizeof(MimeDecEntity)); + if (unlikely(mimeMsg == NULL)) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + memset(mimeMsg, 0x00, sizeof(MimeDecEntity)); + mimeMsg->ctnt_flags |= CTNT_IS_MSG; + + /* Init state */ + state->msg = mimeMsg; + PushStack(state->stack); + if (state->stack->top == NULL) { + SCLogError(SC_ERR_MEM_ALLOC, "memory allocation failed"); + return NULL; + } + state->stack->top->data = mimeMsg; + state->state_flag = HEADER_READY; + state->data = data; + state->dataChunkProcessor = dcpfunc; + + return state; +} + +/** + * \brief De-Init parser by freeing up any residual memory + * + * \param state The parser state + * + * \return none + */ +void MimeDecDeInitParser(MimeDecParseState *state) { + + uint32_t cnt = 0; + + while (state->stack->top != NULL) { + + SCLogDebug("Remaining on stack: [%p]=>[%p]", + state->stack->top, state->stack->top->data); + + PopStack(state->stack); + cnt++; + } + + if (cnt > 1) { + state->msg->anomaly_flags |= ANOM_MALFORMED_MSG; + SCLogDebug("Warning: Stack is not empty upon completion of " + "processing (%u items remaining)", cnt); + } + + SCFree(state->hname); + FreeDataValue(state->hvalue); + FreeMimeDecStack(state->stack); + SCFree(state); +} + +/** + * \brief Called to indicate that the last message line has been processed and + * the parsing operation is complete + * + * This function should be called directly by the caller. + * + * \param state The parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +int MimeDecParseComplete(MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + + SCLogDebug("Parsing flagged as completed"); + + /* Store the header value */ + ret = StoreMimeHeader(state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: StoreMimeHeader() function failed"); + return ret; + } + + /* Lets complete the body */ + ret = ProcessBodyComplete(state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: ProcessBodyComplete() function failed"); + return ret; + } + + if (state->stack->top == NULL) { + state->msg->anomaly_flags |= ANOM_MALFORMED_MSG; + SCLogDebug("Error: Message is malformed"); + return MIME_DEC_ERR_DATA; + } + + /* If encapsulated, pop off the stack */ + if (state->stack->top->is_encap) { + PopStack(state->stack); + if (state->stack->top == NULL) { + state->msg->anomaly_flags |= ANOM_MALFORMED_MSG; + SCLogDebug("Error: Message is malformed"); + return MIME_DEC_ERR_DATA; + } + } + + /* Look extra stack items remaining */ + if (state->stack->top->next != NULL) { + state->msg->anomaly_flags |= ANOM_MALFORMED_MSG; + SCLogDebug("Warning: Message has unclosed message part boundary"); + } + + state->state_flag = PARSE_DONE; + + return ret; +} + +/** + * \brief Parse a line of a MIME message and update the parser state + * + * \param line A string representing the line (w/out CRLF) + * \param len The length of the line + * \param state The parser state + * + * \return MIME_DEC_OK on success, otherwise < 0 on failure + */ +int MimeDecParseLine(const char *line, const uint32_t len, + MimeDecParseState *state) { + + int ret = MIME_DEC_OK; + + /* For debugging purposes */ + if (len > 0) { + PrintChars(SC_LOG_DEBUG, "\nSMTP LINE", (char *) line, len); + } else { + SCLogDebug("\nSMTP LINE - EMPTY"); + } + + /* Process the entity */ + ret = ProcessMimeEntity(line, len, state); + if (ret != MIME_DEC_OK) { + state->state_flag = PARSE_ERROR; + SCLogDebug("Error: ProcessMimeEntity() function failed: %d", ret); + } + + return ret; +} + +/** + * \brief Parses an entire message when available in its entirety (wraps the + * line-based parsing functions) + * + * \param buf Buffer pointing to the full message + * \param blen Length of the buffer + * \param data Caller data to be available in callback + * \param dcpfunc Callback for processing each decoded body data chunk + * + * \return A pointer to the decoded MIME message, or NULL if the operation fails + */ +MimeDecEntity * MimeDecParseFullMsg(const char *buf, uint32_t blen, void *data, + int (*dcpfunc)(const uint8_t *chunk, uint32_t len, + MimeDecParseState *state)) { + + int ret = MIME_DEC_OK; + char *remainPtr, *tok; + uint32_t tokLen; + + MimeDecParseState *state = MimeDecInitParser(data, dcpfunc); + if (state == NULL) { + SCLogDebug("Error: MimeDecInitParser() function failed to create " + "state"); + return NULL; + } + + MimeDecEntity *msg = state->msg; + + /* Parse each line one by one */ + remainPtr = (char *) buf; + char *line = NULL; + do { + tok = GetLine(remainPtr, blen - (remainPtr - buf), &remainPtr, &tokLen); + if (tok != remainPtr) { + + line = tok; + + /* Parse the line */ + ret = MimeDecParseLine(line, tokLen, state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: MimeDecParseLine() function failed: %d", + ret); + break; + } + } + + } while (tok != remainPtr && remainPtr - buf < blen); + + if (ret == MIME_DEC_OK) { + SCLogDebug("Message parser was successful"); + + /* Now complete message */ + ret = MimeDecParseComplete(state); + if (ret != MIME_DEC_OK) { + SCLogDebug("Error: MimeDecParseComplete() function failed"); + } + } + + /* De-allocate memory for parser */ + MimeDecDeInitParser(state); + + if (ret != MIME_DEC_OK) { + MimeDecFreeEntity(msg); + msg = NULL; + } + + return msg; +} + +#ifdef UNITTESTS + +/* Helper body chunk callback function */ +static int TestDataChunkCallback(const uint8_t *chunk, uint32_t len, + MimeDecParseState *state) { + + uint32_t *line_count = (uint32_t *) state->data; + + if (state->body_begin) { + SCLogDebug("Body begin (len=%u)", len); + } + + /* Add up the line counts */ + if (len > 0) { + + char *remainPtr; + char *tok; + uint32_t tokLen; + + PrintChars(SC_LOG_DEBUG, (char *) "CHUNK", (char *) chunk, len); + + /* Parse each line one by one */ + remainPtr = (char *) chunk; + do { + tok = GetLine(remainPtr, len - (remainPtr - (char *) chunk), + &remainPtr, &tokLen); + if (tok != NULL && tok != remainPtr) { + (*line_count)++; + } + + } while (tok != NULL && tok != remainPtr && + remainPtr - (char *) chunk < len); + + SCLogDebug("line count (len=%u): %u", len, *line_count); + } + + if (state->body_end) { + SCLogDebug("Body end (len=%u)", len); + } + + return MIME_DEC_OK; +} + +/* Test simple case of line counts */ +static int MimeDecParseLineTest01(void) { + int ret = MIME_DEC_OK; + + uint32_t expected_count = 3; + uint32_t line_count = 0; + + /* Init parser */ + MimeDecParseState *state = MimeDecInitParser(&line_count, + TestDataChunkCallback); + + char str[256]; + strcpy(str, "From: Sender1"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "To: Recipient1"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "Content-Type: text/plain"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, ""); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "A simple message line 1"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "A simple message line 2"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "A simple message line 3"); + ret = MimeDecParseLine(str, strlen(str), state); + + /* Completed */ + ret = MimeDecParseComplete(state); + if (ret != MIME_DEC_OK) { + return ret; + } + + MimeDecEntity *msg = state->msg; + if (msg->next != NULL || msg->child != NULL) { + SCLogInfo("Error: Invalid sibling or child message"); + return -1; + } + + MimeDecFreeEntity(msg); + + /* De Init parser */ + MimeDecDeInitParser(state); + + SCLogInfo("LINE COUNT FINISHED: %d", line_count); + + if (expected_count != line_count) { + SCLogInfo("Error: Line count is invalid: expected - %d actual - %d", + expected_count, line_count); + return -1; + } + + return ret; +} + +/* Test simple case of EXE URL extraction */ +static int MimeDecParseLineTest02(void) { + int ret = MIME_DEC_OK; + + uint32_t expected_count = 2; + uint32_t line_count = 0; + + MimeDecGetConfig()->decode_base64 = 1; + MimeDecGetConfig()->decode_quoted_printable = 1; + MimeDecGetConfig()->extract_urls = 1; + + /* Init parser */ + MimeDecParseState *state = MimeDecInitParser(&line_count, + TestDataChunkCallback); + + char str[256]; + strcpy(str, "From: Sender1"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "To: Recipient1"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "Content-Type: text/plain"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, ""); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "A simple message line 1"); + ret = MimeDecParseLine(str, strlen(str), state); + + strcpy(str, "A simple message line 2 click on http://www.test.com/malware.exe?" + "hahah hopefully you click this link"); + ret = MimeDecParseLine(str, strlen(str), state); + + /* Completed */ + ret = MimeDecParseComplete(state); + if (ret != MIME_DEC_OK) { + return ret; + } + + MimeDecEntity *msg = state->msg; + if (msg->url_list == NULL || (msg->url_list != NULL && + !(msg->url_list->url_flags & URL_IS_EXE))) { + SCLogInfo("Warning: Expected EXE URL not found"); + return -1; + } + + MimeDecFreeEntity(msg); + + /* De Init parser */ + MimeDecDeInitParser(state); + + SCLogInfo("LINE COUNT FINISHED: %d", line_count); + + if (expected_count != line_count) { + SCLogInfo("Warning: Line count is invalid: expected - %d actual - %d", + expected_count, line_count); + return -1; + } + + return ret; +} + +/* Test full message with linebreaks */ +static int MimeDecParseFullMsgTest01(void) { + int ret = MIME_DEC_OK; + + uint32_t expected_count = 3; + uint32_t line_count = 0; + + char msg[] = "From: Sender1\r\n" + "To: Recipient1\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "Line 1\r\n" + "Line 2\r\n" + "Line 3\r\n"; + + MimeDecEntity *entity = MimeDecParseFullMsg(msg, strlen(msg), &line_count, + TestDataChunkCallback); + if (entity == NULL) { + SCLogInfo("Warning: Message failed to parse"); + return -1; + } + + if (expected_count != line_count) { + SCLogInfo("Warning: Line count is invalid: expected - %d actual - %d", + expected_count, line_count); + return -1; + } + + return ret; +} + +static int MimeBase64DecodeTest01(void){ + + int ret = -1; + + char *msg = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@" + "#$%^&*()-=_+,./;'[]<>?:"; + char *base64msg = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QU" + "VJTVFVWV1hZWjEyMzQ1Njc4OTBAIyQlXiYqKCktPV8rLC4vOydbXTw+Pzo="; + + char *dst; + dst = (char *)malloc(strlen(msg)-1); + + ret = DecodeBase64((uint8_t *)dst, base64msg, strlen(base64msg)); + + if(strcmp(dst, msg) == 0){ + ret = 0; + } + + return ret; +} + +static int MimeIsExeURLTest01(void){ + int ret = -1; + char *url1 = "http://www.google.com/"; + char *url2 = "http://www.google.com/test.exe"; + + if(IsExeUrl(url1, strlen(url1)) != 0){ + SCLogDebug("Debug: URL1 error"); + goto end; + } + if(IsExeUrl(url2, strlen(url2)) != 1){ + SCLogDebug("Debug: URL2 error"); + goto end; + } + ret = 0; + + end: + + return ret; +} + +static int MimeIsIpv4HostTest01(void) { + + if(IsIpv4Host("192.168.1.1", 11) != 1) { + return 1; + } + + if(IsIpv4Host("999.oogle.com", 14) != 0) { + return 1; + } + + if(IsIpv4Host("0:0:0:0:0:0:0:0", 15) != 0) { + return 1; + } + + if (IsIpv4Host("192.168.255.255", 15) != 1) { + return 1; + } + + if (IsIpv4Host("192.168.255.255/testurl.html", 28) != 1) { + return 1; + } + + if (IsIpv4Host("www.google.com", 14) != 0) { + return 1; + } + + return 0; +} + +static int MimeIsIpv6HostTest01(void) { + + if(IsIpv6Host("0:0:0:0:0:0:0:0", 19) != 1) { + return 1; + } + + if(IsIpv6Host("0000:0000:0000:0000:0000:0000:0000:0000", 39) != 1) { + return 1; + } + + if(IsIpv6Host("0:0:0:0:0:0:0:0", 19) != 1) { + return 1; + } + + if(IsIpv6Host("192:168:1:1:0:0:0:0", 19) != 1) { + return 1; + } + + if(IsIpv6Host("999.oogle.com", 14) != 0) { + return 1; + } + + if (IsIpv6Host("192.168.255.255", 15) != 0) { + return 1; + } + + if (IsIpv6Host("192.168.255.255/testurl.html", 28) != 0) { + return 1; + } + + if (IsIpv6Host("www.google.com", 14) != 0) { + return 1; + } + + return 0; +} + +#endif /* UNITTESTS */ + +void MimeDecRegisterTests(void) { +#ifdef UNITTESTS + UtRegisterTest("MimeDecParseLineTest01", MimeDecParseLineTest01, 0); + UtRegisterTest("MimeDecParseLineTest02", MimeDecParseLineTest02, 0); + UtRegisterTest("MimeDecParseFullMsgTest01", MimeDecParseFullMsgTest01, 0); + UtRegisterTest("MimeBase64DecodeTest01", MimeBase64DecodeTest01, 0); + UtRegisterTest("MimeIsExeURLTest01", MimeIsExeURLTest01, 0); + UtRegisterTest("MimeIsIpv4HostTest01", MimeIsIpv4HostTest01, 0); + UtRegisterTest("MimeIsIpv6HostTest01", MimeIsIpv6HostTest01, 0); +#endif /* UNITTESTS */ +} diff --git a/src/mime-decode.h b/src/mime-decode.h new file mode 100644 index 0000000000..381907c66e --- /dev/null +++ b/src/mime-decode.h @@ -0,0 +1,240 @@ +/* Copyright (C) 2012 BAE Systems + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author David Abarbanel + * + */ + +#ifndef MIME_DECODE_H_ +#define MIME_DECODE_H_ + +#include +#include +#include + +#include "suricata.h" +#include "util-base64.h" +#include "util-debug.h" + +/* Header Flags */ + +/* Content Flags */ +#define CTNT_IS_MSG 1 +#define CTNT_IS_ENV 2 +#define CTNT_IS_ENCAP 4 +#define CTNT_IS_BODYPART 8 +#define CTNT_IS_MULTIPART 16 +#define CTNT_IS_ATTACHMENT 32 +#define CTNT_IS_BASE64 64 +#define CTNT_IS_QP 128 +#define CTNT_IS_TEXT 256 +#define CTNT_IS_HTML 512 + +/* URL Flags */ +#define URL_IS_IP4 1 +#define URL_IS_IP6 2 +#define URL_IS_EXE 4 + +/* Anomaly Flags */ +#define ANOM_INVALID_BASE64 1 /* invalid base64 chars */ +#define ANOM_INVALID_QP 2 /* invalid qouted-printable chars */ +#define ANOM_LONG_HEADER_NAME 4 /* header is abnormally long */ +#define ANOM_LONG_HEADER_VALUE 8 /* header value is abnormally long + * (includes multi-line) */ +#define ANOM_LONG_LINE 16 /* Lines that exceed 998 octets */ +#define ANOM_LONG_ENC_LINE 32 /* Lines that exceed 76 octets */ +#define ANOM_MALFORMED_MSG 64 /* Misc msg format errors found */ + +/* Pubicly exposed size constants */ +#define DATA_CHUNK_SIZE 3072 /* Should be divisible by 3 */ +#define LINEREM_SIZE 256 + +/* Mime Parser Constants */ +#define HEADER_READY 0x01 +#define HEADER_STARTED 0x02 +#define HEADER_DONE 0x03 +#define BODY_STARTED 0x04 +#define BODY_DONE 0x05 +#define BODY_END_BOUND 0x06 +#define PARSE_DONE 0x07 +#define PARSE_ERROR 0x08 + +/** + * \brief Mime Decoder Error Codes + */ +typedef enum MimeDecRetCode { + MIME_DEC_OK = 0, + MIME_DEC_MORE = 1, + MIME_DEC_ERR_DATA = -1, + MIME_DEC_ERR_MEM = -2, + MIME_DEC_ERR_PARSE = -3 +} MimeDecRetCode; + +/** + * \brief Structure for containing configuration options + * + */ +typedef struct MimeDecConfig { + int decode_base64; /**< Decode base64 bodies */ + int decode_quoted_printable; /**< Decode quoted-printable bodies */ + int extract_urls; /**< Extract and store URLs in data structure */ + uint32_t header_value_depth; /**< Depth of which to store header values + (Default is 2000) */ +} MimeDecConfig; + +/** + * \brief This represents a header field name and associated value + */ +typedef struct MimeDecField { + char *name; /**< Name of the header field */ + uint32_t name_len; /**< Length of the name */ + char *value; /**< Value of the header field */ + uint32_t value_len; /**< Length of the value */ + struct MimeDecField *next; /**< Pointer to next field */ +} MimeDecField; + +/** + * \brief This represents a URL value node in a linked list + * + * Since HTML can sometimes contain a high number of URLs, this + * structure only features the URL host name/IP or those that are + * pointing to an executable file (see url_flags to determine which). + */ +typedef struct MimeDecUrl { + char *url; /**< String representation of full or partial URL */ + uint32_t url_len; /**< Length of the URL string */ + uint32_t url_flags; /**< Flags indicating type of URL */ + uint32_t url_cnt; /**< Count of URLs with same value */ + struct MimeDecUrl *next; /**< Pointer to next URL */ +} MimeDecUrl; + +/** + * \brief This represents the MIME Entity (or also top level message) in a + * child-sibling tree + */ +typedef struct MimeDecEntity { + MimeDecField *field_list; /**< Pointer to list of header fields */ + MimeDecUrl *url_list; /**< Pointer to list of URLs */ + uint32_t body_len; /**< Length of body (prior to any decoding) */ + uint32_t decoded_body_len; /**< Length of body after decoding */ + uint32_t header_flags; /**< Flags indicating header characteristics */ + uint32_t ctnt_flags; /**< Flags indicating type of content */ + uint32_t anomaly_flags; /**< Flags indicating an anomaly in the message */ + char *filename; /**< Name of file attachment */ + uint32_t filename_len; /**< Length of file attachment name */ + char *ctnt_type; /**< Quick access pointer to short-hand content type field */ + uint32_t ctnt_type_len; /**< Length of content type field value */ + char *msg_id; /**< Quick access pointer to message Id */ + uint32_t msg_id_len; /**< Quick access pointer to message Id */ + struct MimeDecEntity *next; /**< Pointer to list of sibling entities */ + struct MimeDecEntity *child; /**< Pointer to list of child entities */ +} MimeDecEntity; + +/** + * \brief Structure contains boundary and entity for the current node (entity) + * in the stack + * + */ +typedef struct MimeDecStackNode { + MimeDecEntity *data; /**< Pointer to the entity data structure */ + char *bdef; /**< Copy of boundary definition for child entity */ + uint32_t bdef_len; /**< Boundary length for child entity */ + int is_encap; /**< Flag indicating entity is encapsulated in message */ + struct MimeDecStackNode *next; /**< Pointer to next item on the stack */ +} MimeDecStackNode; + +/** + * \brief Structure holds the top of the stack along with some free reusable nodes + * + */ +typedef struct MimeDecStack { + MimeDecStackNode *top; /**< Pointer to the top of the stack */ + MimeDecStackNode *free_nodes; /**< Pointer to the list of free nodes */ + uint32_t free_nodes_cnt; /**< Count of free nodes in the list */ +} MimeDecStack; + +/** + * \brief Structure contains a list of value and lengths for robust data processing + * + */ +typedef struct DataValue { + char *value; /**< Copy of data value */ + uint32_t value_len; /**< Length of data value */ + struct DataValue *next; /**< Pointer to next value in the list */ +} DataValue; + +/** + * \brief Structure contains the current state of the MIME parser + * + */ +typedef struct MimeDecParseState { + MimeDecEntity *msg; /**< Pointer to the top-level message entity */ + MimeDecStack *stack; /**< Pointer to the top of the entity stack */ + char *hname; /**< Copy of the last known header name */ + uint32_t hlen; /**< Length of the last known header name */ + DataValue *hvalue; /**< Pointer to the incomplete header value list */ + uint32_t hvlen; /**< Total length of value list */ + char linerem[LINEREM_SIZE]; /**< Remainder from previous line (for URL extraction) */ + uint16_t linerem_len; /**< Length of remainder from previous line */ + char 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 */ + uint32_t data_chunk_len; /**< Length of data chunk */ + int found_child; /**< Flag indicating a child entity was found */ + int body_begin; /**< Currently at beginning of body */ + int body_end; /**< Currently at end of body */ + uint8_t state_flag; /**< Flag representing current state of parser */ + void *data; /**< Pointer to data specific to the caller */ + int (*dataChunkProcessor) (const uint8_t *chunk, uint32_t len, + struct MimeDecParseState *state); /**< Data chunk processing function callback */ +} MimeDecParseState; + +/* Config functions */ +void MimeDecSetConfig(MimeDecConfig *config); +MimeDecConfig * MimeDecGetConfig(void); + +/* Memory functions */ +void MimeDecFreeEntity(MimeDecEntity *entity); +void MimeDecFreeField(MimeDecField *field); +void MimeDecFreeUrl(MimeDecUrl *url); + +/* List functions */ +MimeDecField * MimeDecAddField(MimeDecEntity *entity); +MimeDecField * MimeDecFindField(const MimeDecEntity *entity, const char *name); +MimeDecUrl * MimeDecAddUrl(MimeDecEntity *entity); +MimeDecEntity * MimeDecAddEntity(MimeDecEntity *parent); + +/* Helper functions */ +MimeDecField * MimeDecFillField(MimeDecEntity *entity, const char *name, + uint32_t nlen, const char *value, uint32_t vlen, int copy_name_value); + +/* Parser functions */ +MimeDecParseState * MimeDecInitParser(void *data, int (*dcpfunc)(const uint8_t *chunk, + uint32_t len, MimeDecParseState *state)); +void MimeDecDeInitParser(MimeDecParseState *state); +int MimeDecParseComplete(MimeDecParseState *state); +int MimeDecParseLine(const char *line, const uint32_t len, MimeDecParseState *state); +MimeDecEntity * MimeDecParseFullMsg(const char *buf, uint32_t blen, void *data, + int (*dcpfunc)(const uint8_t *chunk, uint32_t len, MimeDecParseState *state)); + +/* Test functions */ +void MimeDecRegisterTests(void); + +#endif diff --git a/src/suricata.c b/src/suricata.c index 5d756eb6d4..13cdd8ea1f 100644 --- a/src/suricata.c +++ b/src/suricata.c @@ -151,6 +151,8 @@ #include "util-coredump-config.h" +#include "mime-decode.h" + #include "defrag.h" #include "runmodes.h" diff --git a/src/util-base64.c b/src/util-base64.c new file mode 100644 index 0000000000..40194d3270 --- /dev/null +++ b/src/util-base64.c @@ -0,0 +1,146 @@ +/* Copyright (C) 2007-2012 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author David Abarbanel + * + */ + +#include "util-base64.h" + +/* Constants */ +#define BASE64_TABLE_MAX 122 + +/* Base64 character to index conversion table */ +/* Characters are mapped as "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" */ +static const int b64table[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, + -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51 }; + +/** + * \brief Gets a base64-decoded value from an encoded character + * + * \param c The encoded character + * + * \return The decoded value (0 or above), or -1 if the parameter is invalid + */ +static inline int GetBase64Value(char c) { + + int val = -1; + + /* Pull from conversion table */ + if (c <= BASE64_TABLE_MAX) { + val = b64table[(int) c]; + } + + return val; +} + +/** + * \brief Decodes a 4-byte base64-encoded block into a 3-byte ascii-encoded block + * + * \param ascii the 3-byte ascii output block + * \param b64 the 4-byte base64 input block + * + * \return none + */ +static inline void DecodeBase64Block(uint8_t ascii[ASCII_BLOCK], uint8_t b64[B64_BLOCK]) { + + ascii[0] = (uint8_t) (b64[0] << 2) | (b64[1] >> 4); + ascii[1] = (uint8_t) (b64[1] << 4) | (b64[2] >> 2); + ascii[2] = (uint8_t) (b64[2] << 6) | (b64[3]); +} + +/** + * \brief Decodes a base64-encoded string buffer into an ascii-encoded byte buffer + * + * \param dest The destination byte buffer + * \param src The source string + * \param len The length of the source string + * + * \return Number of bytes decoded, or 0 if no data is decoded or it fails + */ +uint32_t DecodeBase64(uint8_t *dest, const char *src, uint32_t len) { + + int val; + uint32_t padding = 0, numDecoded = 0, bbidx = 0, valid = 1, i; + uint8_t *dptr = dest; + uint8_t b64[B64_BLOCK]; + + /* Traverse through each alpha-numeric letter in the source array */ + for(i = 0; i < len && src[i] != 0; i++) { + + /* Get decimal representation */ + val = GetBase64Value(src[i]); + if (val < 0) { + + /* Invalid character found, so decoding fails */ + if (src[i] != '=') { + valid = 0; + numDecoded = 0; + break; + } + padding++; + } + + /* For each alpha-numeric letter in the source array, find the numeric + * value */ + b64[bbidx++] = (val > 0 ? val : 0); + + /* Decode every 4 base64 bytes into 3 ascii bytes */ + if (bbidx == B64_BLOCK) { + + /* For every 4 bytes, add 3 bytes but deduct the '=' padded blocks */ + numDecoded += ASCII_BLOCK - (padding < B64_BLOCK ? + padding : ASCII_BLOCK); + + /* Decode base-64 block into ascii block and move pointer */ + DecodeBase64Block(dptr, b64); + dptr += ASCII_BLOCK; + + /* Reset base-64 block and index */ + bbidx = 0; + padding = 0; + } + } + + /* Finish remaining b64 bytes by padding */ + if (valid && bbidx > 0) { + + /* Decode remaining */ + numDecoded += ASCII_BLOCK - (B64_BLOCK - bbidx); + DecodeBase64Block(dptr, b64); + } + + if (numDecoded == 0) { + SCLogDebug("base64 decoding failed"); + } + + return numDecoded; +} diff --git a/src/util-base64.h b/src/util-base64.h new file mode 100644 index 0000000000..5e7c39c8a4 --- /dev/null +++ b/src/util-base64.h @@ -0,0 +1,54 @@ +/* Copyright (C) 2007-2012 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author David Abarbanel + * + */ + +#ifndef __UTIL_BASE64_H_ +#define __UTIL_BASE64_H_ + +#include "suricata-common.h" +#include "threads.h" +#include "debug.h" +#include "decode.h" + +#include "detect.h" +#include "detect-parse.h" + +#include "detect-engine.h" +#include "detect-engine-mpm.h" +#include "detect-engine-state.h" + +#include "flow.h" +#include "flow-var.h" +#include "flow-util.h" + +#include "util-debug.h" +#include "util-spm-bm.h" + +/* Constants */ +#define ASCII_BLOCK 3 +#define B64_BLOCK 4 + +/* Function prototypes */ +uint32_t DecodeBase64(uint8_t *dest, const char *src, uint32_t len); + +#endif diff --git a/suricata.yaml.in b/suricata.yaml.in index 715c53e4cc..f1da99ba07 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -1294,6 +1294,23 @@ app-layer: # double-decode-path: no # double-decode-query: no +# Configure SMTP-MIME Decoder enhancements +smtp-mime: + + # Decode MIME messages from SMTP transactions (may be resource intensive) + # This field supercedes all others because it turns the entire process on or off + decode-mime: no + + # Decode MIME entity bodies (ie. base64, quoted-printable, etc.) + decode-base64: yes + decode-quoted-printable: yes + + # Maximum bytes per header data value stored in the data structure (default is 2000) + header-value-depth: 2000 + + # Extract URLs and save in state data structure + extract-urls: yes + # Profiling settings. Only effective if Suricata has been built with the # the --enable-profiling configure flag. #