diff options
-rw-r--r-- | docbook/release-notes.asciidoc | 1 | ||||
-rw-r--r-- | epan/dissectors/CMakeLists.txt | 2 | ||||
-rw-r--r-- | epan/dissectors/Makefile.am | 2 | ||||
-rw-r--r-- | epan/dissectors/packet-snort.c | 1384 | ||||
-rw-r--r-- | epan/dissectors/snort-config.c | 1103 | ||||
-rw-r--r-- | epan/dissectors/snort-config.h | 194 |
6 files changed, 2686 insertions, 0 deletions
diff --git a/docbook/release-notes.asciidoc b/docbook/release-notes.asciidoc index 5837bd56b6..1bf32ac781 100644 --- a/docbook/release-notes.asciidoc +++ b/docbook/release-notes.asciidoc @@ -64,6 +64,7 @@ Health Level 7 (HL7) Local Service Discovery (LSD) Fc00/cjdns Protocol iPerf2 +Snort Post-dissector --sort-and-group-- diff --git a/epan/dissectors/CMakeLists.txt b/epan/dissectors/CMakeLists.txt index aebdea0279..23ff9f4db3 100644 --- a/epan/dissectors/CMakeLists.txt +++ b/epan/dissectors/CMakeLists.txt @@ -1222,6 +1222,7 @@ set(DISSECTOR_SRC packet-snaeth.c packet-sndcp-xid.c packet-sndcp.c + packet-snort.c packet-socketcan.c packet-socks.c packet-soupbintcp.c @@ -1422,6 +1423,7 @@ set(DISSECTOR_SRC set(DISSECTOR_SUPPORT_SRC packet-dcerpc-nt.c usb.c + snort-config.c register.c ) source_group(dissector-support FILES ${DISSECTOR_SUPPORT_SRC}) diff --git a/epan/dissectors/Makefile.am b/epan/dissectors/Makefile.am index 21f800dd53..2d6d83241e 100644 --- a/epan/dissectors/Makefile.am +++ b/epan/dissectors/Makefile.am @@ -1243,6 +1243,7 @@ DISSECTOR_SRC = \ packet-snaeth.c \ packet-sndcp-xid.c \ packet-sndcp.c \ + packet-snort.c \ packet-socketcan.c \ packet-socks.c \ packet-soupbintcp.c \ @@ -1826,6 +1827,7 @@ DISSECTOR_INCLUDES = \ # used to generate "register.c"). DISSECTOR_SUPPORT_SRC = \ packet-dcerpc-nt.c \ + snort-config.c \ usb.c \ register.c diff --git a/epan/dissectors/packet-snort.c b/epan/dissectors/packet-snort.c new file mode 100644 index 0000000000..4bed149eda --- /dev/null +++ b/epan/dissectors/packet-snort.c @@ -0,0 +1,1384 @@ +/* packet-snort.c + * + * Copyright 2011, Jakub Zawadzki <darkjames-ws@darkjames.pl> + * Copyright 2016, Martin Mathieson + * + * Google Summer of Code 2011 for The Honeynet Project + * Mentors: + * Guillaume Arcas <guillaume.arcas (at) retiaire.org> + * Jeff Nathan <jeffnathan (at) gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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. + */ + + +/* TODO: + * - sort out threading/channel-sync so works reliably in tshark + * - postponed for now, as Qt crashes if call g_main_context_iteration() + * at an inopportune time + * - would be good if could set [Snort Running] in the title bar while Snort is running, + * but don't see how a dissector could do that. + * - for a content match, find all protocol fields that cover same bytes and show in tree + * - after tcp.reassembled_in fixed, offer to move alert to that frame? + * - other use-cases as suggested in https://sharkfesteurope.wireshark.org/assets/presentations16eu/14.pptx + */ + + +#include "config.h" + +#include <errno.h> +#include <ctype.h> + +#include <epan/epan.h> +#include <epan/proto.h> +#include <epan/packet.h> +#include <epan/prefs.h> +#include <epan/expert.h> +#include <wsutil/report_err.h> +#include <epan/wmem/wmem.h> +#include <wiretap/wtap-int.h> + +#include "snort-config.h" + +/* Forward declarations */ +void proto_register_snort(void); +void proto_reg_handoff_snort(void); + + +static int proto_snort = -1; + +/* These are from parsing snort fast_alert output and/or looking up snort config */ +static int hf_snort_raw_alert = -1; +static int hf_snort_classification = -1; +static int hf_snort_rule = -1; +static int hf_snort_msg = -1; +static int hf_snort_rev = -1; +static int hf_snort_sid = -1; +static int hf_snort_generator = -1; +static int hf_snort_priority = -1; +static int hf_snort_rule_string = -1; +static int hf_snort_rule_protocol = -1; +static int hf_snort_rule_filename = -1; +static int hf_snort_rule_line_number = -1; +static int hf_snort_rule_ip_var = -1; +static int hf_snort_rule_port_var = -1; + +/* Patterns to match */ +static int hf_snort_content = -1; +static int hf_snort_uricontent = -1; +static int hf_snort_pcre = -1; + +/* Web links */ +static int hf_snort_reference = -1; + +/* General stats about the rule set */ +static int hf_snort_global_stats = -1; +static int hf_snort_global_stats_rule_file_count = -1; /* number of rules files */ +static int hf_snort_global_stats_rule_count = -1; /* number of rules in config */ + +static int hf_snort_global_stats_total_alerts_count = -1; +static int hf_snort_global_stats_alert_match_number = -1; + +static int hf_snort_global_stats_rule_alerts_count = -1; +static int hf_snort_global_stats_rule_match_number = -1; + + +/* Subtrees */ +static int ett_snort = -1; +static int ett_snort_rule = -1; +static int ett_snort_global_stats = -1; + +/* Expert info */ +static expert_field ei_snort_alert = EI_INIT; +static expert_field ei_snort_content_not_matched = EI_INIT; + + +/*****************************************/ +/* Preferences */ + +/* Use explicit preference as want to disable this dissector by default */ +static gboolean snort_enable_dissector = FALSE; + +/* Where to look for alerts. */ +enum alerts_source { + FromRunningSnort, + FromUserComments /* see https://blog.packet-foo.com/2015/08/verifying-iocs-with-snort-and-tracewrangler/ */ +}; +/* By default schoose to run Snort to look for alerts */ +static gint pref_snort_alerts_source = (gint)FromRunningSnort; + +/* Snort binary and config file */ +#ifndef _WIN32 +static const char *pref_snort_binary_filename = "/usr/sbin/snort"; +static const char *pref_snort_config_filename = "/etc/snort/snort.conf"; +#else +/* Default locations from Snort Windows installer */ +static const char *pref_snort_binary_filename = "C:\\Snort\\bin\\snort.exe"; +static const char *pref_snort_config_filename = "C:\\Snort\\etc\\snort.conf"; +#endif + +/* Should rule stats be shown in protocol tree? */ +static gboolean snort_show_rule_stats = FALSE; + +/* Should alerts be added as expert info? */ +static gboolean snort_show_alert_expert_info = FALSE; + +#if 0 +/* Should we try to attach the alert to the tcp.reassembled_in frame instead of current one? */ +static gboolean snort_alert_in_reassembled_frame = FALSE; +#endif + + + +/********************************************************/ +/* Global variable with single parsed snort config */ +static SnortConfig_t *g_snort_config = NULL; + + +/******************************************************/ +/* This is to keep track of the running Snort process */ +typedef struct { + gboolean running; + gboolean working; + + GPid pid; + int in, out, err; /* fds for talking to snort process */ + + GString *buf; /* Incomplete alert output that has been read */ + wtap_dumper *pdh; /* wiretap dumper used to deliver packets to 'in' */ + + GIOChannel *channel; /* IO channel used for readimg stdout (alerts) */ + + wmem_tree_t *alerts_tree; /* Lookup from frame-number -> Alerts_t* */ +} snort_session_t; + +/* Global instance of the snort session */ +static snort_session_t current_session; + +static int snort_config_ok = TRUE; /* N.B. Not running test at the moment... */ + + + +/*************************************************/ +/* An alert. + Created by parsing alert from snort, hopefully with more details linked from matched_rule. */ +typedef struct Alert_t { + /* Time */ + struct timeval tv; + /* Rule */ + guint32 sid; /* Rule identifier */ + guint32 rev; /* Revision number of rule */ + guint32 gen; /* Which engine generated alert (not often interesting) */ + int prio; /* Priority as reported in alert (not usually interesting) */ + + const char *raw_alert; /* The whole alert string as reported by snort */ + + char *msg; /* Rule msg/description as it appears in the alert */ + char *classification; /* Classification type of rule */ + + Rule_t *matched_rule; /* Link to corresponding rule from snort config */ + + /* Stats for this alert among the capture file. */ + unsigned int overall_match_number; + unsigned int rule_match_number; +} Alert_t; + +/* Can have multiple alerts fire on same frame, so define this container */ +typedef struct Alerts_t { +/* N.B. Snort limit appears to be 6 (at least with default config..) */ +#define MAX_ALERTS_PER_FRAME 8 + Alert_t alerts[MAX_ALERTS_PER_FRAME]; + guint num_alerts; +} Alerts_t; + + +/* Add an alert to the map stored in current_session */ +static void add_alert_to_session_tree(guint frame_number, Alert_t *alert) +{ + /* First look up tree to see if there is an existing entry */ + Alerts_t *alerts = (Alerts_t*)wmem_tree_lookup32(current_session.alerts_tree, frame_number); + if (alerts == NULL) { + /* Create a new entry for the table */ + alerts = (Alerts_t*)g_malloc(sizeof(Alerts_t)); + alerts->alerts[0] = *alert; + alerts->num_alerts = 1; + wmem_tree_insert32(current_session.alerts_tree, frame_number, alerts); + } + else { + /* See if there is room in the existing Alerts_t struct for this frame */ + if (alerts->num_alerts < MAX_ALERTS_PER_FRAME) { + alerts->alerts[alerts->num_alerts++] = *alert; + } + } +} + + +/******************************************************************/ + +/* Given an alert struct, look up by Snort ID (sid) and try to fill in other details to display. */ +static void fill_alert_config(SnortConfig_t *snort_config, Alert_t *alert) +{ + guint global_match_number=0, rule_match_number=0; + + /* Look up rule by sid */ + alert->matched_rule = get_rule(snort_config, alert->sid); + + /* Classtype usually filled in from alert rather than rule, but missing for supsported + comment format. */ + if (pref_snort_alerts_source == FromUserComments) { + alert->classification = g_strdup(alert->matched_rule->classtype); + } + + /* Inform the config/rule about the alert */ + rule_set_alert(snort_config, alert->matched_rule, + &global_match_number, &rule_match_number); + /* Copy updated counts into the alert */ + alert->overall_match_number = global_match_number; + alert->rule_match_number = rule_match_number; +} + + +/* Helper functions for matching expected bytes against the packet buffer. + Case-sensitive comparison - can just memcmp(). + Case-insensitive comparison - need to look at each byte and compare uppercase version */ +static gboolean content_compare_case_sensitive(const guint8* memory, const char* target, guint length) +{ + return (memcmp(memory, target, length) == 0); +} + +static gboolean content_compare_case_insensitive(const guint8* memory, const char* target, guint length) +{ + for (guint n=0; n < length; n++) { + if (g_ascii_isalpha(target[n])) { + if (g_ascii_toupper(memory[n]) != g_ascii_toupper(target[n])) { + return FALSE; + } + } + else { + if ((guint8)memory[n] != (guint8)target[n]) { + return FALSE; + } + } + } + + return TRUE; +} + + +/* Move through the bytes of the tvbuff, looking for a match against the expanded + binary contents of this content object. + */ +static gboolean look_for_content(content_t *content, tvbuff_t *tvb, guint start_offset, guint *match_offset, guint *match_length) +{ + gint tvb_len = tvb_captured_length(tvb); + + /* Make sure content has been translated into binary string. */ + guint converted_content_length = content_convert_to_binary(content); + + /* Look for a match at each position. */ + for (guint m=start_offset; m <= (tvb_len-converted_content_length); m++) { + const guint8 *ptr = tvb_get_ptr(tvb, m, converted_content_length); + if (content->nocase) { + if (content_compare_case_insensitive(ptr, content->binary_str, content->translated_length)) { + *match_offset = m; + *match_length = content->translated_length; + return TRUE; + } + } + else { + if (content_compare_case_sensitive(ptr, content->binary_str, content->translated_length)) { + *match_offset = m; + *match_length = content->translated_length; + return TRUE; + } + } + } + + return FALSE; +} + + + + +/* Look for where the content match happens within the tvb. + * Set out parameters match_offset and match_length */ +static gboolean get_content_match(Alert_t *alert, guint content_idx, + tvbuff_t *tvb, guint content_start_match, + guint *match_offset, guint *match_length) +{ + content_t *content; + Rule_t *rule = alert->matched_rule; + + /* Can't match if don't know rule */ + if (rule == NULL) { + return FALSE; + } + + /* Get content object. */ + content = &(rule->contents[content_idx]); + + /* Look for content match in the packet */ + return look_for_content(content, tvb, content_start_match, match_offset, match_length); +} + + +/* Gets called when snort process has died */ +static void snort_reaper(GPid pid, gint status _U_, gpointer data) +{ + snort_session_t *session = (snort_session_t *) data; + if (session->running && session->pid == pid) { + session->working = session->running = FALSE; + /* XXX, cleanup */ + } else { + g_print("Errrrmm snort_reaper() %d != %d\n", session->pid, pid); + } + + /* Close the snort pid (may only make a difference on Windows?) */ + g_spawn_close_pid(pid); +} + +/* Parse timestamp line of output. This is done in part to get the packet_number back out of usec field... + * Return valuee is the input stream moved onto the next field following the timestamp */ +static const char* snort_parse_ts(const char *ts, struct timeval *tv) +{ + struct tm tm; + unsigned int usec; + + /* Timestamp */ + memset(&tm, 0, sizeof(tm)); + tm.tm_isdst = -1; + if (sscanf(ts, "%02d/%02d/%02d-%02d:%02d:%02d.%06u ", + &(tm.tm_mon), &(tm.tm_mday), &(tm.tm_year), &(tm.tm_hour), &(tm.tm_min), &(tm.tm_sec), &usec) != 7) { + return NULL; + } + tm.tm_mon -= 1; + tm.tm_year += 100; + + tv->tv_sec = (long)mktime(&tm); + tv->tv_usec = usec; + + return strchr(ts, ' '); +} + +/* Parse a fast output alert string */ +static gboolean snort_parse_fast_line(const char *line, Alert_t *alert) +{ + static const char stars[] = " [**] "; + + static const char classification[] = "[Classification: "; + static const char priority[] = "[Priority: "; + const char *tmp_msg; + + /* Look for timestamp */ + if (!(line = snort_parse_ts(line, &(alert->tv)))) { + return FALSE; + } + + /* [**] */ + if (!g_str_has_prefix(line+1, stars)) { + return FALSE; + } + line += sizeof(stars); + + /* [%u:%u:%u] */ + if (sscanf(line, "[%u:%u:%u] ", &(alert->gen), &(alert->sid), &(alert->rev)) != 3) { + return FALSE; + } + if (!(line = strchr(line, ' '))) { + return FALSE; + } + + /* [**] again */ + tmp_msg = line+1; + if (!(line = strstr(line, stars))) { + return FALSE; + } + + /* msg */ + alert->msg = g_strndup(tmp_msg, line - tmp_msg); + line += (sizeof(stars)-1); + + /* [Classification: Attempted Administrator Privilege Gain] [Priority: 10] */ + + if (g_str_has_prefix(line, classification)) { + /* [Classification: %s] */ + char *tmp; + line += (sizeof(classification)-1); + + if (!(tmp = (char*)strstr(line, "] [Priority: "))) { + return FALSE; + } + + /* assume "] [Priority: " is not inside classification text :) */ + alert->classification = g_strndup(line, tmp - line); + + line = tmp+2; + } else + alert->classification = NULL; + + /* Optimized: if al->classification we already checked this in strstr() above */ + if (alert->classification || g_str_has_prefix(line, priority)) { + /* [Priority: %d] */ + line += (sizeof(priority)-1); + + if ((sscanf(line, "%d", &(alert->prio))) != 1) { + return FALSE; + } + + if (!(line = strstr(line, "] "))) { + return FALSE; + } + } else { + alert->prio = -1; /* XXX */ + } + + return TRUE; +} + +/** + * snort_parse_user_comment() + * + * Parse line as written by TraceWranger + * e.g. "1:2011768:4 - ET WEB_SERVER PHP tags in HTTP POST" + */ +static gboolean snort_parse_user_comment(const char *line, Alert_t *alert) +{ + /* %u:%u:%u */ + if (sscanf(line, "%u:%u:%u", &(alert->gen), &(alert->sid), &(alert->rev)) != 3) { + return FALSE; + } + + /* Skip separator between numbers and msg */ + if (!(line = strstr(line, " - "))) { + return FALSE; + } + + /* Copy to be consistent with other use of Alert_t */ + alert->msg = g_strdup(line); + + /* No need to set other fields as assume zero'd out before this call.. */ + return TRUE; +} + +/* Output data has been received from snort. Read from channel and look for whole alerts. */ +static gboolean snort_fast_output(GIOChannel *source, GIOCondition condition, gpointer data) +{ + snort_session_t *session = (snort_session_t *)data; + + /* Loop here until all available input read */ + while (condition & G_IO_IN) { + GIOStatus status; + char _buf[1024]; + gsize len = 0; + + char *old_buf = NULL; + char *buf = _buf; + char *line; + + /* Try to read snort output info _buf */ + status = g_io_channel_read_chars(source, _buf, sizeof(_buf)-1, &len, NULL); + if (status != G_IO_STATUS_NORMAL) { + if (status == G_IO_STATUS_AGAIN) { + /* Blocked, so unset G_IO_IN and get out of this function */ + condition = (GIOCondition)(condition & ~G_IO_IN); + break; + } + /* Other conditions here could be G_IO_STATUS_ERROR, G_IO_STATUS_EOF */ + return FALSE; + } + /* Terminate buffer */ + buf[len] = '\0'; + + /* If we previously had part of a line, append the new bit we just saw */ + if (session->buf) { + g_string_append(session->buf, buf); + buf = old_buf = g_string_free(session->buf, FALSE); + session->buf = NULL; + } + + /* Extract every complete line we find in the output */ + while ((line = strchr(buf, '\n'))) { + /* Have a whole line, so can parse */ + Alert_t alert; + + /* Terminate received line */ + *line = '\0'; + + if (snort_parse_fast_line(buf, &alert)) { + /*******************************************************/ + /* We have an alert line. */ +#if 0 + g_print("%ld.%lu [%u,%u,%u] %s {%s} [%d]\n", + alert.tv.tv_sec, alert.tv.tv_usec, + alert.gen, alert.sid, alert.rev, + alert.msg, + alert.classification ? alert.classification : "(null)", + alert.prio); +#endif + + /* Copy the raw alert string itself */ + alert.raw_alert = g_strdup(buf); + + /* See if we can get more info from the parsed config details */ + fill_alert_config(g_snort_config, &alert); + + /* Add parsed alert into session->tree */ + /* Store in tree. pfino->fd->num is hidden in usec time field. */ + add_alert_to_session_tree((guint)alert.tv.tv_usec, &alert); + + } + else { + g_print("snort_fast_output() line: '%s'\n", buf); + } + + buf = line+1; + } + + if (buf[0]) { + /* Only had part of a line - store it */ + /* N.B. typically happens maybe once every 5-6 alerts. */ + session->buf = g_string_new(buf); + } + + g_free(old_buf); + } + + if (condition) { + /* Will report errors (hung-up, or error) */ + + /* g_print("snort_fast_output() cond: (h:%d,e:%d,r:%d)\n", + * !!(condition & G_IO_HUP), !!(condition & G_IO_ERR), condition); */ + return FALSE; + } + + return TRUE; +} + + +/* Return the offset in the frame where snort should begin looking inside payload. */ +static guint get_protocol_payload_start(const char *protocol, proto_tree *tree) +{ + guint value = 0; + + /* For icmp, look from start, whereas for others start after them. */ + gboolean look_after_protocol = (strcmp(protocol, "icmp") != 0); + + if (tree != NULL) { + GPtrArray *items = proto_all_finfos(tree); + if (items) { + guint i; + for (i=0; i< items->len; i++) { + field_info *field = (field_info *)g_ptr_array_index(items,i); + if (strcmp(field->hfinfo->abbrev, protocol) == 0) { + value = field->start; + if (look_after_protocol) { + value += field->length; + } + break; + } + } + g_ptr_array_free(items,TRUE); + } + } + return value; +} + + +/* Return offset that application layer traffic will begin from. */ +static guint get_content_start_match(Rule_t *rule, proto_tree *tree) +{ + /* Work out where snort would start looking for data in the frame */ + return get_protocol_payload_start(rule->protocol, tree); +} + +/* Show the Snort protocol tree based on the info in alert */ +static void snort_show_alert(proto_tree *tree, tvbuff_t *tvb, packet_info *pinfo, Alert_t *alert) +{ + proto_tree *snort_tree = NULL; + unsigned int n; + proto_item *ti, *rule_ti; + proto_tree *rule_tree; + Rule_t *rule = alert->matched_rule; + + /* Can only find start if we have the rule and know the protocol */ + guint content_start_match = 0; + if (rule) { + content_start_match = get_content_start_match(rule, tree); + } + + /* Snort output arrived and was previously stored - so add to tree */ + /* Take care not to try to highlight bytes that aren't there.. */ + proto_item *alert_ti = proto_tree_add_protocol_format(tree, proto_snort, tvb, + content_start_match >= tvb_captured_length(tvb) ? 0 : content_start_match, + content_start_match >= tvb_captured_length(tvb) ? 0 : -1, + "Snort: (msg: \"%s\" sid: %u rev: %u) [from %s]", + alert->msg, alert->sid, alert->rev, + (pref_snort_alerts_source == FromUserComments) ? + "User Comment" : + "Running Snort"); + snort_tree = proto_item_add_subtree(alert_ti, ett_snort); + + /* Show in expert info if configured to. */ + if (snort_show_alert_expert_info) { + expert_add_info_format(pinfo, alert_ti, &ei_snort_alert, "Alert %u: \"%s\"", alert->sid, alert->msg); + } + + /* Show the raw alert string. */ + if (rule) { + ti = proto_tree_add_string(snort_tree, hf_snort_raw_alert, tvb, 0, 0, alert->raw_alert); + PROTO_ITEM_SET_GENERATED(ti); + } + + /* Rule classification */ + if (alert->classification) { + ti = proto_tree_add_string(snort_tree, hf_snort_classification, tvb, 0, 0, alert->classification); + PROTO_ITEM_SET_GENERATED(ti); + } + + /* Put rule fields under a rule subtree */ + + rule_ti = proto_tree_add_string_format(snort_tree, hf_snort_rule, tvb, 0, 0, "", "Rule"); + PROTO_ITEM_SET_GENERATED(rule_ti); + rule_tree = proto_item_add_subtree(rule_ti, ett_snort_rule); + + /* msg/description */ + ti = proto_tree_add_string(rule_tree, hf_snort_msg, tvb, 0, 0, alert->msg); + PROTO_ITEM_SET_GENERATED(ti); + /* Snort ID */ + ti = proto_tree_add_uint(rule_tree, hf_snort_sid, tvb, 0, 0, alert->sid); + PROTO_ITEM_SET_GENERATED(ti); + /* Rule revision */ + ti = proto_tree_add_uint(rule_tree, hf_snort_rev, tvb, 0, 0, alert->rev); + PROTO_ITEM_SET_GENERATED(ti); + /* Generator seems to correspond to gid. */ + ti = proto_tree_add_uint(rule_tree, hf_snort_generator, tvb, 0, 0, alert->gen); + PROTO_ITEM_SET_GENERATED(ti); + /* Default priority is 2 - very few rules have a different priority... */ + ti = proto_tree_add_uint(rule_tree, hf_snort_priority, tvb, 0, 0, alert->prio); + PROTO_ITEM_SET_GENERATED(ti); + + /* If we know the rule for this alert, show some of the rule fields */ + if (rule && rule->rule_string) { + size_t rule_string_length = strlen(rule->rule_string); + + /* Show rule string itself. Add it as a separate data source so can read it all */ + if (rule_string_length > 60) { + tvbuff_t *rule_string_tvb = tvb_new_child_real_data(tvb, rule->rule_string, + (guint)rule_string_length, + (guint)rule_string_length); + add_new_data_source(pinfo, rule_string_tvb, "Rule String"); + ti = proto_tree_add_string(rule_tree, hf_snort_rule_string, rule_string_tvb, 0, + (gint)rule_string_length, + rule->rule_string); + } + else { + ti = proto_tree_add_string(rule_tree, hf_snort_rule_string, tvb, 0, 0, + rule->rule_string); + } + PROTO_ITEM_SET_GENERATED(ti); + + /* Protocol from rule */ + ti = proto_tree_add_string(rule_tree, hf_snort_rule_protocol, tvb, 0, 0, rule->protocol); + PROTO_ITEM_SET_GENERATED(ti); + + /* Show file alert came from */ + ti = proto_tree_add_string(rule_tree, hf_snort_rule_filename, tvb, 0, 0, rule->file); + PROTO_ITEM_SET_GENERATED(ti); + /* Line number within file */ + ti = proto_tree_add_uint(rule_tree, hf_snort_rule_line_number, tvb, 0, 0, rule->line_number); + PROTO_ITEM_SET_GENERATED(ti); + + /* Show IP vars */ + for (n=0; n < rule->relevant_vars.num_ip_vars; n++) { + ti = proto_tree_add_none_format(rule_tree, hf_snort_rule_ip_var, tvb, 0, 0, "IP Var: ($%s -> %s)", + rule->relevant_vars.ip_vars[n].name, + rule->relevant_vars.ip_vars[n].value); + PROTO_ITEM_SET_GENERATED(ti); + } + /* Show Port vars */ + for (n=0; n < rule->relevant_vars.num_port_vars; n++) { + ti = proto_tree_add_none_format(rule_tree, hf_snort_rule_ip_var, tvb, 0, 0, "Port Var: ($%s -> %s)", + rule->relevant_vars.port_vars[n].name, + rule->relevant_vars.port_vars[n].value); + PROTO_ITEM_SET_GENERATED(ti); + } + } + + + /* Show summary information in rule tree root */ + proto_item_append_text(rule_ti, " %s (sid=%u, rev=%u)", + alert->msg, alert->sid, alert->rev); + + /* More fields retrieved from the parsed config */ + if (rule) { + guint content_last_match_end = 0; + + /* Work out which ip and port vars are relevant */ + rule_set_relevant_vars(g_snort_config, rule); + + /* Contents */ + for (n=0; n < rule->number_contents; n++) { + + /* Search for string among tvb contents so we can highlight likely bytes. */ + unsigned int content_offset; + gboolean match_found = FALSE; + unsigned int converted_content_length; + int content_hf_item; + char *content_text_template; + gboolean attempt_match; + + /* Choose type of content field to add */ + switch (rule->contents[n].content_type) { + case Content: + content_hf_item = hf_snort_content; + content_text_template = "Content: \"%s\""; + attempt_match = TRUE; + break; + case UriContent: + content_hf_item = hf_snort_uricontent; + content_text_template = "Uricontent: \"%s\""; + attempt_match = TRUE; + break; + case Pcre: + content_hf_item = hf_snort_pcre; + content_text_template = "Pcre: \"%s\""; + attempt_match = FALSE; + break; + default: + continue; + } + + /* Will only try to look for content in packet ourselves if not + a negated content entry (i.e. beginning with '!') */ + if (attempt_match && !rule->contents[n].negation) { + /* Look up offset of match. N.B. would only expect to see on first content... */ + guint offset_to_add = 0; + + /* May need to add absolute offset into packet... */ + if (rule->contents[n].offset_set) { + offset_to_add = rule->contents[n].offset; + } + /* ... or a number of bytes beyond the previous content match */ + else if (rule->contents[n].distance_set) { + offset_to_add = (content_last_match_end-content_start_match) + rule->contents[n].distance; + } + + /* Now actually look for match from calculated position */ + /* TODO: could take 'depth' and 'within' into account to limit extent of search, + but OK if just trying to verify what Snort already found. */ + match_found = get_content_match(alert, n, + tvb, content_start_match+offset_to_add, + &content_offset, &converted_content_length); + if (match_found) { + content_last_match_end = content_offset + converted_content_length; + } + } + + + /* Show content in tree (showing position if known) */ + ti = proto_tree_add_string_format(snort_tree, content_hf_item, tvb, + (match_found) ? content_offset : 0, + (match_found) ? converted_content_length : 0, + rule->contents[n].str, + content_text_template, + rule->contents[n].str); + if (!attempt_match) { + /* TODO: for pcre could try to use same library used by + display filter 'matches' operator? */ + proto_item_append_text(ti, " (no match attempt made)"); + } + + /* Show (only as text) attributes of content field */ + if (rule->contents[n].fastpattern) { + proto_item_append_text(ti, " (fast_pattern)"); + } + if (rule->contents[n].nocase) { + proto_item_append_text(ti, " (nocase)"); + } + if (rule->contents[n].negation) { + proto_item_append_text(ti, " (negated)"); + } + if (rule->contents[n].offset_set) { + proto_item_append_text(ti, " (offset=%d)", rule->contents[n].offset); + } + if (rule->contents[n].depth != 0) { + proto_item_append_text(ti, " (depth=%u)", rule->contents[n].depth); + } + if (rule->contents[n].distance_set) { + proto_item_append_text(ti, " (distance=%d)", rule->contents[n].distance); + } + if (rule->contents[n].within != 0) { + proto_item_append_text(ti, " (within=%u)", rule->contents[n].within); + } + + /* HTTP preprocessor modifiers */ + if (rule->contents[n].http_method != 0) { + proto_item_append_text(ti, " (http_method)"); + } + if (rule->contents[n].http_client_body != 0) { + proto_item_append_text(ti, " (http_client_body)"); + } + if (rule->contents[n].http_cookie != 0) { + proto_item_append_text(ti, " (http_cookie)"); + } + + if (attempt_match && !rule->contents[n].negation && !match_found) { + /* Useful for debugging, may also happen when Snort is reassembling.. */ + proto_item_append_text(ti, " - not located"); + expert_add_info_format(pinfo, ti, &ei_snort_content_not_matched, + "Content \"%s\" not found in frame", + rule->contents[n].str); + } + } + + /* References */ + for (n=0; n < rule->number_references; n++) { + /* Substitute prefix and add to tree as clickable web links */ + ti = proto_tree_add_string(snort_tree, hf_snort_reference, tvb, 0, 0, + expand_reference(g_snort_config, rule->references[n])); + /* Make clickable */ + PROTO_ITEM_SET_URL(ti); + PROTO_ITEM_SET_GENERATED(ti); + } + } + + /* Global rule stats if configured to. */ + if (snort_show_rule_stats) { + unsigned int number_rule_files, number_rules, alerts_detected, this_rule_alerts_detected; + proto_item *stats_ti; + proto_tree *stats_tree; + + /* Create tree for these items */ + stats_ti = proto_tree_add_string_format(snort_tree, hf_snort_global_stats, tvb, 0, 0, "", "Global Stats"); + PROTO_ITEM_SET_GENERATED(rule_ti); + stats_tree = proto_item_add_subtree(stats_ti, ett_snort_global_stats); + + /* Get overall number of rules */ + get_global_rule_stats(g_snort_config, alert->sid, &number_rule_files, &number_rules, &alerts_detected, + &this_rule_alerts_detected); + ti = proto_tree_add_uint(stats_tree, hf_snort_global_stats_rule_file_count, tvb, 0, 0, number_rule_files); + PROTO_ITEM_SET_GENERATED(ti); + ti = proto_tree_add_uint(stats_tree, hf_snort_global_stats_rule_count, tvb, 0, 0, number_rules); + PROTO_ITEM_SET_GENERATED(ti); + + /* Overall alert stats (total, and where this one comes in order) */ + ti = proto_tree_add_uint(stats_tree, hf_snort_global_stats_total_alerts_count, tvb, 0, 0, alerts_detected); + PROTO_ITEM_SET_GENERATED(ti); + ti = proto_tree_add_uint(stats_tree, hf_snort_global_stats_alert_match_number, tvb, 0, 0, alert->overall_match_number); + PROTO_ITEM_SET_GENERATED(ti); + + if (rule) { + /* Stats just for this rule (overall, and where this one comes in order) */ + ti = proto_tree_add_uint(stats_tree, hf_snort_global_stats_rule_alerts_count, tvb, 0, 0, this_rule_alerts_detected); + PROTO_ITEM_SET_GENERATED(ti); + ti = proto_tree_add_uint(stats_tree, hf_snort_global_stats_rule_match_number, tvb, 0, 0, alert->rule_match_number); + PROTO_ITEM_SET_GENERATED(ti); + + /* Add a summary to the stats root */ + proto_item_append_text(stats_ti, " (%u rules from %u files, #%u of %u alerts seen (%u/%u for sid %u))", + number_rules, number_rule_files, alert->overall_match_number, alerts_detected, + alert->rule_match_number, this_rule_alerts_detected, alert->sid); + } + else { + /* Add a summary to the stats root */ + proto_item_append_text(stats_ti, " (%u rules from %u files, #%u of %u alerts seen)", + number_rules, number_rule_files, alert->overall_match_number, alerts_detected); + } + } +} + +/* Look for, and return, any user comment set for this packet. + Currently used for fetching alerts in the format TraceWrangler can write out to */ +static const char *get_user_comment_string(proto_tree *tree) +{ + const char *value = NULL; + + if (tree != NULL) { + GPtrArray *items = proto_all_finfos(tree); + if (items) { + guint i; + + for (i=0; i< items->len; i++) { + field_info *field = (field_info *)g_ptr_array_index(items,i); + if (strcmp(field->hfinfo->abbrev, "frame.comment") == 0) { + value = field->value.value.string; + break; + } + /* This is the only item that can come before "frame.comment", so otherwise break out */ + if (strncmp(field->hfinfo->abbrev, "pkt_comment", 11) != 0) { + break; + } + } + g_ptr_array_free(items,TRUE); + } + } + return value; +} + + +#if 0 +/* TODO: unfortunately, the first frame in a series of frames to be reassembled has often been + * seen to lack this field, despite being referenced in the reassmbled frame! */ +static guint get_reassembled_in_frame(proto_tree *tree) +{ + guint value = 0; + + if (tree != NULL) { + GPtrArray *items = proto_all_finfos(tree); + if (items) { + guint i; + for (i=0; i< items->len; i++) { + field_info *field = (field_info *)g_ptr_array_index(items,i); + if (strcmp(field->hfinfo->abbrev, "tcp.reassembled_in") == 0) { + value = field->value.value.uinteger; + break; + } + } + g_ptr_array_free(items,TRUE); + } + } + return value; +} +#endif + +/********************************************************************************/ +/* Main (post-)dissector function. */ +static int +snort_dissector(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_) +{ + Alerts_t *alerts; + + /* Are we looking for alerts in user comments? */ + if (pref_snort_alerts_source == FromUserComments) { + /* Look for user comments containing alerts */ + const char *alert_string = get_user_comment_string(tree); + if (alert_string) { + alerts = (Alerts_t*)wmem_tree_lookup32(current_session.alerts_tree, pinfo->num); + if (!alerts) { + Alert_t alert; + memset(&alert, 0, sizeof(alert)); + if (snort_parse_user_comment(alert_string, &alert)) { + /* Copy the raw alert itself */ + alert.raw_alert = g_strdup(alert_string); + + /* See if we can get more info from the parsed config details */ + fill_alert_config(g_snort_config, &alert); + + /* Add parsed alert into session->tree */ + add_alert_to_session_tree(pinfo->num, &alert); + } + } + } + } + else { + /* We expect alerts from Snort. Pass frame into snort on first pass. */ + if (!pinfo->fd->flags.visited && current_session.working) { + int write_err = 0; + gchar *err_info; + struct wtap_pkthdr wtp; + + /* First time, open current_session.in to write to for dumping into snort with */ + if (!current_session.pdh) { + int open_err; + + /* Older versions of Snort don't support capture file with several encapsulations (like pcapng), + * so write in pcap format and hope we have just one encap. + * Newer versions of Snort can read pcapng now, but still write in pcap format. + */ + current_session.pdh = wtap_dump_fdopen(current_session.in, + WTAP_FILE_TYPE_SUBTYPE_PCAP, + pinfo->pkt_encap, + WTAP_MAX_PACKET_SIZE, + FALSE, /* compressed */ + &open_err); + if (!current_session.pdh) { + current_session.working = FALSE; + return 0; + } + } + + /* Start with all same values... */ + memcpy(&wtp, pinfo->phdr, sizeof(wtp)); + + /* Copying packet details into wtp for writing */ + wtp.ts.secs = pinfo->fd->abs_ts.secs; + wtp.ts.nsecs = pinfo->fd->abs_ts.nsecs; + + /* NB: overwriting wtp.ts.nsecs so we can see packet number back if an alert is written for this frame!!!! */ + /* TODO: does this seriously affect snort's ability to reason about time? + * At least all packets will still be in order... */ + wtp.ts.nsecs = pinfo->fd->num * 1000; /* XXX, max 999'999 frames */ + + wtp.caplen = tvb_captured_length(tvb); + wtp.len = tvb_reported_length(tvb); + wtp.pkt_encap = pinfo->pkt_encap; + if (current_session.pdh->encap != wtp.pkt_encap) { + /* XXX, warning! convert? */ + } + + /* Dump frame into snort's stdin */ + if (!wtap_dump(current_session.pdh, &wtp, tvb_get_ptr(tvb, 0, tvb_reported_length(tvb)), &write_err, &err_info)) { + current_session.working = FALSE; + return 0; + } + wtap_dump_flush(current_session.pdh); + + /* Give the io channel a chance to deliver alerts. + TODO: g_main_context_iteration(NULL, FALSE); causes crashes sometimes when Qt events get to execute.. */ + } + } + + /* Now look up stored alerts for this packet number, and display if found */ + if (current_session.alerts_tree && (alerts = (Alerts_t*)wmem_tree_lookup32(current_session.alerts_tree, pinfo->fd->num))) { + guint n; + + for (n=0; n < alerts->num_alerts; n++) { + snort_show_alert(tree, tvb, pinfo, &(alerts->alerts[n])); + } + } else { + /* XXX, here either this frame doesn't generate alerts or we haven't received data from snort (async) + * + * It's problem when user want to filter tree on initial run, or is running one-pass tshark. + */ + } + + return tvb_reported_length(tvb); +} + +/* N.B. is being called.. */ +static void snort_config(gpointer user_data _U_) +{ + /* N.B. original code tried to get line-buffered (or unbuffered) output from snort. + It wasn't very portable, and measurements indicated it didn't make any difference + to how often whole lines were output. */ +} + +/*------------------------------------------------------------------*/ +/* Start up Snort. */ +static void snort_start(void) +{ + GIOChannel *channel; + /* int snort_output_id; */ + const gchar *argv[] = { + pref_snort_binary_filename, "-c", pref_snort_config_filename, + /* read from stdin */ + "-r", "-", + /* don't log */ + "-N", + /* output to console and silence snort */ + "-A", "console", "-q", + /* normalize time */ + "-y", /* -U", */ + NULL + }; + + /* Create tree mapping packet_number -> Alerts_t*. It will get recreated when packet list is reloaded */ + current_session.alerts_tree = wmem_tree_new_autoreset(wmem_epan_scope(), wmem_file_scope()); + + /* Create afresh the config object by parsing the same file that snort uses */ + if (g_snort_config) { + delete_config(&g_snort_config); + } + create_config(&g_snort_config, pref_snort_config_filename); + + /* Don't run Snort if not configured to */ + if (pref_snort_alerts_source == FromUserComments) { + return; + } + + if (current_session.running) { + return; + } + current_session.running = TRUE; + + /* Reset global stats */ + reset_global_rule_stats(g_snort_config); + + /* Need to test that we can run snort --version and that config can be parsed... */ + /* Does nothing at present */ + if (!snort_config_ok) { + current_session.running = FALSE; + /* Can carry on without snort... */ + return; + } + + /* Create snort process and set up pipes */ + if (!g_spawn_async_with_pipes(NULL, /* working_directory */ + (char **)argv, + NULL, /* envp */ + (GSpawnFlags)( G_SPAWN_DO_NOT_REAP_CHILD), /* Leave out G_SPAWN_SEARCH_PATH */ + snort_config, /* child setup - not currently doing anything.. */ + NULL, /* user-data */ + ¤t_session.pid, /* PID */ + ¤t_session.in, /* stdin */ + ¤t_session.out, /* stdout */ + ¤t_session.err, /* stderr */ + NULL)) /* error */ + { + current_session.running = FALSE; + current_session.working = FALSE; + return; + } + + /* Setup handler for when process goes away */ + g_child_watch_add(current_session.pid, snort_reaper, ¤t_session); + + /******************************************************************/ + /* Create channel to get notified of snort alert output on stdout */ + + /* Create channel itself */ + channel = g_io_channel_unix_new(current_session.out); + current_session.channel = channel; + + /* NULL encoding supports binary or whatever the application outputs */ + g_io_channel_set_encoding(channel, NULL, NULL); + /* Don't buffer the channel (settable because encoding set to NULL). */ + g_io_channel_set_buffered(channel, FALSE); + /* Set flags */ + /* TODO: could set to be blocking and get sync that way? */ + g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, NULL); + /* Try setting a large buffer here. */ + g_io_channel_set_buffer_size(channel, 256000); + + current_session.buf = NULL; + + /* Set callback for receiving data from the channel */ + g_io_add_watch_full(channel, + G_PRIORITY_HIGH, + (GIOCondition)(G_IO_IN|G_IO_ERR|G_IO_HUP), + snort_fast_output, /* Callback upon data being written by snort */ + ¤t_session, /* User data */ + NULL); /* Destroy notification callback */ + + current_session.working = TRUE; +} + +/* This is the cleanup routine registered with register_postseq_cleanup_routine() */ +static void snort_cleanup(void) +{ + /* Only close if we think its running */ + if (!current_session.running) { + return; + } + + /* Close dumper writing into snort's stdin. This will cause snort to exit! */ + if (current_session.pdh) { + int write_err; + if (!wtap_dump_close(current_session.pdh, &write_err)) { + + } + current_session.pdh = NULL; + } +} + +static void snort_file_cleanup(void) +{ + if (g_snort_config) { + delete_config(&g_snort_config); + } +} + +void +proto_reg_handoff_snort(void) +{ + /* N.B. snort self-test here deleted, as I was struggling to get it to + * work as a non-root user (couldn't read stdin) + * TODO: could run snort just to get the version number and check the config file is readable? + * TODO: could make snort config parsing less forgiving and use that as a test? */ + + /* Our own preference for turning off completely. Don't want to run at all unless turned on */ + proto_set_decoding(proto_snort, snort_enable_dissector); +} + +void +proto_register_snort(void) +{ + static hf_register_info hf[] = { + { &hf_snort_sid, + { "Rule SID", "snort.sid", FT_UINT32, BASE_DEC, NULL, 0x00, + "Snort Rule identifier", HFILL }}, + { &hf_snort_raw_alert, + { "Raw Alert", "snort.raw-alert", FT_STRING, BASE_NONE, NULL, 0x00, + "Full text of Snort alert", HFILL }}, + { &hf_snort_rule, + { "Rule", "snort.rule", FT_STRING, BASE_NONE, NULL, 0x00, + "Entire Snort rule string", HFILL }}, + { &hf_snort_msg, + { "Alert Message", "snort.msg", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Description of what the rule detects", HFILL }}, + { &hf_snort_classification, + { "Alert Classification", "snort.class", FT_STRINGZ, BASE_NONE, NULL, 0x00, + NULL, HFILL }}, + { &hf_snort_priority, + { "Alert Priority", "snort.priority", FT_UINT32, BASE_DEC, NULL, 0x00, + NULL, HFILL }}, + { &hf_snort_generator, + { "Rule Generator", "snort.generator", FT_UINT32, BASE_DEC, NULL, 0x00, + NULL, HFILL }}, + { &hf_snort_rev, + { "Rule Revision", "snort.rev", FT_UINT32, BASE_DEC, NULL, 0x00, + NULL, HFILL }}, + { &hf_snort_rule_string, + { "Rule String", "snort.rule-string", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Full text of Snort rule", HFILL }}, + { &hf_snort_rule_protocol, + { "Protocol", "snort.protocol", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Protocol name as given in the rule", HFILL }}, + { &hf_snort_rule_filename, + { "Rule Filename", "snort.rule-filename", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Rules file where Snort rule was parsed from", HFILL }}, + { &hf_snort_rule_line_number, + { "Line number within rules file where rule was parsed from", "snort.rule-line-number", FT_UINT32, BASE_DEC, NULL, 0x00, + NULL, HFILL }}, + { &hf_snort_rule_ip_var, + { "IP variable", "snort.rule-ip-var", FT_NONE, BASE_NONE, NULL, 0x00, + "IP variable used in rule", HFILL }}, + { &hf_snort_rule_port_var, + { "Port variable used in rule", "snort.rule-port-var", FT_NONE, BASE_NONE, NULL, 0x00, + NULL, HFILL }}, + { &hf_snort_content, + { "Content", "snort.content", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Snort content field", HFILL }}, + { &hf_snort_uricontent, + { "URI Content", "snort.uricontent", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Snort URI content field", HFILL }}, + { &hf_snort_pcre, + { "PCRE", "snort.pcre", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Perl Compatible Regular Expression", HFILL }}, + { &hf_snort_reference, + { "Reference", "snort.reference", FT_STRINGZ, BASE_NONE, NULL, 0x00, + "Web reference provided as part of rule", HFILL }}, + + /* Global stats */ + { &hf_snort_global_stats, + { "Global Stats", "snort.global-stats", FT_STRING, BASE_NONE, NULL, 0x00, + "Global statistics for rules and alerts", HFILL }}, + { &hf_snort_global_stats_rule_file_count, + { "Number of rule files", "snort.global-stats.rule-file-count", FT_UINT32, BASE_DEC, NULL, 0x00, + "Total number of rules files found in Snort config", HFILL }}, + { &hf_snort_global_stats_rule_count, + { "Number of rules", "snort.global-stats.rule-count", FT_UINT32, BASE_DEC, NULL, 0x00, + "Total number of rules found in Snort config", HFILL }}, + { &hf_snort_global_stats_total_alerts_count, + { "Number of alerts detected", "snort.global-stats.total-alerts", FT_UINT32, BASE_DEC, NULL, 0x00, + "Total number of alerts detected in this capture", HFILL }}, + { &hf_snort_global_stats_alert_match_number, + { "Match number", "snort.global-stats.match-number", FT_UINT32, BASE_DEC, NULL, 0x00, + "Number of match for this alert among all alerts", HFILL }}, + + { &hf_snort_global_stats_rule_alerts_count, + { "Number of alerts for this rule", "snort.global-stats.rule.match-number", FT_UINT32, BASE_DEC, NULL, 0x00, + "Number of alerts detected for this rule", HFILL }}, + { &hf_snort_global_stats_rule_match_number, + { "Match number for this rule", "snort.global-stats.rule.match-number", FT_UINT32, BASE_DEC, NULL, 0x00, + "Number of match for this alert among those for this rule", HFILL }} + }; + static gint *ett[] = { + &ett_snort, + &ett_snort_rule, + &ett_snort_global_stats + }; + + static const enum_val_t alerts_source_vals[] = { + {"from-running-snort", "From running Snort", FromRunningSnort}, + {"from-user-comments", "From user comments", FromUserComments}, + {NULL, NULL, -1} + }; + + static ei_register_info ei[] = { + { &ei_snort_alert, { "snort.alert.expert", PI_SECURITY, PI_WARN, "Snort alert detected", EXPFILL }}, + { &ei_snort_content_not_matched, { "snort.content.not-matched", PI_PROTOCOL, PI_NOTE, "Failed to find content field of alert in frame", EXPFILL }}, + }; + + expert_module_t* expert_snort; + + + dissector_handle_t snort_handle; + module_t *snort_module; + + proto_snort = proto_register_protocol("Snort Alerts", "Snort", "snort"); + + proto_register_field_array(proto_snort, hf, array_length(hf)); + proto_register_subtree_array(ett, array_length(ett)); + + /* Expert info */ + expert_snort = expert_register_protocol(proto_snort); + expert_register_field_array(expert_snort, ei, array_length(ei)); + + snort_module = prefs_register_protocol(proto_snort, proto_reg_handoff_snort); + + prefs_register_bool_preference(snort_module, "enable_snort_dissector", + "Enable the snort dissector", + "Whether or not the snort post-dissector should run.", + &snort_enable_dissector); + + prefs_register_enum_preference(snort_module, "alerts_source", + "Source of Snort alerts", + "Set whether dissector should run Snort itself or use user packet comments", + &pref_snort_alerts_source, alerts_source_vals, FALSE); + + prefs_register_filename_preference(snort_module, "binary", + "Snort binary", + "The name of the snort binary file to run", + &pref_snort_binary_filename); + prefs_register_filename_preference(snort_module, "config", + "Configuration filename", + "The name of the file containing the snort IDS configuration. Typically snort.conf", + &pref_snort_config_filename); + + prefs_register_bool_preference(snort_module, "show_rule_set_stats", + "Show rule stats in protocol tree", + "Whether or not information about the rule set and detected alerts should " + "be shown in the tree of every snort PDU tree", + &snort_show_rule_stats); + prefs_register_bool_preference(snort_module, "show_alert_expert_info", + "Show alerts in expert info", + "Whether or not expert info should be used to highlight fired alerts", + &snort_show_alert_expert_info); +#if 0 + prefs_register_bool_preference(snort_module, "show_alert_in_reassembled_frame", + "Try to show alerts in reassembled frame", + "Attempt to show alert in reassembled frame where possible", + &snort_alert_in_reassembled_frame); +#endif + + + snort_handle = create_dissector_handle(snort_dissector, proto_snort); + + register_init_routine(snort_start); + register_postdissector(snort_handle); + + /* Callback to make sure we cleanup dumper being used to deliver packets to snort (this will tsnort). */ + register_postseq_cleanup_routine(snort_cleanup); + /* Callback to allow us to delete snort config */ + register_cleanup_routine(snort_file_cleanup); +} + +/* + * Editor modelines - http://www.wireshark.org/tools/modelines.html + * + * Local variables: + * c-basic-offset: 4 + * tab-width: 8 + * indent-tabs-mode: nil + * End: + * + * vi: set shiftwidth=4 tabstop=8 expandtab: + * :indentSize=4:tabSize=8:noTabs=true: + */ diff --git a/epan/dissectors/snort-config.c b/epan/dissectors/snort-config.c new file mode 100644 index 0000000000..1d53e5de5c --- /dev/null +++ b/epan/dissectors/snort-config.c @@ -0,0 +1,1103 @@ +/* snort-config.c + * + * Copyright 2016, Martin Mathieson + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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. + */ + +#include "config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +#include <wsutil/file_util.h> +#include <wsutil/strtoi.h> + +#include "snort-config.h" + +/* #define SNORT_CONFIG_DEBUG */ +#ifdef SNORT_CONFIG_DEBUG +#define snort_debug_printf printf +#else +#define snort_debug_printf(...) +#endif + +#ifndef _WIN32 +const char* g_file_separator = "/"; +#else +const char* g_file_separator = "\\"; +#endif + + +/* Forward declaration */ +static void parse_config_file(SnortConfig_t *snort_config, FILE *config_file_fd, const char *filename, const char *dirname, int recursion_level); + +/* Skip white space from 'source', return pointer to first non-whitespace char */ +static char *skipWhiteSpace(char *source, int *accumulated_offset) +{ + int offset = 0; + + /* Skip any leading whitespace */ + while (source[offset] != '\0' && source[offset] == ' ') { + offset++; + } + + *accumulated_offset += offset; + return source + offset; +} + +/* Read a token from source, stop when get to end of string or delimiter. */ +/* - source: input string + * - delimiter: char to stop at + * - length: out param set to delimiter or end-of-string offset + * - accumulated_Length: out param that gets length added to it + * - copy: whether or an allocated string should be returned + * - returns: requested string. Returns from static buffer when copy is FALSE */ +static char* read_token(char* source, char delimeter, int *length, int *accumulated_length, gboolean copy) +{ + static char static_buffer[512]; + int offset = 0; + + char *source_proper = skipWhiteSpace(source, accumulated_length); + + while (source_proper[offset] != '\0' && source_proper[offset] != delimeter) { + offset++; + } + + *length = offset; + *accumulated_length += offset; + if (copy) { + /* Copy into new string */ + char *new_string = g_strndup(source_proper, offset+1); + new_string[offset] = '\0'; + return new_string; + } + else { + /* Return in static buffer */ + memcpy(&static_buffer, source_proper, offset); + static_buffer[offset] = '\0'; + return static_buffer; + } +} + +/* Add a new content field to the rule */ +static gboolean rule_add_content(Rule_t *rule, const char *content_string, gboolean negated) +{ + if (rule->number_contents < MAX_CONTENT_ENTRIES) { + content_t *new_content = &(rule->contents[rule->number_contents++]); + new_content->str = g_strdup(content_string); + new_content->negation = negated; + rule->last_added_content = new_content; + return TRUE; + } + return FALSE; +} + +/* Set the nocase property for a rule */ +static void rule_set_content_nocase(Rule_t *rule) +{ + if (rule->last_added_content) { + rule->last_added_content->nocase = TRUE; + } +} + +/* Set the offset property of a content field */ +static void rule_set_content_offset(Rule_t *rule, gint value) +{ + if (rule->last_added_content) { + rule->last_added_content->offset = value; + rule->last_added_content->offset_set = TRUE; + } +} + +/* Set the depth property of a content field */ +static void rule_set_content_depth(Rule_t *rule, guint value) +{ + if (rule->last_added_content) { + rule->last_added_content->depth = value; + } +} + +/* Set the distance property of a content field */ +static void rule_set_content_distance(Rule_t *rule, gint value) +{ + if (rule->last_added_content) { + rule->last_added_content->distance = value; + rule->last_added_content->distance_set = TRUE; + } +} + +/* Set the distance property of a content field */ +static void rule_set_content_within(Rule_t *rule, guint value) +{ + if (rule->last_added_content) { + /* Assuming won't be 0... */ + rule->last_added_content->within = value; + } +} + +/* Set the fastpattern property of a content field */ +static void rule_set_content_fast_pattern(Rule_t *rule) +{ + if (rule->last_added_content) { + rule->last_added_content->fastpattern = TRUE; + } +} + +/* Set the http_method property of a content field */ +static void rule_set_content_http_method(Rule_t *rule) +{ + if (rule->last_added_content) { + rule->last_added_content->http_method = TRUE; + } +} + +/* Set the http_client property of a content field */ +static void rule_set_content_http_client_body(Rule_t *rule) +{ + if (rule->last_added_content) { + rule->last_added_content->http_client_body = TRUE; + } +} + +/* Set the http_cookie property of a content field */ +static void rule_set_content_http_cookie(Rule_t *rule) +{ + if (rule->last_added_content) { + rule->last_added_content->http_cookie = TRUE; + } +} + +/* Add a uricontent field to the rule */ +static gboolean rule_add_uricontent(Rule_t *rule, const char *uricontent_string, gboolean negated) +{ + if (rule_add_content(rule, uricontent_string, negated)) { + rule->last_added_content->content_type = UriContent; + return TRUE; + } + return FALSE; +} + +/* This content field now becomes a uricontent after seeing modifier */ +static void rule_set_http_uri(Rule_t *rule) +{ + if (rule->last_added_content != NULL) { + rule->last_added_content->content_type = UriContent; + } +} + +/* Add a pcre field to the rule */ +static gboolean rule_add_pcre(Rule_t *rule, const char *pcre_string) +{ + if (rule_add_content(rule, pcre_string, FALSE)) { + rule->last_added_content->content_type = Pcre; + return TRUE; + } + return FALSE; +} + +/* Set the rule's classtype field */ +static gboolean rule_set_classtype(Rule_t *rule, const char *classtype) +{ + rule->classtype = g_strdup(classtype); + return TRUE; +} + +/* Add a reference string to the rule */ +static void rule_add_reference(Rule_t *rule, const char *reference_string) +{ + if (rule->number_references < MAX_REFERENCE_ENTRIES) { + rule->references[rule->number_references++] = g_strdup(reference_string); + } +} + +/* Check to see if the ip 'field' corresponds to an entry in the ipvar dictionary. + * If it is add entry to rule */ +static void rule_check_ip_vars(SnortConfig_t *snort_config, Rule_t *rule, char *field) +{ + gpointer original_key = NULL; + gpointer value = NULL; + + /* Make sure field+1 not NULL. */ + if (strlen(field) < 2) { + return; + } + + /* Make sure there is room for another entry */ + if (rule->relevant_vars.num_ip_vars >= MAX_RULE_IP_VARS) { + return; + } + + /* TODO: a loop re-looking up the answer until its not just another ipvar! */ + if (g_hash_table_lookup_extended(snort_config->ipvars, field+1, &original_key, &value)) { + + rule->relevant_vars.ip_vars[rule->relevant_vars.num_ip_vars].name = (char*)original_key; + rule->relevant_vars.ip_vars[rule->relevant_vars.num_ip_vars].value = (char*)value; + + rule->relevant_vars.num_ip_vars++; + } +} + +/* Check to see if the port 'field' corresponds to an entry in the portvar dictionary. + * If it is add entry to rule */ +static void rule_check_port_vars(SnortConfig_t *snort_config _U_, Rule_t *rule, char *field) +{ + gpointer original_key = NULL; + gpointer value = NULL; + + /* Make sure field+1 not NULL. */ + if (strlen(field) < 2) { + return; + } + + /* Make sure there is room for another entry */ + if (rule->relevant_vars.num_port_vars >= MAX_RULE_PORT_VARS) { + return; + } + + /* TODO: a loop re-looking up the answer until its not just another portvar! */ + if (g_hash_table_lookup_extended(snort_config->portvars, field+1, &original_key, &value)) { + rule->relevant_vars.port_vars[rule->relevant_vars.num_port_vars].name = (char*)original_key; + rule->relevant_vars.port_vars[rule->relevant_vars.num_port_vars].value = (char*)value; + + rule->relevant_vars.num_port_vars++; + } +} + +/* Look over the IP addresses and ports, and work out which variables/values are being used */ +void rule_set_relevant_vars(SnortConfig_t *snort_config, Rule_t *rule) +{ + int length; + int accumulated_length = 0; + char *field; + + /* No need to do this twice */ + if (rule->relevant_vars.relevant_vars_set) { + return; + } + + /* Walk tokens up to the options, and look up ones that are addresses or ports. */ + + /* Skip "alert" */ + read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, FALSE); + + /* Skip protocol. */ + read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, FALSE); + + /* Read source address */ + field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, FALSE); + snort_debug_printf("source address is (%s)\n", field); + rule_check_ip_vars(snort_config, rule, field); + + /* Read source port */ + field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, FALSE); + snort_debug_printf("source port is (%s)\n", field); + rule_check_port_vars(snort_config, rule, field); + + /* Read direction */ + read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, FALSE); + + /* Dest address */ + field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, FALSE); + snort_debug_printf("dest address is (%s)\n", field); + rule_check_ip_vars(snort_config, rule, field); + + /* Dest port */ + field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, FALSE); + snort_debug_printf("dest port is (%s)\n", field); + rule_check_port_vars(snort_config, rule, field); + + /* Set flag so won't do again for this rule */ + rule->relevant_vars.relevant_vars_set = TRUE; +} + + +typedef enum vartype_e { var, ipvar, portvar, unknownvar } vartype_e; + +/* Look for a "var", "ipvar" or "portvar" entry in this line */ +static gboolean parse_variables_line(SnortConfig_t *snort_config, char *line) +{ + vartype_e var_type = unknownvar; + + char * variable_type; + char * variable_name; + char * value; + + int length; + int accumulated_length = 0; + + /* Get variable type */ + variable_type = read_token(line, ' ', &length, &accumulated_length, FALSE); + if (variable_type == NULL) { + return FALSE; + } + + if (strncmp(variable_type, "var", 3) == 0) { + var_type = var; + } + else if (strncmp(variable_type, "ipvar", 5) == 0) { + var_type = ipvar; + } + else if (strncmp(variable_type, "portvar", 7) == 0) { + var_type = portvar; + } + else { + return FALSE; + } + + /* Get variable name */ + variable_name = read_token(line+ accumulated_length, ' ', &length, &accumulated_length, TRUE); + if (variable_name == NULL) { + return FALSE; + } + + /* Now value */ + value = read_token(line + accumulated_length, ' ', &length, &accumulated_length, TRUE); + if (value == NULL) { + return FALSE; + } + + /* Add (name->value) to table according to variable type. */ + switch (var_type) { + case var: + if (strcmp(variable_name, "RULE_PATH") == 0) { + /* This can be relative or absolute. */ + snort_config->rule_path = value; + snort_config->rule_path_is_absolute = g_path_is_absolute(value); + snort_debug_printf("rule_path set to %s (is_absolute=%d)\n", + snort_config->rule_path, snort_config->rule_path_is_absolute); + } + g_hash_table_insert(snort_config->vars, variable_name, value); + break; + case ipvar: + g_hash_table_insert(snort_config->ipvars, variable_name, value); + break; + case portvar: + g_hash_table_insert(snort_config->portvars, variable_name, value); + break; + + default: + return FALSE; + } + + return FALSE; +} + +/* Hash function for where key is a string. Just add up the value of each character and return that.. */ +static guint string_hash(gconstpointer key) +{ + guint total=0, n=0; + const char *key_string = (const char *)key; + char c = key_string[n]; + + while (c != '\0') { + total += (int)c; + c = key_string[++n]; + } + return total; +} + +/* Comparison function for where key is a string. Simple comparison using strcmp() */ +static gboolean string_equal(gconstpointer a, gconstpointer b) +{ + const char *stringa = (const char*)a; + const char *stringb = (const char*)b; + + return (strcmp(stringa, stringb) == 0); +} + +/* Process a line that configures a reference line (invariably from 'reference.config') */ +static gboolean parse_references_prefix_file_line(SnortConfig_t *snort_config, char *line) +{ + char *source; + char *prefix_name, *prefix_value; + int length=0, accumulated_length=0; + int n; + + if (strncmp(line, "config reference: ", 18) != 0) { + return FALSE; + } + + /* Read the prefix and value */ + source = line+18; + prefix_name = read_token(source, ' ', &length, &accumulated_length, TRUE); + /* Store all name chars in lower case. */ + for (n=0; prefix_name[n] != '\0'; n++) { + prefix_name[n] = g_ascii_tolower(prefix_name[n]); + } + + prefix_value = read_token(source+accumulated_length, ' ', &length, &accumulated_length, TRUE); + + /* Add entry into table */ + g_hash_table_insert(snort_config->references_prefixes, prefix_name, prefix_value); + + return FALSE; +} + +/* Try to expand the reference using the prefixes stored in the config */ +char *expand_reference(SnortConfig_t *snort_config, char *reference) +{ + static char expanded_reference[512]; + int length = (int)strlen(reference); + int accumulated_length = 0; + + /* Extract up to ',', then substitute prefix! */ + snort_debug_printf("expand_reference(%s)\n", reference); + char *prefix = read_token(reference, ',', &length, &accumulated_length, FALSE); + + if (prefix != '\0') { + /* Convert to lowercase before lookup */ + guint n; + for (n=0; prefix[n] != '\0'; n++) { + prefix[n] = g_ascii_tolower(prefix[n]); + } + + /* Look up prefix in table. */ + char *prefix_replacement; + prefix_replacement = (char*)g_hash_table_lookup(snort_config->references_prefixes, prefix); + + /* Append prefix and remainder, and return!!!! */ + g_snprintf(expanded_reference, 512, "%s%s", prefix_replacement, reference+length+1); + return expanded_reference; + } + return "ERROR: Reference didn't contain prefix and ','!"; +} + +/* The rule has been matched with an alert, so update global config stats */ +void rule_set_alert(SnortConfig_t *snort_config, Rule_t *rule, + guint *global_match_number, + guint *rule_match_number) +{ + snort_config->stat_alerts_detected++; + *global_match_number = snort_config->stat_alerts_detected; + if (rule != NULL) { + *rule_match_number = ++rule->matches_seen; + } +} + + + +/* Delete an individual entry from a string table. */ +static gboolean delete_string_entry(gpointer key, + gpointer value, + gpointer user_data _U_) +{ + char *key_string = (char*)key; + char *value_string = (char*)value; + + g_free(key_string); + g_free(value_string); + + return TRUE; +} + +/* See if this is an include line, if it is open the file and call parse_config_file() */ +static gboolean parse_include_file(SnortConfig_t *snort_config, char *line, const char *config_directory, int recursion_level) +{ + int length; + int accumulated_length = 0; + char *include_filename; + + /* Look for "include " */ + char *include_token = read_token(line, ' ', &length, &accumulated_length, FALSE); + if (strlen(include_token) == 0) { + return FALSE; + } + if (strncmp(include_token, "include", 7) != 0) { + return FALSE; + } + + /* Read the filename */ + include_filename = read_token(line+accumulated_length, ' ', &length, &accumulated_length, FALSE); + if (include_filename != '\0') { + FILE *new_config_fd; + char substituted_filename[512]; + gboolean is_rule_file = FALSE; + + /* May need to substitute variables into include path. */ + if (strncmp(include_filename, "$RULE_PATH", 10) == 0) { + /* Write rule path variable value */ + /* Don't assume $RULE_PATH will end in a file separator */ + if (snort_config->rule_path_is_absolute) { + g_snprintf(substituted_filename, 512, "%s%s%s", + snort_config->rule_path, + g_file_separator, + include_filename + 10); + } + else { + g_snprintf(substituted_filename, 512, "%s%s%s%s%s", + config_directory, + g_file_separator, + snort_config->rule_path, + g_file_separator, + include_filename + 10); + } + is_rule_file = TRUE; + } + else { + /* No $RULE_PATH, just use directory and filename */ + g_snprintf(substituted_filename, 512, "%s/%s", config_directory, include_filename); + } + + /* Try to open the file. */ + snort_debug_printf("Trying to open: %s\n", substituted_filename); + new_config_fd = ws_fopen(substituted_filename, "r"); + if (new_config_fd == NULL) { + snort_debug_printf("Failed to open config file %s\n", substituted_filename); + return FALSE; + } + + /* Parse the file */ + if (is_rule_file) { + snort_config->stat_rules_files++; + } + parse_config_file(snort_config, new_config_fd, substituted_filename, config_directory, recursion_level + 1); + + /* Close the file */ + fclose(new_config_fd); + + return TRUE; + } + return FALSE; +} + +/* Process an individual option - i.e. the elements found between '(' and ')' */ +static void process_rule_option(Rule_t *rule, char *options, int option_start_offset, int options_end_offset, int colon_offset) +{ + static char name[1024], value[1024]; + name[0] = '\0'; + value[0] = '\0'; + gint value_length = 0; + guint32 value32 = 0; + + if (colon_offset != 0) { + /* Name and value */ + g_snprintf(name, colon_offset-option_start_offset, "%s", options+option_start_offset); + g_snprintf(value, options_end_offset-colon_offset, "%s", options+colon_offset); + value_length = (gint)strlen(value); + } + else { + /* Just name */ + g_snprintf(name, options_end_offset-option_start_offset, "%s", options+option_start_offset); + } + + /* Do this extraction in one place (may not be number but should be OK) */ + ws_strtoi32(value, (const gchar**)&value[value_length], &value32); + + /* Process the rule options that we are interested in */ + if (strcmp(name, "msg") == 0) { + rule->msg = g_strdup(value); + } + else if (strcmp(name, "sid") == 0) { + rule->sid = value32; + } + else if (strcmp(name, "rev") == 0) { + value32 = rule->rev; + } + else if (strcmp(name, "content") == 0) { + int value_start = 0; + + if (value_length < 3) { + return; + } + + /* Need to trim off " ", but first check for ! */ + if (value[0] == '!') { + value_start = 1; + if (value_length < 4) { + return; + } + } + + value[options_end_offset-colon_offset-2] = '\0'; + rule_add_content(rule, value+value_start+1, value_start == 1); + } + else if (strcmp(name, "uricontent") == 0) { + int value_start = 0; + + if (value_length < 3) { + return; + } + + /* Need to trim off " ", but first check for ! */ + if (value[0] == '!') { + value_start = 1; + if (value_length < 4) { + return; + } + } + + value[options_end_offset-colon_offset-2] = '\0'; + rule_add_uricontent(rule, value+value_start+1, value_start == 1); + } + else if (strcmp(name, "http_uri") == 0) { + rule_set_http_uri(rule); + } + else if (strcmp(name, "pcre") == 0) { + rule_add_pcre(rule, value); + } + else if (strcmp(name, "nocase") == 0) { + rule_set_content_nocase(rule); + } + else if (strcmp(name, "offset") == 0) { + rule_set_content_offset(rule, value32); + } + else if (strcmp(name, "depth") == 0) { + rule_set_content_depth(rule, value32); + } + else if (strcmp(name, "within") == 0) { + rule_set_content_within(rule, value32); + } + else if (strcmp(name, "distance") == 0) { + rule_set_content_distance(rule, value32); + } + else if (strcmp(name, "fast_pattern") == 0) { + rule_set_content_fast_pattern(rule); + } + else if (strcmp(name, "http_method") == 0) { + rule_set_content_http_method(rule); + } + else if (strcmp(name, "http_client_body") == 0) { + rule_set_content_http_client_body(rule); + } + else if (strcmp(name, "http_cookie") == 0) { + rule_set_content_http_cookie(rule); + } + + else if (strcmp(name, "classtype") == 0) { + rule_set_classtype(rule, value); + } + else if (strcmp(name, "reference") == 0) { + rule_add_reference(rule, value); + } + else { + /* Ignore an option we don't currently handle */ + } +} + +/* Parse a Snort alert, return TRUE if successful */ +static gboolean parse_rule(SnortConfig_t *snort_config, char *line, const char *filename, int line_number, int line_length) +{ + char *options_start; + char *options; + gboolean in_quotes = FALSE; + int options_start_index = 0, options_index = 0, colon_offset = 0; + char c; + int length; + Rule_t *rule = NULL; + + /* Rule will begin with alert */ + if (strncmp(line, "alert ", 6) != 0) { + return FALSE; + } + + /* Allocate the rule itself */ + rule = (Rule_t*)g_malloc(sizeof(Rule_t)); + + snort_debug_printf("looks like a rule: %s\n", line); + memset(rule, 0, sizeof(Rule_t)); + + rule->rule_string = g_strdup(line); + rule->file = g_strdup(filename); + rule->line_number = line_number; + + /* Next token is the protocol */ + rule->protocol = read_token(line+6, ' ', &length, &length, TRUE); + + /* Find start of options. */ + options_start = strstr(line, "("); + if (options_start == NULL) { + snort_debug_printf("start of options not found\n"); + return FALSE; + } + options_index = (int)(options_start-line) + 1; + + /* To make parsing simpler, replace final ')' with ';' */ + if (line[line_length-1] != ')') { + g_free(rule); + return FALSE; + } + else { + line[line_length-1] = ';'; + } + + /* Now look for next ';', process one option at a time */ + options = &line[options_index]; + options_index = 0; + + while ((c = options[options_index++])) { + if (c == '"') { + in_quotes = !in_quotes; + } + /* Ignore ; or ; if inside quotes */ + if (!in_quotes) { + if (c == ':') { + colon_offset = options_index; + } + if (c == ';') { + /* End of current option - add to rule. */ + process_rule_option(rule, options, options_start_index, options_index, colon_offset); + + /* Skip any spaces before next option */ + while (options[options_index] == ' ') options_index++; + + /* Next rule will start here */ + options_start_index = options_index; + colon_offset = 0; + in_quotes = FALSE; + } + } + } + + /* Add rule to map of rules. */ + g_hash_table_insert(snort_config->rules, GUINT_TO_POINTER((guint)rule->sid), rule); + + return TRUE; +} + +/* Delete an individual rule */ +static gboolean delete_rule(gpointer key _U_, + gpointer value, + gpointer user_data _U_) +{ + Rule_t *rule = (Rule_t*)value; + unsigned int n; + + snort_debug_printf("delete_rule(value=%p)\n", value); + + /* Delete strings on heap. */ + g_free(rule->rule_string); + g_free(rule->file); + g_free(rule->msg); + g_free(rule->classtype); + g_free(rule->protocol); + + for (n=0; n < rule->number_contents; n++) { + g_free(rule->contents[n].str); + g_free(rule->contents[n].binary_str); + } + + for (n=0; n < rule->number_references; n++) { + g_free(rule->references[n]); + } + + snort_debug_printf("Freeing rule at :%p\n", rule); + g_free(rule); + return TRUE; +} + + +/* Create a new config, starting with the given snort config file. */ +/* N.B. using recursion_level to limit stack depth. */ +static void parse_config_file(SnortConfig_t *snort_config, FILE *config_file_fd, + const char *filename, const char *dirname, int recursion_level) +{ + #define MAX_LINE_LENGTH 4096 + char line[MAX_LINE_LENGTH]; + int line_number = 0; + + snort_debug_printf("parse_config_file(filename=%s, recursion_level=%d)\n", filename, recursion_level); + + if (recursion_level > 8) { + return; + } + + /* Read each line of the file in turn, and see if we want any info from it. */ + while (fgets(line, MAX_LINE_LENGTH, config_file_fd)) { + + int line_length; + ++line_number; + + /* Nothing interesting to parse */ + if ((line[0] == '\0') || (line[0] == '#')) { + continue; + } + + /* Trim newline from end */ + line_length = (int)strlen(line); + while (line_length && ((line[line_length - 1] == '\n') || (line[line_length - 1] == '\r'))) { + --line_length; + } + line[line_length] = '\0'; + if (line_length == 0) { + continue; + } + + /* Offer line to the various parsing functions. Could optimise order.. */ + if (parse_variables_line(snort_config, line)) { + continue; + } + if (parse_references_prefix_file_line(snort_config, line)) { + continue; + } + if (parse_include_file(snort_config, line, dirname, recursion_level)) { + continue; + } + if (parse_rule(snort_config, line, filename, line_number, line_length)) { + snort_config->stat_rules++; + continue; + } + } +} + + + +/* Create the global ConfigParser */ +void create_config(SnortConfig_t **snort_config, const char *snort_config_file) +{ + gchar* dirname; + gchar* basename; + FILE *config_file_fd; + + snort_debug_printf("create_config (%s)\n", snort_config_file); + + *snort_config = (SnortConfig_t*)g_malloc(sizeof(SnortConfig_t)); + memset(*snort_config, 0, sizeof(SnortConfig_t)); + + /* Create rule table */ + (*snort_config)->rules = g_hash_table_new(g_direct_hash, g_direct_equal); + + /* Create reference prefix table */ + (*snort_config)->references_prefixes = g_hash_table_new(string_hash, string_equal); + + /* Vars tables */ + (*snort_config)->vars = g_hash_table_new(string_hash, string_equal); + (*snort_config)->ipvars = g_hash_table_new(string_hash, string_equal); + (*snort_config)->portvars = g_hash_table_new(string_hash, string_equal); + + /* Extract separate directory and filename. */ + dirname = g_path_get_dirname(snort_config_file); + basename = g_path_get_basename(snort_config_file); + + /* Attempt to open the config file */ + config_file_fd = ws_fopen(snort_config_file, "r"); + if (config_file_fd == NULL) { + snort_debug_printf("Failed to open config file %s\n", snort_config_file); + return; + } + + /* Start parsing from the top-level config file. */ + parse_config_file(*snort_config, config_file_fd, snort_config_file, dirname, 0); + + g_free(dirname); + g_free(basename); + + fclose(config_file_fd); +} + + +/* Delete the entire config */ +void delete_config(SnortConfig_t **snort_config) +{ + snort_debug_printf("delete_config()\n"); + + /* Iterate over all rules, freeing each one! */ + g_hash_table_foreach_remove((*snort_config)->rules, delete_rule, NULL); + g_hash_table_destroy((*snort_config)->rules); + + /* References table */ + g_hash_table_foreach_remove((*snort_config)->references_prefixes, delete_string_entry, NULL); + g_hash_table_destroy((*snort_config)->references_prefixes); + + /* Free up variable tables */ + g_hash_table_foreach_remove((*snort_config)->vars, delete_string_entry, NULL); + g_hash_table_destroy((*snort_config)->vars); + g_hash_table_foreach_remove((*snort_config)->ipvars, delete_string_entry, NULL); + g_hash_table_destroy((*snort_config)->ipvars); + g_hash_table_foreach_remove((*snort_config)->portvars, delete_string_entry, NULL); + g_hash_table_destroy((*snort_config)->portvars); + + g_free(*snort_config); + + *snort_config = NULL; +} + +/* Look for a rule corresponding to the given SID */ +Rule_t *get_rule(SnortConfig_t *snort_config, guint32 sid) +{ + if ((snort_config == NULL) || (snort_config->rules == NULL)) { + return NULL; + } + else { + return (Rule_t*)g_hash_table_lookup(snort_config->rules, GUINT_TO_POINTER(sid)); + } +} + +/* Fetch some statistics. */ +void get_global_rule_stats(SnortConfig_t *snort_config, unsigned int sid, + unsigned int *number_rules_files, unsigned int *number_rules, + unsigned int *alerts_detected, unsigned int *this_rule_alerts_detected) +{ + *number_rules_files = snort_config->stat_rules_files; + *number_rules = snort_config->stat_rules; + *alerts_detected = snort_config->stat_alerts_detected; + Rule_t *rule; + + /* Look up rule and get current/total matches */ + rule = get_rule(snort_config, sid); + if (rule) { + *this_rule_alerts_detected = rule->matches_seen; + } + else { + *this_rule_alerts_detected = 0; + } +} + +/* Reset stats on individual rule */ +static void reset_rule_stats(gpointer key _U_, + gpointer value, + gpointer user_data _U_) +{ + Rule_t *rule = (Rule_t*)value; + rule->matches_seen = 0; +} + +void reset_global_rule_stats(SnortConfig_t *snort_config) +{ + /* Reset global stats */ + if (snort_config == NULL) { + return; + } + snort_config->stat_alerts_detected = 0; + + /* Iterate over all rules, resetting the stats of each */ + g_hash_table_foreach(snort_config->rules, reset_rule_stats, NULL); +} + + +/*************************************************************************************/ +/* Dealing with content fields and trying to find where it matches within the packet */ +/* Parse content strings to interpret binary and escaped characters. Do this */ +/* so we can look for in frame using memcmp(). */ +static unsigned char content_get_nibble_value(char c) +{ + static unsigned char values[256]; + static gboolean values_set = FALSE; + + if (!values_set) { + /* Set table once and for all */ + unsigned char ch; + for (ch='a'; ch <= 'f'; ch++) { + values[ch] = 0xa + (ch-'a'); + } + for (ch='A'; ch <= 'F'; ch++) { + values[ch] = 0xa + (ch-'A'); + } + for (ch='0'; ch <= '9'; ch++) { + values[ch] = (ch-'0'); + } + values_set = TRUE; + } + + return values[(unsigned char)c]; +} + +/* Go through string, converting hex digits into guint8, and removing escape characters. */ +guint content_convert_to_binary(content_t *content) +{ + int output_idx = 0; + gboolean in_binary_mode = FALSE; /* Are we in a binary region of the string? */ + gboolean have_one_nibble = FALSE; /* Do we have the first nibble of the pair needed to make a byte? */ + unsigned char one_nibble = 0; /* Value of first nibble if we have it */ + char c; + int n; + gboolean have_backslash = FALSE; + static gchar binary_str[1024]; + + /* Just return length if have previously translated in binary string. */ + if (content->translated) { + return content->translated_length; + } + + /* Walk over each character, work out what needs to be written into output */ + for (n=0; content->str[n] != '\0'; n++) { + c = content->str[n]; + if (c == '|') { + /* Flip binary mode */ + in_binary_mode = !in_binary_mode; + continue; + } + + if (!in_binary_mode) { + /* Not binary mode. Copying characters into output buffer, but watching out for escaped chars. */ + if (!have_backslash) { + if (c == '\\') { + /* Just note that we have a backslash */ + have_backslash = TRUE; + continue; + } + else { + /* Just copy the character straight into output. */ + binary_str[output_idx++] = (unsigned char)c; + } + } + else { + /* Currently have a backslash. Reset flag. */ + have_backslash = 0; + /* Just copy the character into output. Really, the only characters that should be escaped + are ';' and '\' and '"' */ + binary_str[output_idx++] = (unsigned char)c; + } + } + else { + /* Binary mode. Handle pairs of hex digits and translate into guint8 */ + if (c == ' ') { + /* Ignoring inside binary mode */ + continue; + } + else { + unsigned char nibble = content_get_nibble_value(c); + if (!have_one_nibble) { + /* Store first nibble of a pair */ + one_nibble = nibble; + have_one_nibble = TRUE; + } + else { + /* Combine both nibbles into a byte */ + binary_str[output_idx++] = (one_nibble << 4) + nibble; + /* Reset flag - looking for new pair of nibbles */ + have_one_nibble = FALSE; + } + } + } + } + + /* Store result for next time. */ + content->binary_str = (guchar*)g_malloc(output_idx+1); + memcpy(content->binary_str, binary_str, output_idx+1); + content->translated = TRUE; + content->translated_length = output_idx; + + return output_idx; +} + +/* + * Editor modelines - http://www.wireshark.org/tools/modelines.html + * + * Local variables: + * c-basic-offset: 4 + * tab-width: 8 + * indent-tabs-mode: nil + * End: + * + * vi: set shiftwidth=4 tabstop=8 expandtab: + * :indentSize=4:tabSize=8:noTabs=true: + */ diff --git a/epan/dissectors/snort-config.h b/epan/dissectors/snort-config.h new file mode 100644 index 0000000000..ec0c23c761 --- /dev/null +++ b/epan/dissectors/snort-config.h @@ -0,0 +1,194 @@ +/* snort-config.h + * + * Copyright 2016, Martin Mathieson + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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. + */ + + +#include <glib.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifndef SNORT_CONFIG_H +#define SNORT_CONFIG_H + +/************************************************************************/ +/* Rule related data types */ + +typedef enum content_type_t { + Content, + UriContent, + Pcre +} content_type_t; + +/* Content (within an alert/rule) */ +typedef struct content_t { + /* Details as parsed from rule */ + content_type_t content_type; + + char *str; + gboolean negation; /* i.e. pattern must not appear */ + gboolean nocase; /* when set, do case insensitive match */ + + gboolean offset_set; /* Where to start looking within packet. -65535 -> 65535 */ + gint offset; + + guint depth; /* How far to look into packet. Can't be 0 */ + + gboolean distance_set; + gint distance; /* Same as offset but relative to last match. -65535 -> 65535 */ + + guint within; /* Most bytes from end of previous match. Max 65535 */ + + gboolean fastpattern; /* Is most distinctive content in rule */ + + /* http preprocessor modifiers */ + gboolean http_method; + gboolean http_client_body; + gboolean http_cookie; + + /* Pattern converted into bytes for matching against packet */ + guchar *binary_str; + gboolean translated; + guint translated_length; +} content_t; + +/* This is to keep track of a variable referenced by a rule */ +typedef struct used_variable_t { + char *name; + char *value; +} used_variable_t; + +/* The collection of variables referenced by a rule */ +typedef struct relevant_vars_t { + gboolean relevant_vars_set; + + #define MAX_RULE_PORT_VARS 6 + guint num_port_vars; + used_variable_t port_vars[MAX_RULE_PORT_VARS]; + + #define MAX_RULE_IP_VARS 6 + guint num_ip_vars; + used_variable_t ip_vars[MAX_RULE_IP_VARS]; + +} relevant_vars_t; + + +/* This is purely the information parsed from the config */ +typedef struct Rule_t { + + char *rule_string; /* The whole rule as read from the rule file */ + char *file; /* Name of the rule file */ + guint line_number; /* Line number of rule within rule file */ + + char *msg; /* Description of the rule */ + char *classtype; + guint32 sid, rev; + + char *protocol; + + /* content strings to match on */ + unsigned int number_contents; +#define MAX_CONTENT_ENTRIES 30 + content_t contents[MAX_CONTENT_ENTRIES]; + + /* Keep this pointer so can update attributes as parse modifier options */ + content_t *last_added_content; + + /* References describing the rule */ + unsigned int number_references; +#define MAX_REFERENCE_ENTRIES 20 + char *references[MAX_REFERENCE_ENTRIES]; + + relevant_vars_t relevant_vars; + + /* Statistics */ + guint matches_seen; +} Rule_t; + + + +/* Whole global snort config as learned by parsing config files */ +typedef struct SnortConfig_t +{ + /* Variables (var, ipvar, portvar) */ + GHashTable *vars; + GHashTable *ipvars; + GHashTable *portvars; + + char *rule_path; + gboolean rule_path_is_absolute; + + /* (sid -> Rule_t*) table */ + GHashTable *rules; + /* Reference (web .link) prefixes */ + GHashTable *references_prefixes; + + /* Statistics (that may be reset) */ + guint stat_rules_files; + guint stat_rules; + guint stat_alerts_detected; + +} SnortConfig_t; + + +/*************************************************************************************/ +/* API functions */ +void create_config(SnortConfig_t **snort_config, const char *snort_config_file); +void delete_config(SnortConfig_t **snort_config); + +/* Look up rule by SID */ +Rule_t *get_rule(SnortConfig_t *snort_config, guint32 sid); +void rule_set_alert(SnortConfig_t *snort_config, Rule_t *rule, guint *global_match_number, guint *rule_match_number); + +/* Debug only */ +void rule_print(Rule_t *rule); + +/* IP and port vars */ +void rule_set_relevant_vars(SnortConfig_t *snort_config, Rule_t *rule); + +/* Substitute prefix (from reference.config) into reference string */ +char *expand_reference(SnortConfig_t *snort_config, char *reference); + +/* Rule stats */ +void get_global_rule_stats(SnortConfig_t *snort_config, unsigned int sid, + unsigned int *number_rules_files, unsigned int *number_rules, + unsigned int *alerts_detected, unsigned int *this_rule_alerts_detected); +void reset_global_rule_stats(SnortConfig_t *snort_config); + +/* Expanding a content field string to the expected binary bytes */ +guint content_convert_to_binary(content_t *content); + +#endif + +/* + * Editor modelines - http://www.wireshark.org/tools/modelines.html + * + * Local variables: + * c-basic-offset: 4 + * tab-width: 8 + * indent-tabs-mode: nil + * End: + * + * vi: set shiftwidth=4 tabstop=8 expandtab: + * :indentSize=4:tabSize=8:noTabs=true: + */ |