diff --git a/policy/dns-ext.bro b/policy/dns-ext.bro new file mode 100644 index 0000000000..58b41976f8 --- /dev/null +++ b/policy/dns-ext.bro @@ -0,0 +1,213 @@ +@load global-ext +@load dns + +type dns_ext_session_info: record { + id: conn_id; + ts: time; + trans_id: count; + query: string; + qtype: count; + qclass: count; + total_answers: count &default=0; + rcode: count &default = 65536; + QR: bool &default=F; + Z: bool &default=F; + AA: bool &default=F; + RD: bool &default=F; + RA: bool &default=F; + TC: bool &default=F; + TTL: interval &default=0secs; + replies: set[string]; +}; + +# Define the generic dns-ext event that can be handled from other scripts +global dns_ext: event(id: conn_id, di: dns_ext_session_info); + +module DNS; + +export { + # Follow this example to define domains that you would consider "local". + # This is primarily useful if you are monitoring authoritative nameservers, + # but also useful for any zones that *should* be pointing at your + # network. + # e.g. + #const local_domains = /(^|\.)(osu|ohio-state)\.edu$/ | + # /(^|\.)akamai(tech)?\.net$/ &redef; + const local_domains = /(^|\.)akamai(tech)?\.net$/ &redef; + + redef enum Notice += { + # Raised when a non-local name is found to be pointing at a local host. + # This only works appropriately when all of your authoritative DNS + # servers are located in your "local_nets". + DNSExternalName, + }; +} + + +global dns_sessions_ext: table[addr, addr, count] of dns_ext_session_info; + +# This doesn't work with live traffic yet. +# It's waiting for support to dynamically construct pattern variables at init time. +#global dns_suffix_regex = build_regex(local_domains, "(^|\.)~~$"); +#event bro_init() +# { +# local i: count = 0; +# local tmp_pattern: pattern; +# for ( d in local_domains ) +# { +# tmp_pattern = string_to_pattern( fmt("=%s@", d), T ); +# +# if ( i == 0 ) +# pat = tmp_pattern; +# else +# pat = merge_pattern(tmp_pattern, pat); +# ++i; +# } +# } + +event expire_DNS_session_ext(orig: addr, resp: addr, trans_id: count) + { + if ( [orig, resp, trans_id] in dns_sessions_ext ) + { + local session = dns_sessions[orig, resp, trans_id]; + local session_ext = dns_sessions_ext[orig, resp, trans_id]; + + event dns_ext(session_ext$id, session_ext); + } + } + +event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count) + { + local id = c$id; + local orig = id$orig_h; + local resp = id$resp_h; + local session = lookup_DNS_session(c, msg$id); + local session_ext: dns_ext_session_info; + if ( [orig, resp, msg$id] !in dns_sessions_ext ) + { + session_ext$id = c$id; + session_ext$trans_id = msg$id; + session_ext$ts = network_time(); + session_ext$RD = msg$RD; + session_ext$TC = msg$TC; + session_ext$qtype = qtype; + session_ext$qclass = qclass; + session_ext$query = query; + local strings: set[string] = set(); + session_ext$replies = strings; + dns_sessions_ext[orig, resp, msg$id] = session_ext; + + # This needs to expire before the original dns.bro script expires the + # the data from the dns_session variable. + schedule 14secs { expire_DNS_session_ext(orig, resp, msg$id) }; + } + } + + +event dns_A_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr) + { + local id = c$id; + local orig = id$orig_h; + local resp = id$resp_h; + local session = lookup_DNS_session(c, msg$id); + local session_ext: dns_ext_session_info; + + if ( [orig, resp, msg$id] in dns_sessions_ext ) + { + session_ext = dns_sessions_ext[orig, resp, msg$id]; + add session_ext$replies[fmt("%s",a)]; + session_ext$RA = msg$RA; + session_ext$TTL = ans$TTL; + session_ext$rcode = msg$rcode; + } + + # Check for out of place domain names + if ( is_local_addr(a) && # referring to a local host + !is_local_addr(c$id$resp_h) && # response from a remote host + local_domains !in ans$query ) # drop known names + { + NOTICE([$note=DNSExternalName, + $msg=fmt("%s is pointing to a local host - %s.", ans$query, a), + $conn=c]); + } + } + +event dns_TXT_reply(c: connection, msg: dns_msg, ans: dns_answer, str: string) + { + local id = c$id; + local orig = id$orig_h; + local resp = id$resp_h; + local session = lookup_DNS_session(c, msg$id); + local session_ext: dns_ext_session_info; + + if ( [orig, resp, msg$id] in dns_sessions_ext ) + { + session_ext = dns_sessions_ext[orig, resp, msg$id]; + session_ext$rcode = msg$rcode; + add session_ext$replies[str]; + } + } + +event dns_AAAA_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr, + astr: string) + { + local id = c$id; + local orig = id$orig_h; + local resp = id$resp_h; + local session = lookup_DNS_session(c, msg$id); + local session_ext: dns_ext_session_info; + + if ( [orig, resp, msg$id] in dns_sessions_ext ) + { + session_ext = dns_sessions_ext[orig, resp, msg$id]; + session_ext$rcode = msg$rcode; + add session_ext$replies[fmt("%s", a)]; + } + } + + +event dns_MX_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string, + preference: count) + { + local id=c$id; + local session = lookup_DNS_session(c, msg$id); + local session_ext: dns_ext_session_info; + + if ( [id$orig_h, id$resp_h, msg$id] in dns_sessions_ext ) + { + session_ext = dns_sessions_ext[id$orig_h, id$resp_h, msg$id]; + session_ext$rcode = msg$rcode; + add session_ext$replies[name]; + } + } + +event dns_PTR_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) + { + local id = c$id; + local orig = id$orig_h; + local resp = id$resp_h; + local session = lookup_DNS_session(c, msg$id); + local session_ext: dns_ext_session_info; + + if ( [orig, resp, msg$id] in dns_sessions_ext ) + { + session_ext = dns_sessions_ext[orig, resp, msg$id]; + session_ext$rcode = msg$rcode; + add session_ext$replies[name]; + } + } + +event dns_end(c: connection, msg: dns_msg) + { + local id = c$id; + local orig = id$orig_h; + local resp = id$resp_h; + local session = lookup_DNS_session(c, msg$id); + local session_ext: dns_ext_session_info; + + if ( [orig, resp, msg$id] in dns_sessions_ext ) + { + session_ext = dns_sessions_ext[orig, resp, msg$id]; + session_ext$rcode = msg$rcode; + } + } diff --git a/policy/functions.bro b/policy/functions.bro new file mode 100644 index 0000000000..5a7685feeb --- /dev/null +++ b/policy/functions.bro @@ -0,0 +1,268 @@ +function numeric_id_string(id: conn_id): string + { + return fmt("%s:%d > %s:%d", + id$orig_h, id$orig_p, + id$resp_h, id$resp_p); + } + +function fmt_addr_set(input: addr_set): string + { + local output = ""; + local tmp = ""; + local len = length(input); + local i = 1; + + for ( item in input ) + { + tmp = fmt("%s", item); + if ( len != i ) + tmp = fmt("%s ", tmp); + i = i+1; + output = fmt("%s%s", output, tmp); + } + return fmt("%s", output); + } + +function fmt_str_set(input: string_set, strip: pattern): string + { + local len = length(input); + if ( len == 0 ) + return "{}"; + + local output = "{"; + local tmp = ""; + local i = 1; + + for ( item in input ) + { + tmp = fmt("%s", gsub(item, strip, "")); + if ( len != i ) + tmp = fmt("%s, ", tmp); + i = i+1; + output = fmt("%s%s", output, tmp); + } + return fmt("%s}", output); + } + +# Regular expressions for matching IP addresses in strings. +const ipv4_addr_regex = /[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}/; +const ipv6_8hex_regex = /([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}/; +const ipv6_compressed_hex_regex = /(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)::(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)/; +const ipv6_hex4dec_regex = /(([0-9A-Fa-f]{1,4}:){6,6})([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/; +const ipv6_compressed_hex4dec_regex = /(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)::(([0-9A-Fa-f]{1,4}:)*)([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/; + +# These are only commented out until this bug is fixed: +# http://www.bro-ids.org/wiki/index.php/Known_Issues#Bug_with_OR-ing_together_pattern_variables +#const ipv6_addr_regex = ipv6_8hex_regex | +# ipv6_compressed_hex_regex | +# ipv6_hex4dec_regex | +# ipv6_compressed_hex4dec_regex; +#const ip_addr_regex = ipv4_addr_regex | ipv6_addr_regex; + +const ipv6_addr_regex = + /([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}/ | + /(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)::(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)/ | # IPv6 Compressed Hex + /(([0-9A-Fa-f]{1,4}:){6,6})([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/ | # 6Hex4Dec + /(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)::(([0-9A-Fa-f]{1,4}:)*)([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/; # CompressedHex4Dec + +const ip_addr_regex = + /[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}/ | + /([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}/ | + /(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)::(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)/ | # IPv6 Compressed Hex + /(([0-9A-Fa-f]{1,4}:){6,6})([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/ | # 6Hex4Dec + /(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4})*)?)::(([0-9A-Fa-f]{1,4}:)*)([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/; # CompressedHex4Dec + +function is_valid_ip(ip_str: string): bool + { + if ( ip_str == ipv4_addr_regex ) + { + local octets = split(ip_str, /\./); + if ( |octets| != 4 ) + return F; + + local num=0; + for ( i in octets ) + { + num = to_count(octets[i]); + if ( num < 0 || 255 < num ) + return F; + } + return T; + } + else if ( ip_str == ipv6_addr_regex ) + { + # TODO: make this work correctly. + return T; + } + return F; + } + +# This outputs a string_array of ip addresses extracted from a string. +# given: "this is 1.1.1.1 a test 2.2.2.2 string with ip addresses 3.3.3.3" +# outputs: { [1] = 1.1.1.1, [2] = 2.2.2.2, [3] = 3.3.3.3 } +function find_ip_addresses(input: string): string_array + { + local parts = split_all(input, ip_addr_regex); + local output: string_array; + + for ( i in parts ) + { + if ( i % 2 == 0 && is_valid_ip(parts[i]) ) + output[|output|+1] = parts[i]; + } + return output; + } + +######################################## +# The following functions are for getting contact information (email) for an +# IP address in your organization. It will return data from nested subnets +# as well. +# Below is an example for using it: +# +# redef subnet_to_admin_table += { +# [146.128.0.0/16] = "security@yourorg.com", +# [146.128.94.0/24] = "someone@yourorg.com", +# }; +# +# event bro_init() +# { +# print fmt_email_string(find_all_emails(146.128.94.3)); +# => "security@yourorg.com, someone@yourorg.com" +# print fmt_email_string(find_all_emails(146.128.3.3)); +# => "security@yourorg.com" +# } +######################################## +# TODO: make this work with IPv6 +function find_all_emails(ip: addr): set[string] + { + if ( ip !in subnet_to_admin_table ) return set(); + + local output_values: set[string] = set(); + local tmp_ip: addr; + local i: count; + local emails: string; + for ( i in one_to_32 ) + { + tmp_ip = mask_addr(ip, one_to_32[i]); + emails = subnet_to_admin_table[tmp_ip]; + if ( emails != "" ) + add output_values[emails]; + } + return output_values; + } + +function fmt_email_string(emails: set[string]): string + { + local output=""; + for( email in emails ) + { + if ( output == "" ) + output = email; + else + output = fmt("%s, %s", output, email); + } + return output; + } + +######################################## + +# Get a software version instance full of zeros. +function get_default_software_version(): software_version + { + local tmp_int: int = 0; + local tmp_v: software_version = [$major=tmp_int, + $minor=tmp_int, + $minor2=tmp_int, + $addl=""]; + return tmp_v; + } + +function default_software_parsing(sw: string): software + { + local software_name = ""; + local v = get_default_software_version(); + + # The regular expression should match the complete version number + local version_parts = split_all(sw, /[0-9\-\._]{2,}/); + if ( |version_parts| >= 2 ) + { + # Remove the name/version separator + software_name = sub(version_parts[1], /.$/, ""); + local version_numbers = split_n(version_parts[2], /[\-\._[:blank:]]/, F, 4); + if ( |version_numbers| >= 4 ) + v$addl = version_numbers[4]; + if ( |version_numbers| >= 3 ) + v$minor2 = to_int(version_numbers[3]); + if ( |version_numbers| >= 2 ) + v$minor = to_int(version_numbers[2]); + if ( |version_numbers| >= 1 ) + v$major = to_int(version_numbers[1]); + } + local this_software: software = [$name=software_name, $version=v]; + return this_software; + } + +type track_count: record { + n: count &default=0; + index: count &default=0; +}; + +function default_track_count(a: addr): track_count + { + local x: track_count; + return x; + } + +const default_notice_thresholds: vector of count = { + 30, 100, 1000, 10000, 100000, 1000000, 10000000, +} &redef; + +# This is total rip off from scan.bro, but placed in the global namespace +# and slightly reworked to be easier to work with and more general. +function check_threshold(v: vector of count, tracker: track_count): bool + { + if ( tracker$index <= |v| && tracker$n >= v[tracker$index] ) + { + ++tracker$index; + return T; + } + return F; + } + +function default_check_threshold(tracker: track_count): bool + { + return check_threshold(default_notice_thresholds, tracker); + } + +# This can be used for &default values on tables when the index is an addr. +function addr_empty_string_set(a: addr): set[string] + { + return set(); + } + +# Some enums for deciding what and when to log. +type Directions_and_Hosts: enum { + Inbound, Outbound, + LocalHosts, RemoteHosts, + Enabled, Disabled +}; +const DIRECTIONS = set(Inbound, Outbound, Enabled, Disabled); +const HOSTS = set(LocalHosts, RemoteHosts, Enabled, Disabled); + +function id_matches_directions(id: conn_id, d: Directions_and_Hosts): bool + { + if ( d == Disabled ) return F; + + return ( d == Enabled || + (d == Outbound && is_local_addr(id$orig_h)) || + (d == Inbound && is_local_addr(id$resp_h)) ); + } + +function addr_matches_hosts(ip: addr, h: Directions_and_Hosts): bool + { + if ( h == Disabled ) return F; + + return ( h == Enabled || + (h == LocalHosts && is_local_addr(ip)) || + (h == RemoteHosts && !is_local_addr(ip)) ); + } diff --git a/policy/global.bro b/policy/global.bro new file mode 100644 index 0000000000..d459b5e83a --- /dev/null +++ b/policy/global.bro @@ -0,0 +1,33 @@ +@load site +@load signatures + +const private_address_space: set[subnet] = {10.0.0.0/8, 192.168.0.0/16, 127.0.0.0/8, 172.16.0.0/12}; + +# These go along with the functions in functions-ext.bro for mapping IP +# addresses to email contact information for IP addresses and subnets. +const one_to_32: vector of count = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32}; +const subnet_to_admin_table: table[subnet] of string = { + [0.0.0.0/0] = "", +} &redef; + +############################################################### +# Make the default signature action ignore certain signatures +############################################################### +const ignored_signatures = /INTENTIONALLY_BLANK/ &redef; +function default_signature_action(sig: string): SigAction + { + if ( ignored_signatures in sig ) + return SIG_IGNORE; + else + return SIG_ALARM; + } +redef signature_actions &default=default_signature_action; +############################################################### + +# This defines the event that is used by the bro-dblogger application +# to push data from Bro directly into a database. +# see: http://github.com/sethhall/bro-dblogger +global db_log: event(db_table: string, data: any); + +@load functions-ext +@load logging-ext diff --git a/policy/logging.dns-ext.bro b/policy/logging.dns-ext.bro new file mode 100644 index 0000000000..7ee5887db0 --- /dev/null +++ b/policy/logging.dns-ext.bro @@ -0,0 +1,82 @@ +@load global-ext +@load dns-ext + +module DNS; + +export { + # Which DNS queries to record. + # Choices are: Inbound, Outbound, Enabled, Disabled + const query_logging = Enabled &redef; + + # If set to T, this will split inbound and outbound requests + # into separate files. F merges everything into a single file. + const split_log_file = F &redef; + + # Make this value true to reduce the logs to only what's being + # queried for + const minimal_logging = F &redef; +} + +event bro_init() + { + LOG::create_logs("dns-ext", query_logging, split_log_file, T); + + if ( minimal_logging ) + LOG::define_header("dns-ext", cat_sep("\t", "\\N", + "ts", "orig_h", + "query_type", "query")); + else + LOG::define_header("dns-ext", cat_sep("\t", "", + "ts", + "orig_h", "orig_p", + "resp_h", "resp_p", + "proto", "query_type", "query_class", + "query", "transaction_id", + "ttl", "flags", "error", "replies")); + + } + +event dns_ext(id: conn_id, di: dns_ext_session_info) &priority=-10 + { + local log = LOG::get_file_by_id("dns-ext", id, F); + + if ( minimal_logging ) + { + print log, cat_sep("\t", "\\N", + di$ts, + id$orig_h, + query_types[di$qtype], + di$query); + } + else + { + local flags: set[string]; + if ( di$RD ) + add flags["RD"]; + if ( di$RA ) + add flags["RA"]; + if ( di$TC ) + add flags["TC"]; + if ( di$QR ) + add flags["QR"]; + if ( di$Z ) + add flags["Z"]; + if ( di$AA ) + add flags["AA"]; + + print log, cat_sep("\t", "\\N", + di$ts, + id$orig_h, port_to_count(id$orig_p), + id$resp_h, port_to_count(id$resp_p), + get_port_transport_proto(id$resp_p), + query_types[di$qtype], + dns_class[di$qclass], + di$query, + fmt("%04x", di$trans_id), + fmt("%.0f", interval_to_double(di$TTL)), + fmt_str_set(flags, /!!!!/), + base_error[di$rcode], + fmt_str_set(di$replies, /!!!!/) + ); + } + } \ No newline at end of file