clamav/libclamav/matcher-byte-comp.c

1014 lines
38 KiB
C
Raw Normal View History

/*
* Byte comparison matcher support functions
*
* Copyright (C) 2018-2022 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*
* Authors: Mickey Sola
*
* This program is free software; you can redistribute it and/or modify
* it 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
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif
#include <errno.h>
#include "clamav.h"
#include "others.h"
#include "matcher.h"
#include "matcher-ac.h"
#include "matcher-byte-comp.h"
#include "mpool.h"
#include "readdb.h"
#include "str.h"
/* DEBUGGING */
//#define MATCHER_BCOMP_DEBUG
#ifdef MATCHER_BCOMP_DEBUG
#define bcm_dbgmsg(...) cli_dbgmsg(__VA_ARGS__)
#else
#define bcm_dbgmsg(...)
#endif
#undef MATCHER_BCOMP_DEBUG
/* BCOMP MATCHER FUNCTIONS */
/**
* @brief function to add the byte compare subsig into the matcher root struct
*
* @param root the matcher root struct in question, houses all relevant lsig and subsig info
* @param virname virusname as given by the signature
* @param hexsig the raw sub signature buffer itself which we will be checking/parsing
* @param lsigid the numeric internal reference number which can be used to access this lsig in the root struct
* @param options additional options for pattern matching, stored as a bitmask
*
*/
cl_error_t cli_bcomp_addpatt(struct cli_matcher *root, const char *virname, const char *hexsig, const uint32_t *lsigid, unsigned int options)
{
size_t len = 0;
uint32_t i = 0;
const char *buf_start = NULL;
const char *buf_end = NULL;
char *buf = NULL;
const char *tokens[4];
size_t toks = 0;
int16_t ref_subsigid = -1;
int64_t offset_param = 0;
int64_t ret = CL_SUCCESS;
size_t byte_length = 0;
int64_t comp_val = 0;
char *comp_buf = NULL;
char *comp_start = NULL;
char *comp_end = NULL;
UNUSEDPARAM(options);
if (!hexsig || !(*hexsig) || !root || !virname) {
return CL_ENULLARG;
}
/* we'll be using these to help the root matcher struct keep track of each loaded byte compare pattern */
struct cli_bcomp_meta **newmetatable;
uint32_t bcomp_count = 0;
/* zero out our byte compare data struct and tie it to the root struct's mempool instance */
struct cli_bcomp_meta *bcomp;
bcomp = (struct cli_bcomp_meta *)MPOOL_CALLOC(root->mempool, 1, sizeof(*bcomp));
if (!bcomp) {
cli_errmsg("cli_bcomp_addpatt: Unable to allocate memory for new byte compare meta\n");
return CL_EMEM;
}
/* bring along the standard lsigid vector, first param marks validity of vector, 2nd is lsigid, 3rd is subsigid */
if (lsigid) {
bcomp->lsigid[0] = 1;
bcomp->lsigid[1] = lsigid[0];
bcomp->lsigid[2] = lsigid[1];
} else {
/* sigtool */
bcomp->lsigid[0] = 0;
}
/* first need to grab the subsig reference, we'll use this later to determine our offset */
buf_start = hexsig;
buf_end = hexsig;
ref_subsigid = strtol(buf_start, (char **)&buf_end, 10);
if (buf_end && buf_end[0] != '(') {
cli_errmsg("cli_bcomp_addpatt: while byte compare subsig parsing, reference subsig id was invalid or included non-decimal character\n");
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
2020-01-06 15:45:40 -05:00
if (ref_subsigid > MAX_LDB_SUBSIGS) {
cli_errmsg("cli_bcomp_addpatt: while byte compare subsig parsing, reference subigid exceeded limits on max LDB subsigs\n");
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
bcomp->ref_subsigid = ref_subsigid;
/* use the passed hexsig buffer to find the start and ending parens and store the param length (minus starting paren) */
buf_start = buf_end;
if (buf_start[0] == '(') {
if ((buf_end = strchr(buf_start, ')'))) {
len = (size_t)(buf_end - ++buf_start);
} else {
cli_errmsg("cli_bcomp_addpatt: ending paren not found\n");
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
} else {
cli_errmsg("cli_bcomp_addpatt: opening paren not found\n");
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
/* make a working copy of the param buffer */
buf = CLI_STRNDUP(buf_start, len);
/* break up the new param buffer into its component strings and verify we have exactly 3 */
toks = cli_strtokenize(buf, '#', 3 + 1, tokens);
if (3 != toks) {
cli_errmsg("cli_bcomp_addpatt: %zu (or more) params provided, 3 expected\n", toks);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
tokens[3] = NULL;
/* since null termination is super guaranteed thanks to strndup and cli_strokenize, we can use strtol to grab the
* offset params. this has the added benefit of letting us parse hex values too */
buf_end = NULL;
buf_start = tokens[0];
switch (buf_start[0]) {
case '<':
if ((++buf_start)[0] == '<') {
offset_param = strtol(++buf_start, (char **)&buf_end, 0);
if (buf_end && buf_end + 1 != tokens[1]) {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), offset parameter included invalid characters\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
/* two's-complement for negative value */
offset_param = (~offset_param) + 1;
} else {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), shift operator not valid\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
break;
case '>':
if ((++buf_start)[0] == '>') {
offset_param = strtol(++buf_start, (char **)&buf_end, 0);
if (buf_end && buf_end + 1 != tokens[1]) {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), offset parameter included invalid characters\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
break;
} else {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), shift operator and/or offset not valid\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
case '0':
case '\0':
offset_param = 0;
break;
default:
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), shift operator included invalid characters\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
bcomp->offset = offset_param;
/* the byte length indicator options are stored in a bitmask--by design each option gets its own nibble */
buf_start = tokens[1];
while (!isdigit(*buf_start)) {
switch (*buf_start) {
case 'h':
/* hex, decimal, auto, and binary options are mutually exclusive parameters */
if (bcomp->options & CLI_BCOMP_DEC || bcomp->options & CLI_BCOMP_BIN || bcomp->options & CLI_BCOMP_AUTO) {
ret = CL_EMALFDB;
} else {
bcomp->options |= CLI_BCOMP_HEX;
}
break;
case 'd':
/* hex, decimal, auto, and binary options are mutually exclusive parameters */
/* decimal may not be used with little-endian. big-endian is implied. */
if (bcomp->options & CLI_BCOMP_HEX || bcomp->options & CLI_BCOMP_BIN || bcomp->options & CLI_BCOMP_AUTO || bcomp->options & CLI_BCOMP_LE) {
ret = CL_EMALFDB;
} else {
bcomp->options |= CLI_BCOMP_DEC;
bcomp->options |= CLI_BCOMP_BE;
}
break;
case 'i':
/* hex, decimal, auto, and binary options are mutually exclusive parameters */
if (bcomp->options & CLI_BCOMP_HEX || bcomp->options & CLI_BCOMP_DEC || bcomp->options & CLI_BCOMP_AUTO) {
ret = CL_EMALFDB;
} else {
bcomp->options |= CLI_BCOMP_BIN;
}
break;
case 'a':
/* for automatic hex or decimal run-time detection */
/* hex, decimal, auto, and binary options are mutually exclusive parameters */
if (bcomp->options & CLI_BCOMP_HEX || bcomp->options & CLI_BCOMP_DEC || bcomp->options & CLI_BCOMP_BIN) {
ret = CL_EMALFDB;
} else {
bcomp->options |= CLI_BCOMP_AUTO;
}
break;
case 'l':
/* little and big endian options are mutually exclusive parameters */
/* decimal may not be used with little-endian */
if (bcomp->options & CLI_BCOMP_BE || bcomp->options & CLI_BCOMP_DEC) {
ret = CL_EMALFDB;
} else {
bcomp->options |= CLI_BCOMP_LE;
}
break;
case 'b':
/* little and big endian options are mutually exclusive parameters */
if (bcomp->options & CLI_BCOMP_LE) {
ret = CL_EMALFDB;
} else {
bcomp->options |= CLI_BCOMP_BE;
}
break;
case 'e':
/* for exact byte length matches */
bcomp->options |= CLI_BCOMP_EXACT;
break;
default:
ret = CL_EMALFDB;
break;
}
if (CL_EMALFDB == ret) {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), option parameter was found invalid\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return ret;
}
buf_start++;
}
/* parse out the byte length parameter */
buf_end = NULL;
byte_length = strtol(buf_start, (char **)&buf_end, 0);
if (buf_end && buf_end + 1 != tokens[2]) {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), byte length parameter included invalid characters\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
if (bcomp->options & CLI_BCOMP_BIN && (byte_length > CLI_BCOMP_MAX_BIN_BLEN || CLI_BCOMP_MAX_BIN_BLEN % byte_length)) {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), byte length was either too long or not a valid number of bytes\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
/* same deal with hex byte lengths */
if (bcomp->options & CLI_BCOMP_HEX && (byte_length > CLI_BCOMP_MAX_HEX_BLEN)) {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), byte length was too long\n", tokens[0], tokens[1], tokens[2]);
free(buf);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
bcomp->byte_len = byte_length;
/* we can have up to two comparison eval statements, each sperated by a comma, let's parse them in a separate string */
comp_buf = cli_strdup(tokens[2]);
if (!comp_buf) {
cli_errmsg("cli_bcomp_addpatt: Unable to allocate memory for comparison buffer\n");
cli_bcomp_freemeta(root, bcomp);
return CL_EMEM;
}
/* use different buffer start and end markers so we can keep track of what we need to free later */
buf_start = comp_buf;
comp_start = strchr(comp_buf, ',');
comp_end = strrchr(comp_buf, ',');
/* check to see if we have exactly one comma, then set our count and tokenize our string apropriately */
if (comp_start && comp_end) {
if (comp_end == comp_start) {
comp_start[0] = '\0';
bcomp->comp_count = 2;
} else {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), too many commas found in comparison string\n", tokens[0], tokens[1], tokens[2]);
cli_bcomp_freemeta(root, bcomp);
free(buf);
free((void *)buf_start);
return CL_EPARSE;
}
} else {
comp_start = comp_buf;
bcomp->comp_count = 1;
}
/* allocate comp struct list space with the root structure's mempool instance */
bcomp->comps = (struct cli_bcomp_comp **)MPOOL_CALLOC(root->mempool, bcomp->comp_count, sizeof(struct cli_bcomp_comp *));
if (!bcomp->comps) {
cli_errmsg("cli_bcomp_addpatt: unable to allocate memory for comp struct pointers\n");
free(buf);
free((void *)buf_start);
cli_bcomp_freemeta(root, bcomp);
return CL_EMEM;
}
/* loop through our new list, allocate, and parse out the needed comparison evaluation bits for this subsig */
for (i = 0; i < bcomp->comp_count; i++) {
bcomp->comps[i] = (struct cli_bcomp_comp *)MPOOL_CALLOC(root->mempool, 1, sizeof(struct cli_bcomp_comp));
if (!bcomp->comps[i]) {
cli_errmsg("cli_bcomp_addpatt: unable to allocate memory for comp struct\n");
free(buf);
free((void *)buf_start);
cli_bcomp_freemeta(root, bcomp);
return CL_EMEM;
}
/* currently only >, <, and = are supported comparison symbols--this makes parsing very simple */
switch (*comp_buf) {
case '<':
case '>':
case '=':
bcomp->comps[i]->comp_symbol = *comp_buf;
break;
default:
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), byte comparison symbol was invalid (>, <, = are supported operators) %s\n", tokens[0], tokens[1], tokens[2], comp_buf);
free(buf);
free((void *)buf_start);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
/* grab the comparison value itself */
comp_end = NULL;
comp_buf++;
comp_val = strtoll(comp_buf, (char **)&comp_end, 0);
if (*comp_end) {
cli_errmsg("cli_bcomp_addpatt: while parsing (%s#%s#%s), comparison value contained invalid input\n", tokens[0], tokens[1], tokens[2]);
free(buf);
free((void *)buf_start);
cli_bcomp_freemeta(root, bcomp);
return CL_EMALFDB;
}
bcomp->comps[i]->comp_value = comp_val;
/* a bit of tricksy pointer stuffs which handles all count cases, taking advantage of where strtoll drops endptr */
if (comp_end == comp_start) {
comp_buf = comp_start;
comp_buf++;
}
/* manually verify successful pattern parsing */
bcm_dbgmsg("Matcher Byte Compare: (%s%ld#%c%c%s%zu#%c%ld)\n",
bcomp->offset == 0 ? "" : (bcomp->offset < 0 ? "<<" : ">>"),
bcomp->offset,
bcomp->options & CLI_BCOMP_HEX ? 'h' : (bcomp->options & CLI_BCOMP_DEC ? 'd' : 'i'),
bcomp->options & CLI_BCOMP_LE ? 'l' : 'b',
bcomp->options & CLI_BCOMP_EXACT ? "e" : "",
bcomp->byte_len,
bcomp->comps[i]->comp_symbol,
bcomp->comps[i]->comp_value);
}
free((void *)buf_start);
buf_start = NULL;
/* add byte compare info to the root after reallocation */
bcomp_count = root->bcomp_metas + 1;
/* allocate space for new meta table to store in root structure and increment number of byte compare patterns added */
newmetatable = (struct cli_bcomp_meta **)MPOOL_REALLOC(root->mempool, root->bcomp_metatable, bcomp_count * sizeof(struct cli_bcomp_meta *));
if (!newmetatable) {
cli_errmsg("cli_bcomp_addpatt: Unable to allocate memory for new bcomp meta table\n");
cli_bcomp_freemeta(root, bcomp);
return CL_EMEM;
}
newmetatable[bcomp_count - 1] = bcomp;
root->bcomp_metatable = newmetatable;
root->bcomp_metas = bcomp_count;
/* if everything went well bcomp has been totally populated, which means we can cleanup and exit */
free(buf);
return CL_SUCCESS;
}
/**
* @brief function to perform all byte compare matching on the file buffer
*
* @param map the file map to perform logical byte comparison upon
* @param res the result structure, primarily used by sigtool
* @param root the root structure in which all byte compare lsig and subsig information is stored
* @param mdata the ac data struct which contains offset information from recent subsig matches
* @param ctx the clamav context struct
*
*/
Fix byte-compare subsignature premature alert The byte compare feature in logical signatures will cause the rule to alert if it successfully matches regardless of the rest of the logical signature. An easy way to test this is with a logical signature that has two bcomp subsignatures and requires both to match for the rule to alert. In the following example, we have 4 signatures where - the first will match both bcomp subsigs. - the second will match neither. - the last two match just one bcomp subsig. In an --allmatch test, you'll find that the 3 of these match, with the first one matching *twice*, once for each bcomp subsig. test.ldb: ``` bcomp.both;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=255) bcomp.neither;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=254) bcomp.second;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=255) bcomp.first;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=254) ``` test.sample: ``` AA = 7B; BB = FF ``` You can also try a similar test to compare the behavior with regular ac-pattern-match subsigs with this lsig-test.ldb: ``` pattern.both;Engine:51-255,Target:0;0&1;4141;4242 pattern.neither;Engine:51-255,Target:0;0&1;4140;4241 pattern.second;Engine:51-255,Target:0;0&1;4140;4242 pattern.first;Engine:51-255,Target:0;0&1;4141;4241 ``` This commit fixes the issue by incrementing the logical subsignature count for each bcomp subsig match instead of appending an alert for each bcomp match. Also removed call to `lsig_sub_matched()` that didn't do anything.
2022-02-03 16:06:05 -08:00
cl_error_t cli_bcomp_scanbuf(const unsigned char *buffer, size_t buffer_length, struct cli_ac_result **res, const struct cli_matcher *root, struct cli_ac_data *mdata, cli_ctx *ctx)
{
Fix byte-compare subsignature premature alert The byte compare feature in logical signatures will cause the rule to alert if it successfully matches regardless of the rest of the logical signature. An easy way to test this is with a logical signature that has two bcomp subsignatures and requires both to match for the rule to alert. In the following example, we have 4 signatures where - the first will match both bcomp subsigs. - the second will match neither. - the last two match just one bcomp subsig. In an --allmatch test, you'll find that the 3 of these match, with the first one matching *twice*, once for each bcomp subsig. test.ldb: ``` bcomp.both;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=255) bcomp.neither;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=254) bcomp.second;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=255) bcomp.first;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=254) ``` test.sample: ``` AA = 7B; BB = FF ``` You can also try a similar test to compare the behavior with regular ac-pattern-match subsigs with this lsig-test.ldb: ``` pattern.both;Engine:51-255,Target:0;0&1;4141;4242 pattern.neither;Engine:51-255,Target:0;0&1;4140;4241 pattern.second;Engine:51-255,Target:0;0&1;4140;4242 pattern.first;Engine:51-255,Target:0;0&1;4141;4241 ``` This commit fixes the issue by incrementing the logical subsignature count for each bcomp subsig match instead of appending an alert for each bcomp match. Also removed call to `lsig_sub_matched()` that didn't do anything.
2022-02-03 16:06:05 -08:00
size_t i;
int val = 0;
Fix byte-compare subsignature premature alert The byte compare feature in logical signatures will cause the rule to alert if it successfully matches regardless of the rest of the logical signature. An easy way to test this is with a logical signature that has two bcomp subsignatures and requires both to match for the rule to alert. In the following example, we have 4 signatures where - the first will match both bcomp subsigs. - the second will match neither. - the last two match just one bcomp subsig. In an --allmatch test, you'll find that the 3 of these match, with the first one matching *twice*, once for each bcomp subsig. test.ldb: ``` bcomp.both;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=255) bcomp.neither;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=254) bcomp.second;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=255) bcomp.first;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=254) ``` test.sample: ``` AA = 7B; BB = FF ``` You can also try a similar test to compare the behavior with regular ac-pattern-match subsigs with this lsig-test.ldb: ``` pattern.both;Engine:51-255,Target:0;0&1;4141;4242 pattern.neither;Engine:51-255,Target:0;0&1;4140;4241 pattern.second;Engine:51-255,Target:0;0&1;4140;4242 pattern.first;Engine:51-255,Target:0;0&1;4141;4241 ``` This commit fixes the issue by incrementing the logical subsignature count for each bcomp subsig match instead of appending an alert for each bcomp match. Also removed call to `lsig_sub_matched()` that didn't do anything.
2022-02-03 16:06:05 -08:00
cl_error_t ret = CL_SUCCESS;
cl_error_t bcomp_check = CL_SUCCESS;
uint32_t lsigid, ref_subsigid;
uint32_t offset = 0;
struct cli_bcomp_meta *bcomp = NULL;
struct cli_ac_result *newres = NULL;
uint32_t evalcnt = 0;
uint64_t evalids = 0;
char subsigid[3];
if (!(root) || !(root->bcomp_metas) || !(root->bcomp_metatable) || !(mdata) || !(mdata->offmatrix) || !(ctx)) {
return CL_SUCCESS;
}
for (i = 0; i < root->bcomp_metas; i++) {
bcomp = root->bcomp_metatable[i];
lsigid = bcomp->lsigid[1];
ref_subsigid = bcomp->ref_subsigid;
/* check to see if we are being run in sigtool or not */
if (bcomp->lsigid[0]) {
2020-01-06 15:45:40 -05:00
snprintf(subsigid, 3, "%hu", bcomp->ref_subsigid);
/* verify the ref_subsigid */
val = cli_ac_chklsig(subsigid, subsigid + strlen(subsigid), mdata->lsigcnt[bcomp->lsigid[1]], &evalcnt, &evalids, 0);
if (val != 1) {
bcm_dbgmsg("cli_bcomp_scanbuf: could not verify a match for lsig reference subsigid (%s)\n", subsigid);
continue;
}
/* grab the needed offset using from the last matched subsig offset matrix, i.e. the match performed above */
if (mdata->lsigsuboff_last[lsigid]) {
offset = mdata->lsigsuboff_last[lsigid][ref_subsigid];
} else {
ret = CL_SUCCESS;
continue;
}
} else {
Fix byte-compare subsignature premature alert The byte compare feature in logical signatures will cause the rule to alert if it successfully matches regardless of the rest of the logical signature. An easy way to test this is with a logical signature that has two bcomp subsignatures and requires both to match for the rule to alert. In the following example, we have 4 signatures where - the first will match both bcomp subsigs. - the second will match neither. - the last two match just one bcomp subsig. In an --allmatch test, you'll find that the 3 of these match, with the first one matching *twice*, once for each bcomp subsig. test.ldb: ``` bcomp.both;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=255) bcomp.neither;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=254) bcomp.second;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=255) bcomp.first;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=254) ``` test.sample: ``` AA = 7B; BB = FF ``` You can also try a similar test to compare the behavior with regular ac-pattern-match subsigs with this lsig-test.ldb: ``` pattern.both;Engine:51-255,Target:0;0&1;4141;4242 pattern.neither;Engine:51-255,Target:0;0&1;4140;4241 pattern.second;Engine:51-255,Target:0;0&1;4140;4242 pattern.first;Engine:51-255,Target:0;0&1;4141;4241 ``` This commit fixes the issue by incrementing the logical subsignature count for each bcomp subsig match instead of appending an alert for each bcomp match. Also removed call to `lsig_sub_matched()` that didn't do anything.
2022-02-03 16:06:05 -08:00
/* mdata isn't populated in sigtool so run the raw matcher stuffs */
if (res) {
newres = (struct cli_ac_result *)cli_calloc(1, sizeof(struct cli_ac_result));
if (!newres) {
cli_errmsg("cli_bcomp_scanbuf: can't allocate memory for new result\n");
ret = CL_EMEM;
break;
}
newres->virname = "test";
newres->customdata = NULL;
newres->next = *res;
*res = newres;
}
}
/* no offset available, make a best effort */
if (offset == CLI_OFF_NONE) {
offset = 0;
}
/* now we have all the pieces of the puzzle, so lets do our byte compare check */
Fix byte-compare subsignature premature alert The byte compare feature in logical signatures will cause the rule to alert if it successfully matches regardless of the rest of the logical signature. An easy way to test this is with a logical signature that has two bcomp subsignatures and requires both to match for the rule to alert. In the following example, we have 4 signatures where - the first will match both bcomp subsigs. - the second will match neither. - the last two match just one bcomp subsig. In an --allmatch test, you'll find that the 3 of these match, with the first one matching *twice*, once for each bcomp subsig. test.ldb: ``` bcomp.both;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=255) bcomp.neither;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=254) bcomp.second;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=255) bcomp.first;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=254) ``` test.sample: ``` AA = 7B; BB = FF ``` You can also try a similar test to compare the behavior with regular ac-pattern-match subsigs with this lsig-test.ldb: ``` pattern.both;Engine:51-255,Target:0;0&1;4141;4242 pattern.neither;Engine:51-255,Target:0;0&1;4140;4241 pattern.second;Engine:51-255,Target:0;0&1;4140;4242 pattern.first;Engine:51-255,Target:0;0&1;4141;4241 ``` This commit fixes the issue by incrementing the logical subsignature count for each bcomp subsig match instead of appending an alert for each bcomp match. Also removed call to `lsig_sub_matched()` that didn't do anything.
2022-02-03 16:06:05 -08:00
bcomp_check = cli_bcomp_compare_check(buffer, buffer_length, offset, bcomp);
Fix byte-compare subsignature premature alert The byte compare feature in logical signatures will cause the rule to alert if it successfully matches regardless of the rest of the logical signature. An easy way to test this is with a logical signature that has two bcomp subsignatures and requires both to match for the rule to alert. In the following example, we have 4 signatures where - the first will match both bcomp subsigs. - the second will match neither. - the last two match just one bcomp subsig. In an --allmatch test, you'll find that the 3 of these match, with the first one matching *twice*, once for each bcomp subsig. test.ldb: ``` bcomp.both;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=255) bcomp.neither;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=254) bcomp.second;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=124);4242;2(>>5#hb2#=255) bcomp.first;Engine:51-255,Target:0;0&1&2&3;4141;0(>>5#hb2#=123);4242;2(>>5#hb2#=254) ``` test.sample: ``` AA = 7B; BB = FF ``` You can also try a similar test to compare the behavior with regular ac-pattern-match subsigs with this lsig-test.ldb: ``` pattern.both;Engine:51-255,Target:0;0&1;4141;4242 pattern.neither;Engine:51-255,Target:0;0&1;4140;4241 pattern.second;Engine:51-255,Target:0;0&1;4140;4242 pattern.first;Engine:51-255,Target:0;0&1;4141;4241 ``` This commit fixes the issue by incrementing the logical subsignature count for each bcomp subsig match instead of appending an alert for each bcomp match. Also removed call to `lsig_sub_matched()` that didn't do anything.
2022-02-03 16:06:05 -08:00
/* Increase the lsig count for our subsig if the comparison came back positive.
* Later, the lsig-eval will evaluate the logical condition, based on these counts
* and will append the virus alert if the whole logical signature matches. */
if (CL_VIRUS == bcomp_check) {
/* check to see if we are being run in sigtool or not */
if (bcomp->lsigid[0]) {
mdata->lsigcnt[bcomp->lsigid[1]][bcomp->lsigid[2]]++;
} else {
/* Run by sigtool's --test-sigs feature without context of whole lsig or previous subsigs */
ret = cli_append_virus(ctx, "test");
}
}
}
return ret;
}
/**
* @brief does a numerical, logical byte comparison on a particular offset given a filemapping and the offset
*
* @param map the file buffer we'll be accessing to do our comparison check
* @param offset the offset of the referenced subsig match from the start of the file buffer
* @param bm the byte comparison meta data struct, contains all the other info needed to do the comparison
*
*/
cl_error_t cli_bcomp_compare_check(const unsigned char *f_buffer, size_t buffer_length, int offset, struct cli_bcomp_meta *bm)
{
uint32_t byte_len = 0;
uint32_t pad_len = 0;
uint32_t norm_len = 0;
uint32_t length = 0;
uint32_t i = 0;
2020-12-07 16:09:51 -05:00
cl_error_t ret = CL_CLEAN;
uint16_t opt = 0;
uint16_t opt_val = 0;
int64_t value = 0;
2019-05-03 18:25:17 -04:00
int64_t bin_value = 0;
int16_t compare_check = 0;
unsigned char *end_buf = NULL;
2020-08-04 15:32:24 -04:00
unsigned char *buffer = NULL; /* Used for BE, non-binary comparisons */
unsigned char *tmp_buffer = NULL; /* Used for LE, non-binary comparisons */
if (!f_buffer || !bm) {
bcm_dbgmsg("cli_bcomp_compare_check: a param is null\n");
2020-12-07 16:09:51 -05:00
ret = CL_ENULLARG;
goto done;
}
byte_len = bm->byte_len;
length = buffer_length;
opt = bm->options;
/* ensure we won't run off the end of the file buffer */
if (!(offset + bm->offset + byte_len <= length)) {
bcm_dbgmsg("cli_bcomp_compare_check: %u bytes requested at offset %zu would go past file buffer of %u\n", byte_len, (offset + bm->offset), length);
2020-12-07 16:09:51 -05:00
goto done;
}
if (!(offset + bm->offset > 0)) {
bcm_dbgmsg("cli_bcomp_compare_check: negative offset would underflow buffer\n");
2020-12-07 16:09:51 -05:00
goto done;
}
/* jump to byte compare offset, then store off specified bytes into a null terminated buffer */
offset += bm->offset;
f_buffer += offset;
bcm_dbgmsg("cli_bcomp_compare_check: literal extracted bytes before comparison %.*s\n", byte_len, f_buffer);
/* normalize buffer for whitespace */
opt_val = opt & 0x000F;
if (!(opt_val & CLI_BCOMP_BIN)) {
buffer = cli_bcomp_normalize_buffer(f_buffer, byte_len, &pad_len, opt, 1);
if (NULL == buffer) {
cli_errmsg("cli_bcomp_compare_check: unable to whitespace normalize temp buffer, allocation failed\n");
2020-12-07 16:09:51 -05:00
ret = CL_EMEM;
goto done;
}
/* adjust byte_len accordingly */
byte_len -= pad_len;
}
/* normalize buffer for little endian vals */
opt_val = opt & 0x00F0;
if (opt_val == CLI_BCOMP_LE) {
opt_val = opt & 0x000F;
if (!(opt_val & CLI_BCOMP_BIN)) {
tmp_buffer = cli_bcomp_normalize_buffer(buffer, byte_len, NULL, opt, 0);
if (NULL == tmp_buffer) {
cli_errmsg("cli_bcomp_compare_check: unable to normalize temp, allocation failed\n");
2020-12-07 16:09:51 -05:00
ret = CL_EMEM;
goto done;
}
}
}
opt_val = opt;
if (opt_val & CLI_BCOMP_AUTO) {
opt = cli_bcomp_chk_hex(buffer, opt_val, byte_len, 0);
}
/* grab the first byte to handle byte length options to convert the string appropriately */
switch (opt & 0x00FF) {
/*hl*/
case CLI_BCOMP_HEX | CLI_BCOMP_LE:
if (byte_len != 1) {
norm_len = (byte_len % 2) == 0 ? byte_len : byte_len + 1;
} else {
norm_len = 1;
}
errno = 0;
value = cli_strntol((char *)tmp_buffer, norm_len, (char **)&end_buf, 16);
if ((((value == LONG_MAX) || (value == LONG_MIN)) && errno == ERANGE) || NULL == end_buf) {
bcm_dbgmsg("cli_bcomp_compare_check: little endian hex conversion unsuccessful\n");
2020-12-07 16:09:51 -05:00
goto done;
}
/*hle*/
if (opt & CLI_BCOMP_EXACT) {
if (tmp_buffer + byte_len != end_buf || pad_len != 0) {
bcm_dbgmsg("cli_bcomp_compare_check: couldn't extract the exact number of requested bytes\n");
2020-12-07 16:09:51 -05:00
goto done;
}
}
break;
/*hb*/
case CLI_BCOMP_HEX | CLI_BCOMP_BE:
value = cli_strntol((char *)buffer, byte_len, (char **)&end_buf, 16);
if ((((value == LONG_MAX) || (value == LONG_MIN)) && errno == ERANGE) || NULL == end_buf) {
bcm_dbgmsg("cli_bcomp_compare_check: big endian hex conversion unsuccessful\n");
2020-12-07 16:09:51 -05:00
goto done;
}
/*hbe*/
if (opt & CLI_BCOMP_EXACT) {
if (buffer + byte_len != end_buf || pad_len != 0) {
bcm_dbgmsg("cli_bcomp_compare_check: couldn't extract the exact number of requested bytes\n");
2020-12-07 16:09:51 -05:00
goto done;
}
}
break;
/*dl*/
case CLI_BCOMP_DEC | CLI_BCOMP_LE:
/* it may be possible for the auto option to proc this */
bcm_dbgmsg("cli_bcomp_compare_check: auto detection found ascii decimal for specified little endian byte extraction, which is unsupported\n");
2020-12-07 16:09:51 -05:00
goto done;
break;
/*db*/
case CLI_BCOMP_DEC | CLI_BCOMP_BE:
value = cli_strntol((char *)buffer, byte_len, (char **)&end_buf, 10);
if ((((value == LONG_MAX) || (value == LONG_MIN)) && errno == ERANGE) || NULL == end_buf) {
bcm_dbgmsg("cli_bcomp_compare_check: big endian decimal conversion unsuccessful\n");
2020-12-07 16:09:51 -05:00
goto done;
}
/*dbe*/
if (opt & CLI_BCOMP_EXACT) {
if (buffer + byte_len != end_buf || pad_len != 0) {
bcm_dbgmsg("cli_bcomp_compare_check: couldn't extract the exact number of requested bytes\n");
2020-12-07 16:09:51 -05:00
goto done;
}
}
break;
/*il*/
case CLI_BCOMP_BIN | CLI_BCOMP_LE:
/* exact byte_length option is implied for binary extraction */
switch (byte_len) {
case 1:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)(*(uint8_t *)f_buffer);
break;
case 2:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)le16_to_host(*(uint16_t *)f_buffer);
break;
case 4:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)le32_to_host(*(uint32_t *)f_buffer);
break;
case 8:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)le64_to_host(*(uint64_t *)f_buffer);
break;
default:
bcm_dbgmsg("cli_bcomp_compare_check: invalid byte size for binary integer field (%u)\n", byte_len);
2020-12-07 16:09:51 -05:00
ret = CL_EARG;
goto done;
}
break;
/*ib*/
case CLI_BCOMP_BIN | CLI_BCOMP_BE:
/* exact byte_length option is implied for binary extraction */
switch (byte_len) {
case 1:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)(*(uint8_t *)f_buffer);
break;
case 2:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)be16_to_host(*(uint16_t *)f_buffer);
break;
case 4:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)be32_to_host(*(uint32_t *)f_buffer);
break;
case 8:
2019-05-03 18:25:17 -04:00
bin_value = (int64_t)be64_to_host(*(uint64_t *)f_buffer);
break;
default:
bcm_dbgmsg("cli_bcomp_compare_check: invalid byte size for binary integer field (%u)\n", byte_len);
2020-12-07 16:09:51 -05:00
ret = CL_EARG;
goto done;
}
break;
default:
bcm_dbgmsg("cli_bcomp_compare_check: options were found invalid\n");
2020-12-07 16:09:51 -05:00
ret = CL_ENULLARG;
goto done;
}
/* do the actual comparison */
for (i = 0; i < bm->comp_count; i++) {
if (bm->comps && bm->comps[i]) {
switch (bm->comps[i]->comp_symbol) {
case '>':
if (opt & CLI_BCOMP_BIN) {
compare_check = (bin_value > bm->comps[i]->comp_value);
} else {
compare_check = (value > bm->comps[i]->comp_value);
}
if (compare_check) {
bcm_dbgmsg("cli_bcomp_compare_check: extracted value (%ld) greater than comparison value (%ld)\n", (opt & CLI_BCOMP_BIN) ? bin_value : value, bm->comps[i]->comp_value);
ret = CL_VIRUS;
} else {
ret = CL_CLEAN;
}
break;
case '<':
if (opt & CLI_BCOMP_BIN) {
compare_check = (bin_value < bm->comps[i]->comp_value);
} else {
compare_check = (value < bm->comps[i]->comp_value);
}
if (compare_check) {
bcm_dbgmsg("cli_bcomp_compare_check: extracted value (%ld) less than comparison value (%ld)\n", (opt & CLI_BCOMP_BIN) ? bin_value : value, bm->comps[i]->comp_value);
ret = CL_VIRUS;
} else {
ret = CL_CLEAN;
}
break;
case '=':
if (opt & CLI_BCOMP_BIN) {
compare_check = (bin_value == bm->comps[i]->comp_value);
} else {
compare_check = (value == bm->comps[i]->comp_value);
}
if (compare_check) {
bcm_dbgmsg("cli_bcomp_compare_check: extracted value (%ld) equal to comparison value (%ld)\n", (opt & CLI_BCOMP_BIN) ? bin_value : value, bm->comps[i]->comp_value);
ret = CL_VIRUS;
} else {
ret = CL_CLEAN;
}
break;
default:
bcm_dbgmsg("cli_bcomp_compare_check: comparison symbol (%c) invalid\n", bm->comps[i]->comp_symbol);
2020-12-07 16:09:51 -05:00
ret = CL_ENULLARG;
goto done;
}
if (CL_CLEAN == ret) {
/* comparison was not successful */
bcm_dbgmsg("cli_bcomp_compare_check: extracted value (%ld) was not %c %ld\n", (opt & CLI_BCOMP_BIN) ? bin_value : value, bm->comps[i]->comp_symbol, bm->comps[i]->comp_value);
2020-12-07 16:09:51 -05:00
goto done;
}
}
}
2020-12-07 16:09:51 -05:00
done:
if (NULL != tmp_buffer) {
free(tmp_buffer);
}
if (NULL != buffer) {
free(buffer);
}
return ret;
}
/**
* @brief checks to see if an ascii buffer should be considered hex or not
*
* @param buffer is the buffer to evaluate
* @param opts the bcomp opts bitfield to set/evaluate during the check
* @param len the length of the buffer, must be larger than 3 bytes
* @param check_only specifies whether to return true/false or the modified opt value
*
* @return if check only is set, it will return true or false, otherwise it returns a modifiied byte compare bitfield
*/
uint16_t cli_bcomp_chk_hex(const unsigned char *buffer, uint16_t opt, uint32_t len, uint32_t check_only)
{
uint16_t check = 0;
if (!buffer || len < 3) {
if (buffer && len < 3) {
if ((opt & 0x00F0) & CLI_BCOMP_AUTO) {
opt |= CLI_BCOMP_DEC;
opt ^= CLI_BCOMP_AUTO;
}
}
return check_only ? check : opt;
}
if (!strncmp((char *)buffer, "0x", 2) || !strncmp((char *)buffer, "0X", 2)) {
opt |= CLI_BCOMP_HEX;
check = 1;
} else {
opt |= CLI_BCOMP_DEC;
check = 0;
}
opt ^= CLI_BCOMP_AUTO;
return check_only ? check : opt;
}
/**
* @brief multipurpose buffer normalization support function for bytcompare
*
* Currently can be used to normalize a little endian hex buffer to big endian.
* Can also be used to trim whitespace from the front of the buffer.
*
* @param buffer is the ascii bytes which are to be normalized
* @param byte_len is the length of these bytes
* @param pad_len if the address passed is non-null function will store the amount of whitespace found in bytes
* @param opt the byte compare option bitfield
* @param whitespace_only if true will only do whitespace normalization, will not perform whitespace
* normalization if set to no
*
* @return returns an allocated, normalized buffer or NULL if an allocation error has occurred
*/
unsigned char *cli_bcomp_normalize_buffer(const unsigned char *buffer, uint32_t byte_len, uint32_t *pad_len, uint16_t opt, uint16_t whitespace_only)
{
uint32_t norm_len = 0;
uint32_t pad = 0;
uint32_t i = 0;
uint16_t opt_val = 0;
unsigned char *tmp_buffer = NULL;
if (!buffer) {
cli_errmsg("cli_bcomp_compare_check: unable to normalize temp buffer, params null\n");
return NULL;
}
if (whitespace_only) {
for (i = 0; i < byte_len; i++) {
if (isspace(buffer[i])) {
bcm_dbgmsg("cli_bcomp_compare_check: buffer has whitespace \n");
pad++;
} else {
/* break on first non-padding whitespace */
break;
}
}
/* keep in mind byte_len is a stack variable so this won't change byte_len in our calling functioning */
byte_len = byte_len - pad;
tmp_buffer = cli_calloc(byte_len + 1, sizeof(char));
if (NULL == tmp_buffer) {
cli_errmsg("cli_bcomp_compare_check: unable to allocate memory for whitespace normalized temp buffer\n");
return NULL;
}
memset(tmp_buffer, '0', byte_len + 1);
memcpy(tmp_buffer, buffer + pad, byte_len);
tmp_buffer[byte_len] = '\0';
if (pad_len) {
*pad_len = pad;
}
return tmp_buffer;
}
opt_val = opt & 0x000F;
if (opt_val & CLI_BCOMP_HEX || opt_val & CLI_BCOMP_AUTO) {
2020-08-04 15:32:24 -04:00
unsigned char *hex_buffer;
norm_len = (byte_len % 2) == 0 ? byte_len : byte_len + 1;
tmp_buffer = cli_calloc(norm_len + 1, sizeof(char));
if (NULL == tmp_buffer) {
cli_errmsg("cli_bcomp_compare_check: unable to allocate memory for normalized temp buffer\n");
return NULL;
}
hex_buffer = cli_calloc(norm_len + 1, sizeof(char));
if (NULL == hex_buffer) {
free(tmp_buffer);
cli_errmsg("cli_bcomp_compare_check: unable to reallocate memory for hex buffer\n");
return NULL;
}
memset(tmp_buffer, '0', norm_len + 1);
memset(hex_buffer, '0', norm_len + 1);
if (byte_len == 1) {
tmp_buffer[0] = buffer[0];
} else {
if (norm_len == byte_len + 1) {
opt_val = opt;
if (cli_bcomp_chk_hex(buffer, opt_val, byte_len, 1)) {
memcpy(hex_buffer + 3, buffer + 2, byte_len - 2);
hex_buffer[0] = 'x';
} else {
memcpy(hex_buffer + 1, buffer, byte_len);
}
} else {
opt_val = opt;
memcpy(hex_buffer, buffer, byte_len);
if (cli_bcomp_chk_hex(buffer, opt_val, byte_len, 1)) {
hex_buffer[0] = 'x';
}
}
for (i = 0; i < norm_len; i = i + 2) {
if (((int32_t)norm_len - (int32_t)i) - 2 >= 0) {
/* 0000BA -> B0000A */
if (isxdigit(hex_buffer[norm_len - i - 2]) || toupper(hex_buffer[norm_len - i - 2]) == 'X') {
tmp_buffer[i] = hex_buffer[norm_len - i - 2];
} else {
/* non-hex detected, our current buffer is invalid so zero it out and continue */
memset(tmp_buffer, '0', norm_len + 1);
/* nibbles after this are non-good, so skip them */
continue;
}
}
/* 0000BA -> 0A00B0 */
if (isxdigit(hex_buffer[norm_len - i - 1]) || toupper(hex_buffer[norm_len - i - 1]) == 'X') {
tmp_buffer[i + 1] = hex_buffer[norm_len - i - 1];
} else {
/* non-hex detected, our current buffer is invalid so zero it out and continue */
memset(tmp_buffer, '0', norm_len + 1);
}
}
}
tmp_buffer[norm_len] = '\0';
bcm_dbgmsg("cli_bcomp_compare_check: normalized extracted bytes before comparison %.*s\n", norm_len, tmp_buffer);
2020-08-04 15:32:24 -04:00
free(hex_buffer);
}
return tmp_buffer;
}
/**
* @brief cleans up the byte compare data struct
*
* @param root the root matcher struct whose mempool instance the bcomp struct has been allocated with
* @param bm the bcomp struct to be freed
*
*/
void cli_bcomp_freemeta(struct cli_matcher *root, struct cli_bcomp_meta *bm)
{
uint32_t i = 0;
if (!root || !bm) {
return;
}
if (bm->comps) {
for (i = 0; i < bm->comp_count; i++) {
if (bm->comps[i]) {
MPOOL_FREE(root->mempool, bm->comps[i]);
bm->comps[i] = NULL;
}
}
MPOOL_FREE(root->mempool, bm->comps);
bm->comps = NULL;
}
MPOOL_FREE(root->mempool, bm);
bm = NULL;
return;
}