# # $Id: dnsbot.bro # # Lets look for DNS controlled bots # These bots use dns queries for .evil.com # to get messages back and forth. # # in addition we now check for A record responses that return # a normally non-routable address space answer - typically 127.0.0.0/8 . # such responses are put into a table, then tracked so that later answers are # also flagged and the whole mess reported. @load site @load dns-info @load conn @load dns redef enum Notice += { DNS_SuspicousQuery, # domain lookup of my.domain.foo.com DNS_NoRouteLookup, # A record response of 127.0.0.0 DNS_NoRouteChange, # chenge from 127.0.0.0 -> A.B.C.D }; # DPM configuration. global dns_ports = { 53/udp, 53/tcp, 137/udp }; redef dpd_config += { [ANALYZER_DNS] = [$ports = dns_ports] }; global dns_udp_ports = { 53/udp, 137/udp }; global dns_tcp_ports = { 53/tcp }; redef dpd_config += { [ANALYZER_DNS_UDP_BINPAC] = [$ports = dns_udp_ports] }; redef dpd_config += { [ANALYZER_DNS_TCP_BINPAC] = [$ports = dns_tcp_ports] }; # my local domain const local_domain = /nersc\.gov/; # domains to skip (blacklisters) & associated domains # this will also be the DNS_NoRoute filter as well const blacklisters = /rfc-ignorant\.org/ | /surbl\.org/ | /ahbl\.org/ | /securitysage\.com/ | /berkeley.edu/ | /lbl.gov/; const noroute_lookup_nets: set[subnet] &redef; global noroute_state: table[string] of addr; redef noroute_lookup_nets += { 127.0.0.0/8, 192.168.0.0/16, }; #global dnsbot_file = open_log_file("dnsbot") &redef; # make sure we are listening to the correct ports redef capture_filters += { ["dns"] = "port 53", ["netbios-ns"] = "udp port 137", }; # ripped from dns.bro function second_level_domain(name: string): string { local split_on_dots = split(name, /\./); local num_dots = length(split_on_dots); if ( num_dots <= 1 ) return name; return fmt("%s.%s", split_on_dots[num_dots-1], split_on_dots[num_dots]); } event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count) { local orig = c$id$orig_h; if ( is_local_addr(orig) ) { # if its a lookup for an Authority or Mail record... # I suppose they could hide in TXT or almost any part....grrr if (query_types[qtype] == "A" || query_types[qtype] == "MX") { local split_on_dots = split(query, /\./); local num_dots = length(split_on_dots); # can't fit a FQDN + evil.com in here if (num_dots <= 4) return; if ( local_domain in query ) { local sl_query = second_level_domain(query); if (local_domain in sl_query || blacklisters in sl_query) { # just a misconfigured host or blacklister return; } else { #local outmsg = fmt("%.6f %s suspious %s query %s",network_time(), # full_id_string(c), query_types[qtype], query); NOTICE([$note=DNS_SuspicousQuery, $conn=c, $msg=fmt("%s suspicous query %s %s", full_id_string(c), query_types[qtype], query)]); #print dnsbot_file, outmsg; } } } } } # here is the bot C&C identification reply check event dns_A_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr) { local slq = second_level_domain(ans$query); if ( blacklisters in slq ) return; if ( ( a in noroute_lookup_nets) || (ans$query in noroute_state) ) { if ( ans$query in noroute_state ) { # we have seen the name before - look for an address change if ( noroute_state[ans$query] != a ) # this has changed { NOTICE([$note=DNS_NoRouteChange, $conn=c, $msg=fmt("NoRoute address change for %s changed from %s -> %s", ans$query, noroute_state[ans$query], a)]); noroute_state[ans$query] = a; } } else { # new entry for the answer - name pair. do a quick sanity check to make sure that # this is what we really want - ie a noroute_lookup_nets address if ( a in noroute_lookup_nets ) { noroute_state[ans$query] = a; NOTICE([$note=DNS_NoRouteLookup, $conn=c, $msg=fmt("NoRoute address lookup for %s: %s", ans$query, a)]); } } } }