skipfish/src/signatures.c

731 lines
19 KiB
C

/*
skipfish - Signature Matching
----------------------------------------
Author: Niels Heinen <heinenn@google.com>,
Sebastian Roschke <s.roschke@gmail.com>
Copyright 2011, 2012 by Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <stdio.h>
#include "pcre.h"
#define _VIA_SIGNATURE_C
#include "types.h"
#include "config.h"
#include "debug.h"
#include "alloc-inl.h"
#include "database.h"
#include "signatures.h"
struct signature** sig_list;
u32 slist_max_cnt = 0, slist_cnt = 0;
void dump_sig(struct signature *sig);
/* Helper function to lookup a signature keyword. This makes it easier
switch() signature keywords for, for example, storing their values in
a struct */
static s32 lookup(u8* key) {
u32 i;
for (i=0; lookuptable[i].name; i++) {
if (!strcmp((char*)key, lookuptable[i].name))
return lookuptable[i].id;
}
return -1;
}
/* Helper function for parse_sig which takes a string where some
characters might be escaped. It returns a copy with the \'s
removed so \"aa\" becomes "aa" */
static u8* unescape_str(u8* str) {
u32 k = 0, i = 0;
u32 len = strlen((char*)str) + 1;
u8* ret = ck_alloc(len);
for (i=0; i<len; i++) {
if(str[i] == '\\')
continue;
ret[k++] = str[i];
}
return ret;
}
/* Checks the signature and returns 1 if there is an error. This function is
* expected to give warnings that help to resolve the signature error */
static u8 check_signature(struct signature *sig) {
u8 ret = 0;
if (sig->severity < 0 || sig->severity > 4) {
WARN("Signature %d has an invalid severity: %d\n", sig->id, sig->severity);
ret = 1;
}
if (!sig->content_cnt && !sig->mime) {
WARN("Signature %d has no \"content\" nor \"mime\" string\n", sig->id);
ret = 1;
}
if (!sig->memo) {
WARN("Signature %d has no memo string\n", sig->id);
ret = 1;
}
return ret;
}
static u8 compile_content(struct content_struct *content) {
const char* pcre_err;
int pcre_err_offset;
u32 options = PCRE_MULTILINE;
if (content->type != TYPE_REGEX)
return 1;
if (content->nocase) options |= PCRE_CASELESS;
content->pcre_sig = pcre_compile((char*)content->match_str, options,
&pcre_err, &pcre_err_offset, 0);
if (!content->pcre_sig)
return 1;
/* This is extra */
content->pcre_extra_sig = pcre_study(content->pcre_sig, 0, &pcre_err);
return 0;
}
/* Look up a signature. */
static struct signature* get_signature(u32 id) {
u32 i;
for (i=0; i<slist_cnt; i++) {
if (sig_list[i]->id == id)
return sig_list[i];
}
return NULL;
}
/* Parses the signature string that is given as the first parameter and returns
a signature struct */
struct signature* parse_sig(u8* tline) {
u8 *val, *name, *line;
u8 in_quot = 0;
u8 no = 0;
struct content_struct *lcontent = NULL;
line = tline = ck_strdup(tline);
struct signature* sig = ck_alloc(sizeof(struct signature));
while (line) {
/* Skip spaces */
name = line;
no = 0;
while (name && isspace(*name))
name++;
/* Split on the value and, for now, return NULL when there is no
value. We check for keyworks without value, like nocase. */
val = name;
while (*val && val++) {
if (*val == ':') {
*val = 0;
val++;
break;
}
if (*val == ';') break;
}
if(!*val) {
ck_free(sig);
ck_free(tline);
return 0;
}
/* Check if ! is present and set 'not' */
if (*val == '!') {
no = 1;
val++;
}
/* Move to value and check if quoted */
if (*val && (*val == '\'' || *val == '"')) {
in_quot = *val;
val++;
}
/* Find the end of the value string */
line = val;
while (*line && (in_quot || *line != ';') && line++) {
if(*line == '\\') {
line++;
continue;
}
/* End of quotation? */
if (in_quot && *line == in_quot) {
in_quot = 0;
*line = 0;
line++;
continue;
}
}
*line = 0;
switch (lookup(name)) {
case SIG_ID:
sig->id = atoi((char*)val);
break;
case SIG_CHK:
sig->check = atoi((char*)val);
break;
case SIG_CONTENT:
/* If we already have a content struct, try to compile it (when
regex) and create a new content struct */
if (lcontent) {
/* Compile and bail out on failure */
if (lcontent->type == TYPE_REGEX &&
compile_content(lcontent))
FATAL("Unable to compile regex in: %s", tline);
}
if (sig->content_cnt > MAX_CONTENT)
FATAL("Too many content's in line: %s", tline);
sig->content[sig->content_cnt] = ck_alloc(sizeof(struct content_struct));
lcontent = sig->content[sig->content_cnt];
lcontent->match_str = unescape_str(val);
lcontent->match_str_len = strlen((char*)lcontent->match_str);
lcontent->no = no;
sig->content_cnt++;
break;
case SIG_MEMO:
sig->memo = unescape_str(val);
break;
case SIG_PCRE_MATCH:
if (!lcontent) {
WARN("Found 'regex_match' before 'content', skipping..");
break;
}
lcontent->cap_match_str = unescape_str(val);
break;
case SIG_TYPE:
if (!lcontent) {
WARN("Found 'type' before 'content', skipping..");
break;
}
/* 0 is plain to which we default so this only checks if the
rule wants a regex */
if (!strcmp((char*)val, "regex"))
lcontent->type = (u8)TYPE_REGEX;
break;
case SIG_DIST:
if (!lcontent) {
WARN("Found 'distance' before 'content', skipping..");
break;
}
lcontent->distance = atoi((char*)val);
break;
case SIG_OFFSET:
if (!lcontent) {
WARN("Found 'offset' before 'content', skipping..");
break;
}
lcontent->offset = atoi((char*)val);
break;
case SIG_REPORT:
/* TODO(heinenn): support "never" */
if (!strcmp((char*)val, "once")) {
sig->report = REPORT_ONCE;
} else {
sig->report = REPORT_ALWAYS;
}
break;
case SIG_DEPEND:
/* Chain to another signature */
sig->depend = get_signature(atoi((char*)val));
break;
case SIG_SEV:
sig->severity = atoi((char*)val);
break;
case SIG_PROB:
sig->prob = atoi((char*)val);
break;
case SIG_DEPTH:
if (!lcontent) {
WARN("Found 'depth' before 'content', skipping..");
break;
}
lcontent->depth = atoi((char*)val);
break;
case SIG_CASE:
if (!lcontent) {
WARN("Found 'case' before 'content', skipping..");
break;
}
lcontent->nocase = 1;
break;
case SIG_PROTO:
if (!strcmp((char*)val, "https")) {
sig->proto = PROTO_HTTPS;
} else if (!strcmp((char*)val, "http")) {
sig->proto = PROTO_HTTP;
} else {
WARN("Unknown proto specified: %s (skipping)", val);
}
break;
case SIG_MIME:
sig->mime = unescape_str(val);
break;
case SIG_HEADER:
if (sig->header) {
FATAL("Found multiple 'header' keywords, skipping..");
break;
}
sig->header = unescape_str(val);
break;
case SIG_CODE:
sig->rcode = atoi((char*)val);
break;
default:
FATAL("Unknown keyword: %s", name);
}
/* Proceed, or stop when we're at the end of the line. Since 'line'
still points to ; , we'll increase it first */
if(line) line++;
/* Now if we're at EOF or EOL, we'll stop */
if (!line || (*line == '\r' || *line == '\n'))
break;
}
ck_free(tline);
/* Compile the last content entry */
if (lcontent) compile_content(lcontent);
/* Done parsing! Now validate the signature before returning it */
if (check_signature(sig)) {
DEBUG("Skipping signature (didn't validate)\n");
destroy_signature(sig);
return NULL;
}
/* Dump the signature when debugging */
#ifdef LOG_STDERR
dump_sig(sig);
#endif /* !LOG_STDERR */
return sig;
}
/* Loads the signature list from file 'fname' and parses each line. Whenever a
* line starts with #include , the file will be parsed as well to make signature
* management really easy. */
void load_signatures(u8* fname) {
FILE* in;
u8 tmp[MAX_SIG_LEN + 1];
u8 include[MAX_SIG_FNAME + 1];
u32 in_cnt = 0;
u8 fmt[20];
struct signature *sig;
in = fopen((char*)fname, "r");
if (!in) {
PFATAL("Unable to open signature list '%s'", fname);
return;
}
/* Create a signature list */
if (!sig_list)
sig_list = ck_alloc(sizeof(struct signature*) * MAX_SIG_CNT);
u32 sig_off = 0;
s32 tmp_off = 0;
while (fgets((char*)tmp + sig_off, MAX_SIG_LEN - sig_off, in)) {
if (tmp[0] == '#' || tmp[0] == '\n' || tmp[0] == '\r')
continue;
/* We concat signature lines that end with a trailing \ */
tmp_off = strlen((char*)tmp) - 1;
while (tmp_off && isspace(tmp[tmp_off]))
tmp_off--;
if (tmp[tmp_off] == '\\') {
sig_off = tmp_off;
continue;
}
/* When the include directive is present, we'll follow it */
if (!strncmp((char*)tmp, "include ", 8)) {
/* Check the amount of files included already. This is mostly to protect
* against include loops */
if (in_cnt++ > MAX_SIG_INCS)
FATAL("Too many signature includes (max: %d)\n", MAX_SIG_INCS);
sprintf((char*)fmt, "%%%u[^\x01-\x1f]", MAX_SIG_FNAME);
sscanf((char*)tmp + 8,(char*)fmt, (char*)include);
DEBUG("- Including signature file: %s\n", include);
load_signatures(include);
continue;
}
sig = parse_sig(tmp);
sig_off = 0;
if(sig == NULL)
continue;
if (slist_cnt >= MAX_SIG_CNT)
FATAL("* Signature list is too large (max = %d)\n", MAX_SIG_CNT);
sig_list[slist_cnt++] = sig;
}
DEBUG("*- Signatures processed: %s (total sigs %d)\n", fname, slist_cnt);
fclose(in);
}
/* Helper function to check if a certain signature matched. This is,
for example, useful to chain signatures. */
static u8 matched_sig(struct pivot_desc *pv, u32 sid) {
u32 i;
if (!pv->issue_cnt) return 0;
/* Will optimise this later by changing the way signature match
information is stored per pivot */
for (i=0; i<pv->issue_cnt; i++) {
if (pv->issue[i].sid == sid)
return 1;
}
return 0;
}
u8 match_signatures(struct http_request *req, struct http_response *res) {
u8 pcre_ret, matches = 0;
u8 *payload, *match = NULL;
u32 ovector[PCRE_VECTOR];
struct pivot_desc *tpv;
u32 ccnt, pay_len, j = 0, i = 0;
struct content_struct *content = NULL;
for ( j = 0; j < slist_cnt; j++ ) {
/* Check the signature is protocol specific (e.g. SSL-only) */
if (sig_list[j]->proto && (sig_list[j]->proto != req->proto))
continue;
/* Check if the signature is only intended for one of the active tests. */
if (sig_list[j]->check && (req->pivot->check_id > 0 &&
req->pivot->check_id != sig_list[j]->check)) {
continue;
}
/* Compare response code */
if (sig_list[j]->rcode && sig_list[j]->rcode != res->code)
continue;
/* If dependent on another signature, first check if that signature
matches. If it than we're done with this sig */
if (sig_list[j]->depend) {
tpv = req->pivot;
/* If report == 1 then we need to look at the host pivot */
if (sig_list[j]->depend->report == REPORT_ONCE)
tpv = host_pivot(req->pivot);
/* Do the check */
if(!matched_sig(tpv, sig_list[j]->depend->id))
continue;
}
/* Compare the mime types */
if (sig_list[j]->mime && res->header_mime) {
/* Skip if the mime doesn't match */
if (strncmp((char*)res->header_mime, (char*)sig_list[j]->mime,
strlen((char*)sig_list[j]->mime))) continue;
/* We've got a signature match with the mime is the same and no content
* string exists. This is useful for reporting interesting mime types,
* such as application/x-httpd-php-source */
if (!sig_list[j]->content_cnt) {
signature_problem(sig_list[j], req, res);
continue;
}
}
/* Nice, so here the matching will start! Unless... there are not content
* strings, or when the response is mainly binary data. */
if (res->doc_type == 1 || !sig_list[j]->content_cnt)
continue;
/* If this is a header signature, than this is our payload for
matching. Else, we'll take the response body */
if (!sig_list[j]->header) {
payload = res->payload;
pay_len = res->pay_len;
} else {
/* Header is the payload */
payload = GET_HDR(sig_list[j]->header, &res->hdr);
/* A header might very well not be present which means we can
continue with the next signature */
if (!payload) continue;
pay_len = strlen((char*)payload);
}
matches = 0;
for (i=0; pay_len > 0 && i<sig_list[j]->content_cnt; i++) {
content = sig_list[j]->content[i];
/* If there is an offset, we will apply it to the current payload
pointer */
if (content->offset) {
if (pay_len < content->offset) break;
payload = payload + content->offset;
}
/* Use the specified maximum depth to search the string. If no depth
is specified, we search the entire buffer. Note that this is relative
to the beginning of the buffer _or_ the previous content match */
if (content->depth)
pay_len = content->depth;
if (content->distance && pay_len > content->distance) {
payload += content->distance;
pay_len -= content->distance;
}
match = 0;
if (content->type == TYPE_PLAIN) {
if (content->nocase) {
match = inl_findstrcase(payload, content->match_str, pay_len);
} else {
match = inl_findstr(payload, content->match_str, pay_len);
}
if (match && !content->no) {
/* Move the match pointer to allow offset to be applied relative
to the previous content-match */
payload = match + content->match_str_len;
pay_len -= content->match_str_len;
matches++;
} else if(!match && content->no) {
matches++;
} else break;
} else if(content->type == TYPE_REGEX) {
/* Lets do the pcre matching */
pcre_ret = (pcre_exec(content->pcre_sig, content->pcre_extra_sig,
(char*)payload, pay_len, 0, 0, (int*)ovector, PCRE_VECTOR) >= 0);
if (!content->no && pcre_ret) {
/* We care about the first match and update the match pointer
to the first byte that follows the matching string */
/* Check if a string was captured */
pcre_fullinfo(content->pcre_sig, NULL, PCRE_INFO_CAPTURECOUNT, &ccnt);
if (ccnt > 0 && content->cap_match_str) {
/* In pcre we trust.. We only allow one string to be
captured so while we could loop over ccnt: we just grab
the first string. */
u32 cap_size = ovector[3] - ovector[2];
if (cap_size > MAX_PCRE_CSTR_SIZE)
cap_size = MAX_PCRE_CSTR_SIZE;
u8 *pcre_cap_str = ck_alloc(cap_size + 1);
if (pcre_copy_substring((char*)payload, (int*)ovector, 2, 1,
(char*)pcre_cap_str, cap_size)) {
/* No match? break the loop */
if (inl_strcasestr(pcre_cap_str, content->cap_match_str)) {
ck_free(pcre_cap_str);
break;
}
}
ck_free(pcre_cap_str);
}
/* Move to the first byte after the match */
payload = payload + ovector[1];
pay_len -= (ovector[1] - ovector[0]);
/* pay_len is checked in the next match */
matches++;
} else if(!pcre_ret && content->no) {
matches++;
} else break;
}
} /* for i loop */
if (matches == sig_list[j]->content_cnt)
signature_problem(sig_list[j], req, res);
} /* for j loop */
return 0;
}
/* Wrapper for reporting a signature problem */
void signature_problem(struct signature *sig,
struct http_request *req,
struct http_response *res) {
#ifdef _SIGNATURE_TEST
DEBUG("signature_problem() called for %d (%s)\n", sig->id, sig->memo);
#else
/* Register the problem, together with the sid */
register_problem((sig->prob ? sig->prob : sig_serv[sig->severity]),
sig->id, req, res, (sig->memo ? sig->memo : (u8*)""),
sig->report ? host_pivot(req->pivot) : req->pivot, 0);
#endif
}
void destroy_signature(struct signature *sig) {
u32 i;
if (sig->memo) ck_free(sig->memo);
if (sig->mime) ck_free(sig->mime);
if (sig->header) ck_free(sig->header);
for (i=0; i<sig->content_cnt; i++) {
ck_free(sig->content[i]->match_str);
if (sig->content[i]->pcre_sig)
free(sig->content[i]->pcre_sig);
if (sig->content[i]->pcre_extra_sig)
free(sig->content[i]->pcre_extra_sig);
ck_free(sig->content[i]);
}
ck_free(sig);
}
void destroy_signature_lists() {
u32 i;
for (i = 0; i < slist_cnt; i++)
destroy_signature(sig_list[i]);
ck_free(sig_list);
}
/* For debugging: dump a signature */
void dump_sig(struct signature *sig) {
u32 i;
DEBUG("\n=== New signature loaded ===\n");
DEBUG(" id = %d\n", sig->id);
DEBUG(" severity = %d\n", sig->severity);
DEBUG(" content # = %d\n", sig->content_cnt);
for (i=0; i<sig->content_cnt; i++) {
DEBUG(" %d. match_str = %s\n", i, sig->content[i]->match_str);
DEBUG(" %d. type = %s\n", i, sig->content[i]->type ? "REGEX" : "STRING");
DEBUG(" %d. offset = %d\n", i, sig->content[i]->offset);
DEBUG(" %d. depth = %d\n", i, sig->content[i]->depth);
DEBUG(" %d. position = %d\n", i, sig->content[i]->distance);
DEBUG(" %d. nocase = %d\n", i, sig->content[i]->nocase);
DEBUG(" %d. no = %d\n", i, sig->content[i]->no);
}
/* And now the optional fields */
if (sig->memo)
DEBUG(" memo = %s\n", sig->memo);
if (sig->mime)
DEBUG(" mime = %s\n", sig->mime);
if (sig->rcode)
DEBUG(" code = %d\n", sig->rcode);
DEBUG(" depend = %d\n", sig->depend ? sig->depend->id : 0);
DEBUG(" header = %s\n", sig->header ? (char*)sig->header : (char*)"");
switch (sig->proto) {
case '0':
DEBUG(" proto = HTTP/HTTPS\n");
break;
case PROTO_HTTP:
DEBUG(" proto = HTTP\n");
break;
case PROTO_HTTPS:
DEBUG(" proto = HTTPS\n");
}
}