/* Copyright (C) 2012 BAE Systems * Copyright (C) 2020-2021 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 "suricata-common.h" #include "suricata.h" #include "app-layer-smtp.h" #include "util-decode-mime.h" #include "util-ip.h" #include "util-spm-bs.h" #include "util-unittest.h" #include "util-memcmp.h" #include "util-print.h" #include "util-validate.h" #include "rust.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 MSG_STR "message/" #define MULTIPART_STR "multipart/" #define QP_STR "quoted-printable" #define TXT_STR "text/plain" #define HTML_STR "text/html" /* 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 = { true, true, true, NULL, false, false, 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", ".ps", ".ps1", ".sh", ".run", ".hta", ".bin", ".elf", 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 */ static void PrintChars(int log_level, const char *label, const uint8_t *src, uint32_t len) { #ifdef DEBUG if (log_level <= sc_log_global_log_level) { printf("[%s]\n", label); PrintRawDataFp(stdout, (uint8_t *)src, len); } #endif } /** * \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("Invalid null configuration parameters"); } } /** * \brief Get global config policy * * \return config data structure */ MimeDecConfig * MimeDecGetConfig(void) { return &mime_dec_config; } /** * \brief Follow the 'next' pointers to the leaf * * \param node The root entity * * \return Pointer to leaf on 'next' side * */ static MimeDecEntity *findLastSibling(MimeDecEntity *node) { if (node == NULL) return NULL; while(node->next != NULL) node = node->next; return node; } /** * \brief Frees a mime entity tree * * \param entity The root entity * * \return none * */ void MimeDecFreeEntity (MimeDecEntity *entity) { if (entity == NULL) return; MimeDecEntity *lastSibling = findLastSibling(entity); while (entity != NULL) { /* move child to next to transform the tree into a list */ if (entity->child != NULL) { lastSibling->next = entity->child; entity->child = NULL; lastSibling = findLastSibling(lastSibling); } MimeDecEntity *next = entity->next; DEBUG_VALIDATE_BUG_ON( (next != NULL && entity == lastSibling) || (next == NULL && entity != lastSibling)); MimeDecFreeField(entity->field_list); MimeDecFreeUrl(entity->url_list); SCFree(entity->filename); SCFree(entity); entity = next; } } /** * \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)) { return NULL; } memset(node, 0x00, sizeof(MimeDecField)); /* 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 header fields with the specified name * * \param entity The entity to search * \param name The header name (lowercase) * * \return number of items found * */ int MimeDecFindFieldsForEach(const MimeDecEntity *entity, const char *name, int (*DataCallback)(const uint8_t *val, const size_t, void *data), void *data) { MimeDecField *curr = entity->field_list; int found = 0; while (curr != NULL) { /* name is stored lowercase */ if (strlen(name) == curr->name_len) { if (SCMemcmp(curr->name, name, curr->name_len) == 0) { if (DataCallback(curr->value, curr->value_len, data)) found++; } } curr = curr->next; } return found; } /** * \brief Searches for a header field with the specified name * * \param entity The entity to search * \param name The header name (lowercase) * * \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) { /* name is stored lowercase */ if (strlen(name) == curr->name_len) { if (SCMemcmp(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 * */ static MimeDecUrl * MimeDecAddUrl(MimeDecEntity *entity, uint8_t *url, uint32_t url_len, uint8_t flags) { MimeDecUrl *node = SCMalloc(sizeof(MimeDecUrl)); if (unlikely(node == NULL)) { return NULL; } memset(node, 0x00, sizeof(MimeDecUrl)); node->url = url; node->url_len = url_len; node->url_flags = flags; /* 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 *node = SCMalloc(sizeof(MimeDecEntity)); if (unlikely(node == NULL)) { 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; parent->last_child = node; } else { parent->last_child->next = node; parent->last_child = 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 * * \return The field or NULL if the operation fails * * name and val are passed as ptr to ptr and each will be set to NULL * only if the pointer is consumed. This gives the caller an easy way * to free the memory if not consumed. */ static MimeDecField *MimeDecFillField( MimeDecEntity *entity, uint8_t **name, uint32_t nlen, uint8_t **value, uint32_t vlen) { if (nlen == 0 && vlen == 0) return NULL; MimeDecField *field = MimeDecAddField(entity); if (unlikely(field == NULL)) { return NULL; } if (nlen > 0) { uint8_t *n = *name; /* convert to lowercase and store */ for (uint32_t u = 0; u < nlen; u++) n[u] = u8_tolower(n[u]); field->name = (uint8_t *)n; field->name_len = nlen; *name = NULL; } if (vlen > 0) { field->value = (uint8_t *)*value; field->value_len = vlen; *value = NULL; } 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)) { 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)) { 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 olen The output length of the single value * * \return pointer to a single value, otherwise NULL if it fails or is zero-length */ static uint8_t *GetFullValue(const DataValue *dv, uint32_t *olen) { uint32_t offset = 0; uint8_t *val = NULL; uint32_t len = 0; *olen = 0; /* First calculate total length */ for (const DataValue *curr = dv; curr != NULL; curr = curr->next) { len += curr->value_len; } /* Must have at least one character in the value */ if (len > 0) { val = SCCalloc(1, len); if (unlikely(val == NULL)) { return NULL; } for (const DataValue *curr = dv; curr != NULL; curr = curr->next) { memcpy(val + offset, curr->value, curr->value_len); offset += curr->value_len; } } *olen = len; return val; } /** * \brief Find a string while searching up to N characters within a source * buffer * * \param src The source string (not null-terminated) * \param len The length of the source string * \param find The string to find (null-terminated) * \param find_len length of the 'find' string * * \return Pointer to the position it was found, otherwise NULL if not found */ static inline uint8_t *FindBuffer( const uint8_t *src, uint32_t len, const uint8_t *find, uint16_t find_len) { /* Use utility search function */ return BasicSearchNocase(src, len, find, find_len); } /** * \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 uint8_t * GetLine(uint8_t *buf, uint32_t blen, uint8_t **remainPtr, uint32_t *tokLen) { uint32_t i; uint8_t *tok; /* So that it can be used just like strtok_r */ if (buf == NULL) { buf = *remainPtr; } else { *remainPtr = buf; } if (buf == NULL) return NULL; 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 uint8_t * GetToken(uint8_t *buf, uint32_t blen, const char *delims, uint8_t **remainPtr, uint32_t *tokenLen) { uint32_t i, j, delimFound = 0; uint8_t *tok = NULL; /* So that it can be used just like strtok_r */ if (buf == NULL) { buf = *remainPtr; } else { *remainPtr = buf; } if (buf == NULL) return NULL; /* 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; /* Lets save the most recent header */ if (state->hname != NULL || state->hvalue != NULL) { SCLogDebug("Storing last header"); uint32_t vlen; uint8_t *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) == NULL) { ret = MIME_DEC_ERR_MEM; } } 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 */ ret = MIME_DEC_ERR_MEM; } } SCFree(val); 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(const uint8_t *url, uint32_t len) { int isExeUrl = 0; uint32_t i, extLen; uint8_t *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 = FindBuffer(url, len, (uint8_t *)UrlExeExts[i], (uint16_t)strlen(UrlExeExts[i])); if (ext != NULL && (ext + extLen - url == (int)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(const uint8_t *urlhost, uint32_t len) { struct sockaddr_in sa; char tempIp[MAX_IP4_CHARS + 1]; /* Cut off at '/' */ uint32_t i = 0; for ( ; i < len && urlhost[i] != 0; i++) { if (urlhost[i] == '/') { break; } } /* Too many chars */ if (i > MAX_IP4_CHARS) { return 0; } /* Create null-terminated string */ memcpy(tempIp, urlhost, i); tempIp[i] = '\0'; if (!IPv4AddressStringIsValid(tempIp)) return 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(const uint8_t *urlhost, uint32_t len) { struct in6_addr in6; 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 */ memcpy(tempIp, urlhost, i); tempIp[i] = '\0'; if (!IPv6AddressStringIsValid(tempIp)) return 0; return inet_pton(AF_INET6, tempIp, &in6); } /** * \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 (lowercase) * \param url_len The matching URL string length * * \return URL object or NULL if not found */ static MimeDecUrl *FindExistingUrl(MimeDecEntity *entity, uint8_t *url, uint32_t url_len) { MimeDecUrl *curr = entity->url_list; while (curr != NULL) { if (url_len == curr->url_len) { /* search url and stored url are both in * lowercase, so we can do an exact match */ if (SCMemcmp(curr->url, url, url_len) == 0) { break; } } curr = curr->next; } return curr; } /** * \brief This function searches a text or html line for a URL string * * The URL strings are searched for using the URL schemes defined in the global * MIME config e.g. "http", "https". * * The found URL strings are stored in lowercase and with their schemes * stripped unless the MIME config flag for log_url_scheme is set. * * 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 uint8_t *line, uint32_t len, MimeDecParseState *state) { int ret = MIME_DEC_OK; MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; MimeDecConfig *mdcfg = MimeDecGetConfig(); uint8_t *fptr, *remptr, *tok = NULL, *tempUrl, *urlHost; uint32_t tokLen = 0, i, tempUrlLen, urlHostLen; uint16_t schemeStrLen = 0; uint8_t flags = 0; ConfNode *scheme = NULL; char *schemeStr = NULL; if (mdcfg != NULL && mdcfg->extract_urls_schemes == NULL) { SCLogDebug("Error: MIME config extract_urls_schemes was NULL."); return MIME_DEC_ERR_DATA; } TAILQ_FOREACH (scheme, &mdcfg->extract_urls_schemes->head, next) { schemeStr = scheme->val; // checked against UINT16_MAX when setting in SMTPConfigure schemeStrLen = (uint16_t)strlen(schemeStr); remptr = (uint8_t *)line; do { SCLogDebug("Looking for URL String starting with: %s", schemeStr); /* Check for token definition */ fptr = FindBuffer(remptr, len - (remptr - line), (uint8_t *)schemeStr, schemeStrLen); if (fptr != NULL) { if (!mdcfg->log_url_scheme) { fptr += schemeStrLen; /* Strip scheme from stored URL */ } tok = GetToken(fptr, len - (fptr - line), " \"\'<>]\t", &remptr, &tokLen); if (tok == fptr) { SCLogDebug("Found url string"); /* First copy to temp URL string */ tempUrl = SCMalloc(tokLen); if (unlikely(tempUrl == NULL)) { 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 is all lowercase */ tempUrl[tempUrlLen] = u8_tolower(tok[i]); tempUrlLen++; } urlHost = tempUrl; urlHostLen = tempUrlLen; if (mdcfg->log_url_scheme) { /* tempUrl contains the scheme in the string but * IsIpv4Host & IsPv6Host methods below require * an input URL string with scheme stripped. Get a * reference sub-string urlHost which starts with * the host instead of the scheme. */ urlHost += schemeStrLen; urlHostLen -= schemeStrLen; } /* Determine if URL points to an EXE */ if (IsExeUrl(tempUrl, tempUrlLen)) { flags |= URL_IS_EXE; PrintChars(SC_LOG_DEBUG, "EXE URL", tempUrl, tempUrlLen); } /* Make sure remaining URL exists */ if (tempUrlLen > 0) { if (!(FindExistingUrl(entity, tempUrl, tempUrlLen))) { /* Now look for numeric IP */ if (IsIpv4Host(urlHost, urlHostLen)) { flags |= URL_IS_IP4; PrintChars(SC_LOG_DEBUG, "IP URL4", tempUrl, tempUrlLen); } else if (IsIpv6Host(urlHost, urlHostLen)) { flags |= URL_IS_IP6; PrintChars(SC_LOG_DEBUG, "IP URL6", tempUrl, tempUrlLen); } /* Add URL list item */ MimeDecAddUrl(entity, tempUrl, tempUrlLen, flags); } else { SCFree(tempUrl); } } else { SCFree(tempUrl); } /* Reset flags for next URL */ flags = 0; } } } 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; uint8_t *remainPtr, *tok; uint32_t tokLen; if ((state->stack != NULL) && (state->stack->top != NULL) && (state->stack->top->data != NULL)) { MimeDecConfig *mdcfg = MimeDecGetConfig(); if (mdcfg != NULL && mdcfg->extract_urls) { MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; /* If plain text or html, then look for URLs */ if (((entity->ctnt_flags & CTNT_IS_TEXT) || (entity->ctnt_flags & CTNT_IS_MSG) || (entity->ctnt_flags & CTNT_IS_HTML)) && ((entity->ctnt_flags & CTNT_IS_ATTACHMENT) == 0)) { /* Parse each line one by one */ remainPtr = (uint8_t *)chunk; do { tok = GetLine( remainPtr, len - (remainPtr - (uint8_t *)chunk), &remainPtr, &tokLen); if (tok != remainPtr) { /* 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 - (uint8_t *)chunk < (int)len); } } /* Now invoke callback */ if (state->DataChunkProcessorFunc != NULL) { ret = state->DataChunkProcessorFunc(chunk, len, state); if (ret != MIME_DEC_OK) { SCLogDebug("Error: state->dataChunkProcessor() callback function" " failed"); } } } else { SCLogDebug("Error: Stack pointer missing"); ret = MIME_DEC_ERR_DATA; } /* 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 consumed from `buf` */ static uint32_t ProcessBase64Remainder( const uint8_t *buf, const uint32_t len, MimeDecParseState *state, int force) { uint32_t buf_consumed = 0; /* consumed bytes from 'buf' */ uint8_t cnt = 0; uint8_t block[B64_BLOCK]; SCLogDebug("len %u force %d", len, force); /* should be impossible, but lets be defensive */ DEBUG_VALIDATE_BUG_ON(state->bvr_len > B64_BLOCK); if (state->bvr_len > B64_BLOCK) { state->bvr_len = 0; return 0; } /* Strip spaces in remainder */ for (uint8_t i = 0; i < state->bvr_len; i++) { if (IsBase64Alphabet(state->bvremain[i])) { block[cnt++] = state->bvremain[i]; } } /* if we don't have 4 bytes see if we can fill it from `buf` */ if (buf && len > 0 && cnt != B64_BLOCK) { for (uint32_t i = 0; i < len && cnt < B64_BLOCK; i++) { if (IsBase64Alphabet(buf[i])) { block[cnt++] = buf[i]; } buf_consumed++; } DEBUG_VALIDATE_BUG_ON(cnt > B64_BLOCK); for (uint32_t i = 0; i < cnt; i++) { state->bvremain[i] = block[i]; } state->bvr_len = cnt; } else if (!force && cnt != B64_BLOCK) { SCLogDebug("incomplete data and no buffer to backfill"); return 0; } /* in force mode pad the block */ if (force && cnt != B64_BLOCK) { SCLogDebug("force and cnt %u != %u", cnt, B64_BLOCK); for (uint8_t i = state->bvr_len; i < B64_BLOCK; i++) { state->bvremain[state->bvr_len++] = '='; } } /* 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 */ uint32_t ret = ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, state); if (ret != MIME_DEC_OK) { SCLogDebug("Error: ProcessDecodedDataChunk() function failed"); } } if (state->bvr_len == B64_BLOCK || force) { uint32_t consumed_bytes = 0; uint32_t remdec = 0; const uint32_t avail_space = DATA_CHUNK_SIZE - state->data_chunk_len; PrintChars(SC_LOG_DEBUG, "BASE64 INPUT (bvremain)", state->bvremain, state->bvr_len); Base64Ecode code = DecodeBase64(state->data_chunk + state->data_chunk_len, avail_space, state->bvremain, state->bvr_len, &consumed_bytes, &remdec, BASE64_MODE_RFC2045); SCLogDebug("DecodeBase64 result %u", code); if (remdec > 0 && (code == BASE64_ECODE_OK || code == BASE64_ECODE_BUF)) { PrintChars(SC_LOG_DEBUG, "BASE64 DECODED (bvremain)", state->data_chunk + state->data_chunk_len, remdec); 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 */ uint32_t ret = ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, state); if (ret != MIME_DEC_OK) { SCLogDebug("Error: ProcessDecodedDataChunk() function " "failed"); } } } else if (code == BASE64_ECODE_ERR) { /* 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; } DEBUG_VALIDATE_BUG_ON(buf_consumed > len); return buf_consumed; } static inline MimeDecRetCode ProcessBase64BodyLineCopyRemainder( const uint8_t *buf, const uint32_t buf_len, const uint32_t offset, MimeDecParseState *state) { DEBUG_VALIDATE_BUG_ON(offset > buf_len); if (offset > buf_len) return MIME_DEC_ERR_DATA; for (uint32_t i = offset; i < buf_len; i++) { // Skip any characters outside of the base64 alphabet as per RFC 2045 if (IsBase64Alphabet(buf[i])) { DEBUG_VALIDATE_BUG_ON(state->bvr_len >= B64_BLOCK); if (state->bvr_len >= B64_BLOCK) return MIME_DEC_ERR_DATA; state->bvremain[state->bvr_len++] = buf[i]; } } return MIME_DEC_OK; } /** * \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 uint8_t *buf, uint32_t len, MimeDecParseState *state) { int ret = MIME_DEC_OK; uint32_t numDecoded, remaining = len, offset = 0; /* Track long line TODO should we count space padding too? */ 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("max encoded input line length exceeded %u > %u", len, MAX_ENC_LINE_LEN); } if (state->bvr_len + len < B64_BLOCK) { return ProcessBase64BodyLineCopyRemainder(buf, len, 0, state); } /* First process remaining from previous line. We will consume * state->bvremain, filling it from 'buf' until we have a properly * sized block. Spaces are skipped (rfc2045). If state->bvr_len * is not 0 after processing we have no data left at 'buf'. */ if (state->bvr_len > 0) { uint32_t consumed = ProcessBase64Remainder(buf, len, state, 0); DEBUG_VALIDATE_BUG_ON(consumed > len); if (consumed > len) return MIME_DEC_ERR_PARSE; uint32_t left = len - consumed; if (left < B64_BLOCK) { DEBUG_VALIDATE_BUG_ON(left + state->bvr_len > B64_BLOCK); return ProcessBase64BodyLineCopyRemainder(buf, len, consumed, state); } remaining -= consumed; offset = consumed; } while (remaining > 0 && remaining >= B64_BLOCK) { uint32_t consumed_bytes = 0; uint32_t avail_space = DATA_CHUNK_SIZE - state->data_chunk_len; PrintChars(SC_LOG_DEBUG, "BASE64 INPUT (line)", buf + offset, remaining); Base64Ecode code = DecodeBase64(state->data_chunk + state->data_chunk_len, avail_space, buf + offset, remaining, &consumed_bytes, &numDecoded, BASE64_MODE_RFC2045); SCLogDebug("DecodeBase64 result %u", code); DEBUG_VALIDATE_BUG_ON(consumed_bytes > remaining); if (consumed_bytes > remaining) return MIME_DEC_ERR_PARSE; uint32_t leftover_bytes = remaining - consumed_bytes; if (numDecoded > 0 && (code == BASE64_ECODE_OK || code == BASE64_ECODE_BUF)) { PrintChars(SC_LOG_DEBUG, "BASE64 DECODED (line)", state->data_chunk + state->data_chunk_len, numDecoded); 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); return MIME_DEC_ERR_PARSE; } /* 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"); break; } } } else if (code == BASE64_ECODE_ERR) { /* Track failed base64 */ state->stack->top->data->anomaly_flags |= ANOM_INVALID_BASE64; state->msg->anomaly_flags |= ANOM_INVALID_BASE64; SCLogDebug("Error: DecodeBase64() function failed"); return MIME_DEC_ERR_DATA; } /* corner case: multiples spaces in the last data, leading it to exceed the block * size. We strip of spaces this while storing it in bvremain */ if (consumed_bytes == 0 && leftover_bytes > B64_BLOCK) { DEBUG_VALIDATE_BUG_ON(state->bvr_len != 0); ret = ProcessBase64BodyLineCopyRemainder(buf, len, offset, state); break; } else if (leftover_bytes > 0 && leftover_bytes <= B64_BLOCK) { /* If remaining is 4 by this time, we encountered spaces during processing */ DEBUG_VALIDATE_BUG_ON(state->bvr_len != 0); ret = ProcessBase64BodyLineCopyRemainder(buf, len, offset + consumed_bytes, state); break; } /* Update counts */ remaining = leftover_bytes; offset += consumed_bytes; } if (ret == MIME_DEC_OK && state->data_chunk_len > 0) { ret = ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, state); } 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 int8_t DecodeQPChar(char h) { int8_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 uint8_t *buf, uint32_t len, MimeDecParseState *state) { int ret = MIME_DEC_OK; uint32_t remaining, offset; MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; uint8_t 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); } if (len == 0) { memcpy(state->data_chunk + state->data_chunk_len, buf + len, state->current_line_delimiter_len); state->data_chunk_len += state->current_line_delimiter_len; return ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, state); } 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++; /* Add CRLF sequence if end of line, unless its a partial line */ if (remaining == 1 && state->current_line_delimiter_len > 0) { memcpy(state->data_chunk + state->data_chunk_len, CRLF, EOL_LEN); state->data_chunk_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 = (uint8_t)(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++; /* Add CRLF sequence if end of line, unless for partial lines */ if (remaining == 3 && state->current_line_delimiter_len > 0) { memcpy(state->data_chunk + state->data_chunk_len, CRLF, EOL_LEN); state->data_chunk_len += EOL_LEN; } /* Account for extra 2 characters in 3-character 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 uint8_t *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"); /* 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 ? remaining : avail; /* Copy over to buffer */ memcpy(state->data_chunk + state->data_chunk_len, buf + offset, tobuf); state->data_chunk_len += tobuf; 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 == 0) { /* 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; } if (ret == MIME_DEC_OK) { ret = ProcessDecodedDataChunk(state->data_chunk, state->data_chunk_len, state); if (ret != MIME_DEC_OK) { SCLogDebug("Error: ProcessDecodedDataChunk() function " "failed"); } } // keep end of line for next call (and skip it on completion) memcpy(state->data_chunk, buf + offset, state->current_line_delimiter_len); state->data_chunk_len = state->current_line_delimiter_len; } 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 uint8_t * FindMimeHeaderStart(const uint8_t *buf, uint32_t blen, uint32_t *hlen) { uint32_t i, valid = 0; uint8_t *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 = (uint8_t *) 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 uint8_t *buf, uint32_t blen, MimeDecParseState *state) { int ret = MIME_DEC_OK; uint8_t *hname, *hval = NULL; DataValue *dv; uint32_t hlen, vlen; int finish_header = 0, new_header = 0; MimeDecConfig *mdcfg = MimeDecGetConfig(); DEBUG_VALIDATE_BUG_ON(state->current_line_delimiter_len == 0 && blen < SMTP_LINE_BUFFER_LIMIT); /* 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 >= (int)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 >= (int)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) { return MIME_DEC_ERR_MEM; } if (state->hvalue == NULL) { state->hvalue = dv; } dv->value = SCMalloc(vlen); if (unlikely(dv->value == NULL)) { return MIME_DEC_ERR_MEM; } memcpy(dv->value, 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; // Begin the body md5 computation if config asks so if (MimeDecGetConfig()->body_md5 && state->md5_ctx == NULL) { state->md5_ctx = SCMd5New(); SCMd5Update(state->md5_ctx, buf, blen + state->current_line_delimiter_len); } 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)) { return MIME_DEC_ERR_MEM; } memcpy(state->hname, hname, hlen); state->hlen = hlen; if (state->hvalue != NULL) { SCLogDebug("Error: Parser failed due to unexpected header " "value"); return MIME_DEC_ERR_DATA; } 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) { return MIME_DEC_ERR_MEM; } state->hvalue->value = SCMalloc(vlen); if (unlikely(state->hvalue->value == NULL)) { return MIME_DEC_ERR_MEM; } memcpy(state->hvalue->value, hval, vlen); state->hvalue->value_len = vlen; state->hvlen += vlen; } } } return ret; } /** * \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 uint8_t *buf, uint32_t len, MimeDecParseState *state) { int ret = MIME_DEC_OK; MimeDecField *field; uint8_t *rptr = NULL; uint32_t blen = 0; MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data; uint8_t bptr[RS_MIME_MAX_TOKEN_LEN]; /* 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 (FindBuffer(field->value, field->value_len, (const uint8_t *)BASE64_STR, (uint16_t)strlen(BASE64_STR))) { SCLogDebug("Base64 encoding found"); entity->ctnt_flags |= CTNT_IS_BASE64; } else if (FindBuffer(field->value, field->value_len, (const uint8_t *)QP_STR, (uint16_t)strlen(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) { bool truncated_name = false; if (rs_mime_find_header_token(field->value, field->value_len, (const uint8_t *)"filename", strlen("filename"), &bptr, &blen)) { SCLogDebug("File attachment found in disposition"); entity->ctnt_flags |= CTNT_IS_ATTACHMENT; if (blen > RS_MIME_MAX_TOKEN_LEN) { blen = RS_MIME_MAX_TOKEN_LEN; truncated_name = true; } /* Copy over using dynamic memory */ entity->filename = SCMalloc(blen); if (unlikely(entity->filename == NULL)) { return MIME_DEC_ERR_MEM; } memcpy(entity->filename, bptr, blen); entity->filename_len = blen; if (truncated_name) { state->stack->top->data->anomaly_flags |= ANOM_LONG_FILENAME; state->msg->anomaly_flags |= ANOM_LONG_FILENAME; } } } /* 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 */ // RS_MIME_MAX_TOKEN_LEN is RS_MIME_MAX_TOKEN_LEN on the rust side if (rs_mime_find_header_token(field->value, field->value_len, (const uint8_t *)"boundary", strlen("boundary"), &bptr, &blen)) { state->found_child = 1; entity->ctnt_flags |= CTNT_IS_MULTIPART; if (blen > (BOUNDARY_BUF - 2)) { state->stack->top->data->anomaly_flags |= ANOM_LONG_BOUNDARY; return MIME_DEC_ERR_PARSE; } /* Store boundary in parent node */ state->stack->top->bdef = SCMalloc(blen); if (unlikely(state->stack->top->bdef == NULL)) { return MIME_DEC_ERR_MEM; } memcpy(state->stack->top->bdef, bptr, blen); state->stack->top->bdef_len = (uint16_t)blen; } /* Look for file name (if not already found) */ if (!(entity->ctnt_flags & CTNT_IS_ATTACHMENT)) { bool truncated_name = false; if (rs_mime_find_header_token(field->value, field->value_len, (const uint8_t *)"name", strlen("name"), &bptr, &blen)) { SCLogDebug("File attachment found"); entity->ctnt_flags |= CTNT_IS_ATTACHMENT; if (blen > RS_MIME_MAX_TOKEN_LEN) { blen = RS_MIME_MAX_TOKEN_LEN; truncated_name = true; } /* Copy over using dynamic memory */ entity->filename = SCMalloc(blen); if (unlikely(entity->filename == NULL)) { return MIME_DEC_ERR_MEM; } memcpy(entity->filename, bptr, blen); entity->filename_len = blen; if (truncated_name) { state->stack->top->data->anomaly_flags |= ANOM_LONG_FILENAME; state->msg->anomaly_flags |= ANOM_LONG_FILENAME; } } } /* 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 (FindBuffer(entity->ctnt_type, entity->ctnt_type_len, (const uint8_t *)MSG_STR, (uint16_t)strlen(MSG_STR))) { SCLogDebug("Found encapsulated message entity"); entity->ctnt_flags |= CTNT_IS_ENV; /* Create and push child to stack */ MimeDecEntity *child = MimeDecAddEntity(entity); if (child == NULL) return MIME_DEC_ERR_MEM; child->ctnt_flags |= (CTNT_IS_ENCAP | CTNT_IS_MSG); PushStack(state->stack); state->stack->top->data = child; /* Mark as encapsulated child */ state->stack->top->is_encap = 1; /* Ready to parse headers */ state->state_flag = HEADER_READY; } else if (FindBuffer(entity->ctnt_type, entity->ctnt_type_len, (const uint8_t *)MULTIPART_STR, (uint16_t)strlen(MULTIPART_STR))) { /* Check for multipart */ SCLogDebug("Found multipart entity"); entity->ctnt_flags |= CTNT_IS_MULTIPART; } else if (FindBuffer(entity->ctnt_type, entity->ctnt_type_len, (const uint8_t *)TXT_STR, (uint16_t)strlen(TXT_STR))) { /* Check for plain text */ SCLogDebug("Found plain text entity"); entity->ctnt_flags |= CTNT_IS_TEXT; } else if (FindBuffer(entity->ctnt_type, entity->ctnt_type_len, (const uint8_t *)HTML_STR, (uint16_t)strlen(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"); } } MimeDecEntity *entity = (MimeDecEntity *)state->stack->top->data; if ((entity->ctnt_flags & (CTNT_IS_BASE64 | CTNT_IS_QP)) == 0) { // last eol of plaintext is the beginning of the boundary state->data_chunk_len = 0; } /* 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 uint8_t *buf, uint32_t len, uint16_t bdef_len, MimeDecParseState *state) { int ret = MIME_DEC_OK; uint8_t *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 = (uint8_t *) 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); if (child == NULL) return MIME_DEC_ERR_MEM; child->ctnt_flags |= CTNT_IS_BODYPART; PushStack(state->stack); state->stack->top->data = child; /* Reset flag */ state->found_child = 0; } 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); if (child == NULL) return MIME_DEC_ERR_MEM; 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 uint8_t *buf, uint32_t len, MimeDecParseState *state) { int ret = MIME_DEC_OK; uint8_t temp[BOUNDARY_BUF]; uint8_t *bstart; int body_found = 0; uint16_t tlen; /* pass empty lines on if we're parsing the body, otherwise we have no use * for them, and in fact they would disrupt the state tracking */ if (len == 0) { /* don't start a new body after an end bound based on an empty line */ if (state->state_flag == BODY_END_BOUND) { SCLogDebug("skip empty line"); return MIME_DEC_OK; } else if (state->state_flag == HEADER_DONE) { SCLogDebug("empty line, lets see if we skip it. We're in state %s", MimeDecParseStateGetStatus(state)); MimeDecEntity *entity = (MimeDecEntity *)state->stack->top->data; MimeDecConfig *mdcfg = MimeDecGetConfig(); if (entity != NULL && mdcfg != NULL) { if (mdcfg->decode_base64 && (entity->ctnt_flags & CTNT_IS_BASE64)) { SCLogDebug("skip empty line"); return MIME_DEC_OK; } SCLogDebug("not skipping empty line"); } } else { SCLogDebug("not skipping line at state %s", MimeDecParseStateGetStatus(state)); } } /* 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; if (tlen > BOUNDARY_BUF) { if (state->stack->top->data) state->stack->top->data->anomaly_flags |= ANOM_LONG_BOUNDARY; SCLogDebug("Error: Long boundary: tlen %u > %d. Set ANOM_LONG_BOUNDARY", tlen, BOUNDARY_BUF); return MIME_DEC_ERR_PARSE; } memcpy(temp, "--", 2); memcpy(temp + 2, node->bdef, node->bdef_len); /* Find either next boundary or end boundary */ bstart = FindBuffer(buf, len, temp, tlen); if (bstart != NULL) { ret = ProcessMimeBoundary(buf, len, node->bdef_len, state); if (ret != MIME_DEC_OK) { SCLogDebug("Error: ProcessMimeBoundary() function " "failed"); return ret; } } else { /* Otherwise add value to body */ body_found = 1; } } else { /* Otherwise add value to body */ body_found = 1; } } /* 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; } const char *MimeDecParseStateGetStatus(MimeDecParseState *state) { return StateFlags[state->state_flag]; } /** * \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 uint8_t *buf, uint32_t len, MimeDecParseState *state) { int ret = MIME_DEC_OK; SCLogDebug("START FLAG: %s", StateFlags[state->state_flag]); if (state->state_flag == PARSE_ERROR) { SCLogDebug("START FLAG: PARSE_ERROR, bail"); return MIME_DEC_ERR_STATE; } /* 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); } if (!g_disable_hashing) { if ((state->state_flag != HEADER_READY && state->state_flag != HEADER_STARTED) || (state->stack->top->data->ctnt_flags & CTNT_IS_BODYPART)) { if (MimeDecGetConfig()->body_md5) { if (state->body_begin == 1 && state->md5_ctx == NULL) { state->md5_ctx = SCMd5New(); } SCMd5Update(state->md5_ctx, buf, len + state->current_line_delimiter_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 (*DataChunkProcessorFunc)(const uint8_t *chunk, uint32_t len, MimeDecParseState *state)) { MimeDecParseState *state; MimeDecEntity *mimeMsg; state = SCMalloc(sizeof(MimeDecParseState)); if (unlikely(state == NULL)) { return NULL; } memset(state, 0x00, sizeof(MimeDecParseState)); state->stack = SCMalloc(sizeof(MimeDecStack)); if (unlikely(state->stack == NULL)) { SCFree(state); return NULL; } memset(state->stack, 0x00, sizeof(MimeDecStack)); mimeMsg = SCMalloc(sizeof(MimeDecEntity)); if (unlikely(mimeMsg == NULL)) { SCFree(state->stack); SCFree(state); 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) { SCFree(state->stack); SCFree(state); return NULL; } state->stack->top->data = mimeMsg; state->state_flag = HEADER_READY; state->data = data; state->DataChunkProcessorFunc = DataChunkProcessorFunc; 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); if (state->md5_ctx) SCMd5Free(state->md5_ctx); 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"); if (state->state_flag == PARSE_ERROR) { SCLogDebug("parser in error state: PARSE_ERROR"); return MIME_DEC_ERR_STATE; } /* 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->md5_ctx) { SCMd5Finalize(state->md5_ctx, state->md5, sizeof(state->md5)); state->md5_ctx = NULL; state->has_md5 = true; } 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 delim_len The length of the line end delimiter * \param state The parser state * * \return MIME_DEC_OK on success, otherwise < 0 on failure */ int MimeDecParseLine(const uint8_t *line, const uint32_t len, const uint8_t delim_len, MimeDecParseState *state) { int ret = MIME_DEC_OK; /* For debugging purposes */ if (len > 0) { PrintChars(SC_LOG_DEBUG, "SMTP LINE", line, len); } else { SCLogDebug("SMTP LINE - EMPTY"); } state->current_line_delimiter_len = delim_len; /* 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 uint8_t *buf, uint32_t blen, void *data, int (*dcpfunc)(const uint8_t *chunk, uint32_t len, MimeDecParseState *state)) { int ret = MIME_DEC_OK; uint8_t *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 = (uint8_t *) buf; uint8_t *line = NULL; do { tok = GetLine(remainPtr, blen - (remainPtr - buf), &remainPtr, &tokLen); if (tok != remainPtr) { line = tok; if ((remainPtr - tok) - tokLen > UINT8_MAX) { SCLogDebug("Error: MimeDecParseLine() overflow: %ld", (remainPtr - tok) - tokLen); ret = MIME_DEC_ERR_OVERFLOW; break; } state->current_line_delimiter_len = (uint8_t)((remainPtr - tok) - tokLen); /* Parse the line */ ret = MimeDecParseLine(line, tokLen, state->current_line_delimiter_len, state); if (ret != MIME_DEC_OK) { SCLogDebug("Error: MimeDecParseLine() function failed: %d", ret); break; } } } while (tok != remainPtr && remainPtr - buf < (int)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) { if ((*line_count) == 0) { (*line_count)++; } PrintChars(SC_LOG_DEBUG, "CHUNK", chunk, len); for (uint32_t i = 0; i < len; i++) { if (chunk[i] == CR || chunk[i] == LF) { if (i + 1 < len && chunk[i] != chunk[i + 1] && (chunk[i + 1] == CR || chunk[i + 1] == LF)) { i++; } (*line_count)++; } } 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) { uint32_t line_count = 0; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "From: Sender1\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 1, 1, state) == MIME_DEC_OK); str = "To: Recipient1\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 1, 1, state) == MIME_DEC_OK); str = "Content-Type: text/plain\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 1, 1, state) == MIME_DEC_OK); str = "\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 1, 1, state) == MIME_DEC_OK); str = "A simple message line 1\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 1, 1, state) == MIME_DEC_OK); str = "A simple message line 2\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 1, 1, state) == MIME_DEC_OK); str = "A simple message line 3\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 1, 1, state) == MIME_DEC_OK); /* Completed */ FAIL_IF_NOT(MimeDecParseComplete(state) == MIME_DEC_OK); MimeDecEntity *msg = state->msg; FAIL_IF_NOT_NULL(msg->next); FAIL_IF_NOT_NULL(msg->child); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); FAIL_IF_NOT(line_count == 3); PASS; } /* Test simple case of EXE URL extraction */ static int MimeDecParseLineTest02(void) { uint32_t line_count = 0; ConfNode *url_schemes = ConfNodeNew(); ConfNode *scheme = ConfNodeNew(); FAIL_IF_NULL(url_schemes); FAIL_IF_NULL(scheme); url_schemes->is_seq = 1; scheme->val = SCStrdup("http://"); FAIL_IF_NULL(scheme->val); TAILQ_INSERT_TAIL(&url_schemes->head, scheme, next); MimeDecGetConfig()->decode_base64 = true; MimeDecGetConfig()->decode_quoted_printable = true; MimeDecGetConfig()->extract_urls = true; MimeDecGetConfig()->extract_urls_schemes = url_schemes; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "From: Sender1\r\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 2, 2, state) == MIME_DEC_OK); str = "To: Recipient1\r\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 2, 2, state) == MIME_DEC_OK); str = "Content-Type: text/plain\r\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 2, 2, state) == MIME_DEC_OK); str = "\r\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 2, 2, state) == MIME_DEC_OK); str = "A simple message line 1\r\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 2, 2, state) == MIME_DEC_OK); str = "A simple message line 2 click on http://www.test.com/malware.exe?" "hahah hopefully you click this link\r\n"; FAIL_IF_NOT(MimeDecParseLine((uint8_t *)str, strlen(str) - 2, 2, state) == MIME_DEC_OK); /* Completed */ FAIL_IF_NOT(MimeDecParseComplete(state) == MIME_DEC_OK); MimeDecEntity *msg = state->msg; FAIL_IF_NULL(msg); FAIL_IF_NULL(msg->url_list); FAIL_IF_NOT((msg->url_list->url_flags & URL_IS_EXE)); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); ConfNodeFree(url_schemes); MimeDecGetConfig()->extract_urls_schemes = NULL; FAIL_IF_NOT(line_count == 2); PASS; } /* Test error case where no url schemes set in config */ static int MimeFindUrlStringsTest01(void) { int ret = MIME_DEC_OK; uint32_t line_count = 0; MimeDecGetConfig()->extract_urls = true; MimeDecGetConfig()->extract_urls_schemes = NULL; MimeDecGetConfig()->log_url_scheme = false; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "test"; ret = FindUrlStrings((uint8_t *)str, strlen(str), state); /* Expected error since extract_url_schemes is NULL */ FAIL_IF_NOT(ret == MIME_DEC_ERR_DATA); /* Completed */ ret = MimeDecParseComplete(state); FAIL_IF_NOT(ret == MIME_DEC_OK); MimeDecEntity *msg = state->msg; MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); PASS; } /* Test simple case of URL extraction */ static int MimeFindUrlStringsTest02(void) { int ret = MIME_DEC_OK; uint32_t line_count = 0; ConfNode *url_schemes = ConfNodeNew(); ConfNode *scheme = ConfNodeNew(); FAIL_IF_NULL(url_schemes); FAIL_IF_NULL(scheme); url_schemes->is_seq = 1; scheme->val = SCStrdup("http://"); FAIL_IF_NULL(scheme->val); TAILQ_INSERT_TAIL(&url_schemes->head, scheme, next); MimeDecGetConfig()->extract_urls = true; MimeDecGetConfig()->extract_urls_schemes = url_schemes; MimeDecGetConfig()->log_url_scheme = false; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "A simple message click on " "http://www.test.com/malware.exe? " "hahah hopefully you click this link"; ret = FindUrlStrings((uint8_t *)str, strlen(str), state); FAIL_IF_NOT(ret == MIME_DEC_OK); /* Completed */ ret = MimeDecParseComplete(state); FAIL_IF_NOT(ret == MIME_DEC_OK); MimeDecEntity *msg = state->msg; FAIL_IF(msg->url_list == NULL); FAIL_IF_NOT(msg->url_list->url_flags & URL_IS_EXE); FAIL_IF_NOT( memcmp("www.test.com/malware.exe?", msg->url_list->url, msg->url_list->url_len) == 0); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); ConfNodeFree(url_schemes); MimeDecGetConfig()->extract_urls_schemes = NULL; PASS; } /* Test URL extraction with multiple schemes and URLs */ static int MimeFindUrlStringsTest03(void) { int ret = MIME_DEC_OK; uint32_t line_count = 0; ConfNode *url_schemes = ConfNodeNew(); ConfNode *scheme1 = ConfNodeNew(); ConfNode *scheme2 = ConfNodeNew(); FAIL_IF_NULL(url_schemes); FAIL_IF_NULL(scheme1); FAIL_IF_NULL(scheme2); url_schemes->is_seq = 1; scheme1->val = SCStrdup("http://"); FAIL_IF_NULL(scheme1->val); TAILQ_INSERT_TAIL(&url_schemes->head, scheme1, next); scheme2->val = SCStrdup("https://"); FAIL_IF_NULL(scheme2->val); TAILQ_INSERT_TAIL(&url_schemes->head, scheme2, next); MimeDecGetConfig()->extract_urls = true; MimeDecGetConfig()->extract_urls_schemes = url_schemes; MimeDecGetConfig()->log_url_scheme = false; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "A simple message click on " "http://www.test.com/malware.exe? " "hahah hopefully you click this link, or " "you can go to http://www.test.com/test/01.html and " "https://www.test.com/test/02.php"; ret = FindUrlStrings((uint8_t *)str, strlen(str), state); FAIL_IF_NOT(ret == MIME_DEC_OK); /* Completed */ ret = MimeDecParseComplete(state); FAIL_IF_NOT(ret == MIME_DEC_OK); MimeDecEntity *msg = state->msg; FAIL_IF(msg->url_list == NULL); MimeDecUrl *url = msg->url_list; FAIL_IF_NOT(memcmp("www.test.com/test/02.php", url->url, url->url_len) == 0); url = url->next; FAIL_IF_NOT(memcmp("www.test.com/test/01.html", url->url, url->url_len) == 0); url = url->next; FAIL_IF_NOT(memcmp("www.test.com/malware.exe?", url->url, url->url_len) == 0); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); ConfNodeFree(url_schemes); MimeDecGetConfig()->extract_urls_schemes = NULL; PASS; } /* Test URL extraction with multiple schemes and URLs with * log_url_scheme enabled in the MIME config */ static int MimeFindUrlStringsTest04(void) { int ret = MIME_DEC_OK; uint32_t line_count = 0; ConfNode *url_schemes = ConfNodeNew(); ConfNode *scheme1 = ConfNodeNew(); ConfNode *scheme2 = ConfNodeNew(); FAIL_IF_NULL(url_schemes); FAIL_IF_NULL(scheme1); FAIL_IF_NULL(scheme2); url_schemes->is_seq = 1; scheme1->val = SCStrdup("http://"); FAIL_IF_NULL(scheme1->val); TAILQ_INSERT_TAIL(&url_schemes->head, scheme1, next); scheme2->val = SCStrdup("https://"); FAIL_IF_NULL(scheme2->val); TAILQ_INSERT_TAIL(&url_schemes->head, scheme2, next); MimeDecGetConfig()->extract_urls = true; MimeDecGetConfig()->extract_urls_schemes = url_schemes; MimeDecGetConfig()->log_url_scheme = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "A simple message click on " "http://www.test.com/malware.exe? " "hahah hopefully you click this link, or " "you can go to http://www.test.com/test/01.html and " "https://www.test.com/test/02.php"; ret = FindUrlStrings((uint8_t *)str, strlen(str), state); FAIL_IF_NOT(ret == MIME_DEC_OK); /* Completed */ ret = MimeDecParseComplete(state); FAIL_IF_NOT(ret == MIME_DEC_OK); MimeDecEntity *msg = state->msg; FAIL_IF(msg->url_list == NULL); MimeDecUrl *url = msg->url_list; FAIL_IF_NOT(memcmp("https://www.test.com/test/02.php", url->url, url->url_len) == 0); url = url->next; FAIL_IF_NOT(memcmp("http://www.test.com/test/01.html", url->url, url->url_len) == 0); url = url->next; FAIL_IF_NOT(memcmp("http://www.test.com/malware.exe?", url->url, url->url_len) == 0); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); ConfNodeFree(url_schemes); MimeDecGetConfig()->extract_urls_schemes = NULL; PASS; } /* Test URL extraction of IPV4 and IPV6 URLs with log_url_scheme * enabled in the MIME config */ static int MimeFindUrlStringsTest05(void) { int ret = MIME_DEC_OK; uint32_t line_count = 0; ConfNode *url_schemes = ConfNodeNew(); ConfNode *scheme = ConfNodeNew(); FAIL_IF_NULL(url_schemes); FAIL_IF_NULL(scheme); url_schemes->is_seq = 1; scheme->val = SCStrdup("http://"); FAIL_IF_NULL(scheme->val); TAILQ_INSERT_TAIL(&url_schemes->head, scheme, next); MimeDecGetConfig()->extract_urls = true; MimeDecGetConfig()->extract_urls_schemes = url_schemes; MimeDecGetConfig()->log_url_scheme = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "A simple message click on " "http://192.168.1.1/test/01.html " "hahah hopefully you click this link or this one " "http://0:0:0:0:0:0:0:0/test/02.php"; ret = FindUrlStrings((uint8_t *)str, strlen(str), state); FAIL_IF_NOT(ret == MIME_DEC_OK); /* Completed */ ret = MimeDecParseComplete(state); FAIL_IF_NOT(ret == MIME_DEC_OK); MimeDecEntity *msg = state->msg; FAIL_IF(msg->url_list == NULL); MimeDecUrl *url = msg->url_list; FAIL_IF_NOT(url->url_flags & URL_IS_IP6); FAIL_IF_NOT(memcmp("http://0:0:0:0:0:0:0:0/test/02.php", url->url, url->url_len) == 0); url = url->next; FAIL_IF_NOT(url->url_flags & URL_IS_IP4); FAIL_IF_NOT(memcmp("http://192.168.1.1/test/01.html", url->url, url->url_len) == 0); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); ConfNodeFree(url_schemes); MimeDecGetConfig()->extract_urls_schemes = NULL; PASS; } /* Test full message with linebreaks */ static int MimeDecParseFullMsgTest01(void) { 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((uint8_t *)msg, strlen(msg), &line_count, TestDataChunkCallback); if (entity == NULL) { SCLogInfo("Warning: Message failed to parse"); return 0; } MimeDecFreeEntity(entity); if (expected_count != line_count) { SCLogInfo("Warning: Line count is invalid: expected - %d actual - %d", expected_count, line_count); return 0; } return 1; } /* Test full message with linebreaks */ static int MimeDecParseFullMsgTest02(void) { uint32_t expected_count = 3; uint32_t line_count = 0; char msg[] = "From: Sender2\r\n" "To: Recipient2\r\n" "Subject: subject2\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((uint8_t *)msg, strlen(msg), &line_count, TestDataChunkCallback); if (entity == NULL) { SCLogInfo("Warning: Message failed to parse"); return 0; } MimeDecField *field = MimeDecFindField(entity, "subject"); if (field == NULL) { SCLogInfo("Warning: Message failed to parse"); return 0; } if (field->value_len != sizeof("subject2") - 1) { SCLogInfo("Warning: failed to get subject"); return 0; } if (memcmp(field->value, "subject2", field->value_len) != 0) { SCLogInfo("Warning: failed to get subject"); return 0; } MimeDecFreeEntity(entity); if (expected_count != line_count) { SCLogInfo("Warning: Line count is invalid: expected - %d actual - %d", expected_count, line_count); return 0; } return 1; } static int MimeBase64DecodeTest01(void) { int ret = 0; uint32_t consumed_bytes = 0, num_decoded = 0; const char *msg = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@" "#$%^&*()-=_+,./;'[]<>?:"; const char *base64msg = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QU" "VJTVFVWV1hZWjEyMzQ1Njc4OTBAIyQlXiYqKCktPV8rLC4vOydbXTw+Pzo="; uint8_t *dst = SCMalloc(strlen(msg) + 1); if (dst == NULL) return 0; ret = DecodeBase64(dst, strlen(msg) + 1, (const uint8_t *)base64msg, strlen(base64msg), &consumed_bytes, &num_decoded, BASE64_MODE_RFC2045); if (memcmp(dst, msg, strlen(msg)) == 0) { ret = 1; } SCFree(dst); return ret; } static int MimeIsExeURLTest01(void) { int ret = 0; const char *url1 = "http://www.google.com/"; const char *url2 = "http://www.google.com/test.exe"; if(IsExeUrl((const uint8_t *)url1, strlen(url1)) != 0){ SCLogDebug("Debug: URL1 error"); goto end; } if(IsExeUrl((const uint8_t *)url2, strlen(url2)) != 1){ SCLogDebug("Debug: URL2 error"); goto end; } ret = 1; end: return ret; } #define TEST(str, len, expect) { \ SCLogDebug("str %s", (str)); \ int r = IsIpv4Host((const uint8_t *)(str),(len)); \ FAIL_IF_NOT(r == (expect)); \ } static int MimeIsIpv4HostTest01(void) { TEST("192.168.1.1", 11, 1); TEST("192.168.1.1.4", 13, 0); TEST("999.168.1.1", 11, 0); TEST("1111.168.1.1", 12, 0); TEST("999.oogle.com", 14, 0); TEST("0:0:0:0:0:0:0:0", 15, 0); TEST("192.168.255.255", 15, 1); TEST("192.168.255.255/testurl.html", 28, 1); TEST("www.google.com", 14, 0); PASS; } #undef TEST #define TEST(str, len, expect) { \ SCLogDebug("str %s", (str)); \ int r = IsIpv6Host((const uint8_t *)(str),(len)); \ FAIL_IF_NOT(r == (expect)); \ } static int MimeIsIpv6HostTest01(void) { TEST("0:0:0:0:0:0:0:0", 19, 1); TEST("0000:0000:0000:0000:0000:0000:0000:0000", 39, 1); TEST("XXXX:0000:0000:0000:0000:0000:0000:0000", 39, 0); TEST("00001:0000:0000:0000:0000:0000:0000:0000", 40, 0); TEST("0:0:0:0:0:0:0:0", 19, 1); TEST("0:0:0:0:0:0:0:0:0", 20, 0); TEST("192:168:1:1:0:0:0:0", 19, 1); TEST("999.oogle.com", 14, 0); TEST("192.168.255.255", 15, 0); TEST("192.168.255.255/testurl.html", 28, 0); TEST("www.google.com", 14, 0); PASS; } #undef TEST static int MimeDecParseLongFilename01(void) { /* contains 276 character filename -- length restricted to 255 chars */ char mimemsg[] = "Content-Disposition: attachment; filename=\"" "12characters12characters12characters12characters" "12characters12characters12characters12characters" "12characters12characters12characters12characters" "12characters12characters12characters12characters" "12characters12characters12characters12characters" "12characters12characters12characters.exe\""; uint32_t line_count = 0; MimeDecGetConfig()->decode_base64 = true; MimeDecGetConfig()->decode_quoted_printable = true; MimeDecGetConfig()->extract_urls = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "From: Sender1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "To: Recipient1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Type: text/plain"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); /* Contains 276 character filename */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)mimemsg, strlen(mimemsg), 1, state)); str = ""; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "A simple message line 1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); /* Completed */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseComplete(state)); MimeDecEntity *msg = state->msg; FAIL_IF_NOT(msg); FAIL_IF_NOT(msg->anomaly_flags & ANOM_LONG_FILENAME); FAIL_IF_NOT(msg->filename_len == RS_MIME_MAX_TOKEN_LEN); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); PASS; } static int MimeDecParseSmallRemInp(void) { // Remainder dA // New input: AAAA char mimemsg[] = "TWltZSBkZWNvZGluZyB pcyBzbyBO T1QgZnV uISBJIGNhbm5vdA"; uint32_t line_count = 0; MimeDecGetConfig()->decode_base64 = true; MimeDecGetConfig()->decode_quoted_printable = true; MimeDecGetConfig()->extract_urls = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); state->stack->top->data->ctnt_flags |= CTNT_IS_ATTACHMENT; const char *str = "From: Sender1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "To: Recipient1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Type: text/plain"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Transfer-Encoding: base64"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = ""; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)mimemsg, strlen(mimemsg), 1, state)); str = "AAAA"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); /* Completed */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseComplete(state)); MimeDecEntity *msg = state->msg; FAIL_IF_NOT(msg); /* filename is not too long */ FAIL_IF(msg->anomaly_flags & ANOM_LONG_FILENAME); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); PASS; } static int MimeDecParseRemSp(void) { // Should have remainder vd A char mimemsg[] = "TWltZSBkZWNvZGluZyBpc yBzbyBOT1QgZnVuISBJIGNhbm5vd A"; uint32_t line_count = 0; MimeDecGetConfig()->decode_base64 = true; MimeDecGetConfig()->decode_quoted_printable = true; MimeDecGetConfig()->extract_urls = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); state->stack->top->data->ctnt_flags |= CTNT_IS_ATTACHMENT; const char *str = "From: Sender1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "To: Recipient1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Type: text/plain"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Transfer-Encoding: base64"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = ""; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)mimemsg, strlen(mimemsg), 1, state)); /* Completed */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseComplete(state)); MimeDecEntity *msg = state->msg; FAIL_IF_NOT(msg); /* filename is not too long */ FAIL_IF(msg->anomaly_flags & ANOM_LONG_FILENAME); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); PASS; } static int MimeDecVerySmallInp(void) { // Remainder: A // New input: aA char mimemsg[] = "TWltZSBkZWNvZGluZyB pcyBzbyBO T1QgZnV uISBJIGNhbm5vA"; uint32_t line_count = 0; MimeDecGetConfig()->decode_base64 = true; MimeDecGetConfig()->decode_quoted_printable = true; MimeDecGetConfig()->extract_urls = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); state->stack->top->data->ctnt_flags |= CTNT_IS_ATTACHMENT; const char *str = "From: Sender1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "To: Recipient1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Type: text/plain"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Transfer-Encoding: base64"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = ""; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)mimemsg, strlen(mimemsg), 1, state)); str = "aA"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); /* Completed */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseComplete(state)); MimeDecEntity *msg = state->msg; FAIL_IF_NOT(msg); /* filename is not too long */ FAIL_IF(msg->anomaly_flags & ANOM_LONG_FILENAME); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); PASS; } static int MimeDecParseOddLen(void) { char mimemsg[] = "TWltZSBkZWNvZGluZyB pcyBzbyBO T1QgZnV uISBJIGNhbm5vdA"; uint32_t line_count = 0; MimeDecGetConfig()->decode_base64 = true; MimeDecGetConfig()->decode_quoted_printable = true; MimeDecGetConfig()->extract_urls = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); state->stack->top->data->ctnt_flags |= CTNT_IS_ATTACHMENT; const char *str = "From: Sender1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "To: Recipient1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Type: text/plain"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Transfer-Encoding: base64"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = ""; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)mimemsg, strlen(mimemsg), 1, state)); /* Completed */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseComplete(state)); MimeDecEntity *msg = state->msg; FAIL_IF_NOT(msg); /* filename is not too long */ FAIL_IF(msg->anomaly_flags & ANOM_LONG_FILENAME); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); PASS; } static int MimeDecParseLongFilename02(void) { /* contains 40 character filename and 500+ characters following filename */ char mimemsg[] = "Content-Disposition: attachment; filename=\"" "12characters12characters12characters.exe\"; " "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd" "somejunkasfdasfsafasafdsasdasassdssdsd"; uint32_t line_count = 0; MimeDecGetConfig()->decode_base64 = true; MimeDecGetConfig()->decode_quoted_printable = true; MimeDecGetConfig()->extract_urls = true; /* Init parser */ MimeDecParseState *state = MimeDecInitParser(&line_count, TestDataChunkCallback); const char *str = "From: Sender1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "To: Recipient1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "Content-Type: text/plain"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); /* Contains 40 character filename */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)mimemsg, strlen(mimemsg), 1, state)); str = ""; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); str = "A simple message line 1"; FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseLine((uint8_t *)str, strlen(str), 1, state)); /* Completed */ FAIL_IF_NOT(MIME_DEC_OK == MimeDecParseComplete(state)); MimeDecEntity *msg = state->msg; FAIL_IF_NOT(msg); /* filename is not too long */ FAIL_IF(msg->anomaly_flags & ANOM_LONG_FILENAME); MimeDecFreeEntity(msg); /* De Init parser */ MimeDecDeInitParser(state); PASS; } #endif /* UNITTESTS */ void MimeDecRegisterTests(void) { #ifdef UNITTESTS UtRegisterTest("MimeDecParseLineTest01", MimeDecParseLineTest01); UtRegisterTest("MimeDecParseLineTest02", MimeDecParseLineTest02); UtRegisterTest("MimeFindUrlStringsTest01", MimeFindUrlStringsTest01); UtRegisterTest("MimeFindUrlStringsTest02", MimeFindUrlStringsTest02); UtRegisterTest("MimeFindUrlStringsTest03", MimeFindUrlStringsTest03); UtRegisterTest("MimeFindUrlStringsTest04", MimeFindUrlStringsTest04); UtRegisterTest("MimeFindUrlStringsTest05", MimeFindUrlStringsTest05); UtRegisterTest("MimeDecParseFullMsgTest01", MimeDecParseFullMsgTest01); UtRegisterTest("MimeDecParseFullMsgTest02", MimeDecParseFullMsgTest02); UtRegisterTest("MimeBase64DecodeTest01", MimeBase64DecodeTest01); UtRegisterTest("MimeIsExeURLTest01", MimeIsExeURLTest01); UtRegisterTest("MimeIsIpv4HostTest01", MimeIsIpv4HostTest01); UtRegisterTest("MimeIsIpv6HostTest01", MimeIsIpv6HostTest01); UtRegisterTest("MimeDecParseLongFilename01", MimeDecParseLongFilename01); UtRegisterTest("MimeDecParseLongFilename02", MimeDecParseLongFilename02); UtRegisterTest("MimeDecParseSmallRemInp", MimeDecParseSmallRemInp); UtRegisterTest("MimeDecParseRemSp", MimeDecParseRemSp); UtRegisterTest("MimeDecVerySmallInp", MimeDecVerySmallInp); UtRegisterTest("MimeDecParseOddLen", MimeDecParseOddLen); #endif /* UNITTESTS */ }