diff --git a/policy/all.bro b/policy/all.bro index d133598dab..05aa028545 100644 --- a/policy/all.bro +++ b/policy/all.bro @@ -1,9 +1,16 @@ # This script only aims at loading all of the base analysis scripts. - +@load conn +@load dns @load ftp -@load http -@load known-services -@load known-hosts -@load ssh -@load ssl +@load http +@load irc +@load known-hosts +@load known-services +@load smtp +@load ssl +@load ssh + +@load mime +@load software +@load weird \ No newline at end of file diff --git a/policy/bro.init b/policy/bro.init index fcedefcdaa..09fad5cb86 100644 --- a/policy/bro.init +++ b/policy/bro.init @@ -259,12 +259,12 @@ type pcap_packet: record { # GeoIP support. type geo_location: record { - country_code: string &default=""; - region: string &default=""; - city: string &default=""; - latitude: double &default=0.0; - longitude: double &default=0.0; -}; + country_code: string &optional; + region: string &optional; + city: string &optional; + latitude: double &optional; + longitude: double &optional; +} &log; type entropy_test_result: record { entropy: double; @@ -925,10 +925,10 @@ type cipher_suites_list: set[count]; type SSL_sessionID: table[count] of count; type X509_extension: table[count] of string; type X509: record { - issuer: string; - subject: string; + issuer: string; + subject: string; orig_addr: addr; -}; +} &log; type http_stats_rec: record { num_requests: count; diff --git a/policy/dns/base.bro b/policy/dns/base.bro index 656f826ea1..945e7154c5 100644 --- a/policy/dns/base.bro +++ b/policy/dns/base.bro @@ -11,6 +11,7 @@ export { trans_id: count &log &optional; query: string &log &optional; qtype: count &log &optional; + qtype_name: string &log &optional; qclass: count &log &optional; rcode: count &log &optional; QR: bool &log &default=F; @@ -102,9 +103,9 @@ function set_session(c: connection, msg: dns_msg, is_query: bool) if ( info?$total_answers && info$total_answers != msg$num_answers + msg$num_addl + msg$num_auth ) { - print "the total number of answers changed midstream on a dns response."; - print info; - print msg; + #print "the total number of answers changed midstream on a dns response."; + #print info; + #print msg; } else { @@ -119,10 +120,18 @@ event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qcla { set_session(c, msg, T); - c$dns$RD = msg$RD; - c$dns$TC = msg$TC; - c$dns$qtype = qtype; - c$dns$qclass = qclass; + c$dns$RD = msg$RD; + c$dns$TC = msg$TC; + c$dns$qtype = qtype; + c$dns$qtype_name = query_types[qtype]; + c$dns$qclass = qclass; + + # Decode netbios name queries + # Note: I'm ignoring the name type for now. Not sure if this should be + # worked into the query/response in some fashion. + if ( c$id$resp_p == 137/udp ) + query = decode_netbios_name(query); + c$dns$query = query; } @@ -155,6 +164,24 @@ event dns_AAAA_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr, c$dns$replies = set(); add c$dns$replies[fmt("%s", a)]; } + +event dns_NS_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) &priority=5 + { + set_session(c, msg, F); + + if ( ! c$dns?$replies ) + c$dns$replies = set(); + add c$dns$replies[name]; + } + +event dns_CNAME_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) &priority=5 + { + set_session(c, msg, F); + + if ( ! c$dns?$replies ) + c$dns$replies = set(); + add c$dns$replies[name]; + } event dns_MX_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string, @@ -176,6 +203,43 @@ event dns_PTR_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) add c$dns$replies[name]; } +event dns_SOA_reply(c: connection, msg: dns_msg, ans: dns_answer, soa: dns_soa) + { + set_session(c, msg, F); + + if ( ! c$dns?$replies ) + c$dns$replies = set(); + add c$dns$replies[soa$mname]; + } + +event dns_WKS_reply(c: connection, msg: dns_msg, ans: dns_answer) + { + set_session(c, msg, F); + } + +event dns_SRV_reply(c: connection, msg: dns_msg, ans: dns_answer) + { + set_session(c, msg, F); + } + +# TODO: figure out how to handle these +#event dns_EDNS(c: connection, msg: dns_msg, ans: dns_answer) +# { +# +# } +# +#event dns_EDNS_addl(c: connection, msg: dns_msg, ans: dns_edns_additional) +# { +# +# } +# +#event dns_TSIG_addl(c: connection, msg: dns_msg, ans: dns_tsig_additional) +# { +# +# } + + + event dns_rejected(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count) { diff --git a/policy/dpd.bro b/policy/dpd.bro index ff30fe68c0..58c697a368 100644 --- a/policy/dpd.bro +++ b/policy/dpd.bro @@ -1,10 +1,32 @@ ##! Activates port-independent protocol detection. + +@load functions @load signatures +module DPD; + +# Add the DPD signatures. redef signature_files += "dpd.sig"; +redef enum Log::ID += { DPD }; + +export { + type Info: record { + ts: time &log; + id: conn_id &log; + proto: transport_proto &log; + analyzer: string &log; + failure_reason: string &log; + }; +} + +redef record connection += { + dpd: Info &optional; +}; event bro_init() { + Log::create_stream(DPD, [$columns=Info]); + for ( a in dpd_config ) { for ( p in dpd_config[a]$ports ) @@ -16,16 +38,25 @@ event bro_init() } } -event protocol_confirmation(c: connection, atype: count, aid: count) +event protocol_confirmation(c: connection, atype: count, aid: count) &priority=10 { - delete c$service[fmt("-%s",analyzer_name(atype))]; + if ( fmt("-%s",analyzer_name(atype)) in c$service ) + delete c$service[fmt("-%s",analyzer_name(atype))]; + add c$service[analyzer_name(atype)]; } event protocol_violation(c: connection, atype: count, aid: count, - reason: string) &priority = 10 + reason: string) &priority=10 { - delete c$service[analyzer_name(atype)]; + if ( analyzer_name(atype) in c$service ) + delete c$service[analyzer_name(atype)]; add c$service[fmt("-%s",analyzer_name(atype))]; + + Log::write(DPD, [$ts=network_time(), + $id=c$id, + $proto=get_conn_transport_proto(c$id), + $analyzer=analyzer_name(atype), + $failure_reason=reason]); } diff --git a/policy/http/software.bro b/policy/http/software.bro index e7060ce5a2..c80ec4c02a 100644 --- a/policy/http/software.bro +++ b/policy/http/software.bro @@ -3,6 +3,8 @@ @load http/base @load software +module HTTP; + redef enum Software::Type += { WEB_SERVER, WEB_BROWSER, @@ -32,7 +34,7 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) &pr { # Flash doesn't include it's name so we'll add it here since it # simplifies the version parsing. - value = cat("Flash ", value); + value = cat("Flash/", value); local flash_version = Software::parse(value, c$id$orig_h, WEB_BROWSER_PLUGIN); Software::found(c$id, flash_version); } diff --git a/policy/irc/base.bro b/policy/irc/base.bro index 5f57299f99..f3e38fad6d 100644 --- a/policy/irc/base.bro +++ b/policy/irc/base.bro @@ -8,6 +8,8 @@ module IRC; redef enum Log::ID += { IRC }; export { + type Tags: enum { EMPTY }; + type Info: record { ts: time &log; id: conn_id &log; @@ -18,6 +20,7 @@ export { command: string &log &optional; value: string &log &optional; addl: string &log &optional; + tags: set[Tags] &log &default=set(); }; const logged_commands = set("JOIN", "DCC SEND"); @@ -147,11 +150,4 @@ event irc_join_message(c: connection, info_list: irc_join_list) &priority=-5 c$irc$addl = (l$password != "" ? fmt(" with channel key: '%s'", l$password) : ""); Log::write(IRC, c$irc); } - } - -event expected_connection_seen(c: connection, a: count) &priority=10 - { - local id = c$id; - if ( [id$resp_h, id$resp_p] in dcc_expected_transfers ) - add c$service["ftp-data"]; - } + } \ No newline at end of file diff --git a/policy/irc/dcc-send.bro b/policy/irc/dcc-send.bro index 9deb6c7955..af162e77c2 100644 --- a/policy/irc/dcc-send.bro +++ b/policy/irc/dcc-send.bro @@ -12,25 +12,74 @@ module IRC; export { + redef enum Tags += { EXTRACTED_FILE }; + + ## Pattern of file mime types to extract from IRC DCC file transfers. + const extract_file_types = /NO_DEFAULT/ &redef; + + ## The on-disk prefix for files to be extracted from IRC DCC file transfers. + const extraction_prefix = "irc-dcc-item" &redef; + redef record Info += { - file_name: string &optional; - file_size: count &optional; + dcc_file_name: string &log &optional; + dcc_file_size: count &log &optional; + dcc_mime_type: string &log &optional; + + ## The file handle for the file to be extracted + extraction_file: file &log &optional; + + ## A boolean to indicate if the current file transfer shoudl be transfered. + extract_file: bool &default=F; }; } global dcc_expected_transfers: table[addr, port] of Info = table(); event file_transferred(c: connection, prefix: string, descr: string, - mime_type: string) &priority=5 + mime_type: string) &priority=3 { local id = c$id; - if ( [id$resp_h, id$resp_p] in dcc_expected_transfers ) + if ( [id$resp_h, id$resp_p] !in dcc_expected_transfers ) + return; + + local irc = dcc_expected_transfers[id$resp_h, id$resp_p]; + + irc$dcc_mime_type = mime_type; + + if ( extract_file_types in mime_type ) + irc$extract_file = T; + + if ( irc$extract_file ) { - delete dcc_expected_transfers[id$resp_h, id$resp_p]; - local fh = open("irc-dcc-item"); - set_contents_file(id, CONTENTS_RESP, fh); + add irc$tags[EXTRACTED_FILE]; + irc$extraction_file = open(fmt("%s.%s", extraction_prefix, id_string(c$id))); } + } + +event file_transferred(c: connection, prefix: string, descr: string, + mime_type: string) &priority=-4 + { + local id = c$id; + if ( [id$resp_h, id$resp_p] !in dcc_expected_transfers ) + return; + + local irc = dcc_expected_transfers[id$resp_h, id$resp_p]; + + if ( irc$extract_file && irc?$extraction_file ) + set_contents_file(id, CONTENTS_RESP, irc$extraction_file); + + # Delete these values in case another DCC transfer + # happens during the IRC session. + # TODO: uncomment these when this operator works + # delete irc$extract_file; + # delete irc$extraction_file; + # delete irc$dcc_file_name; + # delete irc$dcc_file_size; + # delete irc$dcc_mime_type; + delete dcc_expected_transfers[id$resp_h, id$resp_p]; + } + event irc_server(c: connection, prefix: string, data: string) &priority=5 { @@ -45,11 +94,17 @@ event irc_server(c: connection, prefix: string, data: string) &priority=5 c$irc$command = "DCC SEND"; #local ex_h = count_to_v4_addr(to_count(parts[|parts|-4])); local ex_p = to_port(to_count(parts[|parts|-2]), tcp); - c$irc$file_name = parts[|parts|-6]; - c$irc$file_size = to_count(parts[|parts|]); + c$irc$dcc_file_name = parts[|parts|-6]; + c$irc$dcc_file_size = to_count(parts[|parts|]); #print fmt("file! %s->%s:%d", c$id$orig_h, ex_h, ex_p); #expect_connection(c$id$orig_h, ex_h, ex_p, ANALYZER_FILE, 5 min); #dcc_expected_transfers[ex_h, ex_p]; } } +event expected_connection_seen(c: connection, a: count) &priority=10 + { + local id = c$id; + if ( [id$resp_h, id$resp_p] in dcc_expected_transfers ) + add c$service["irc-dcc-data"]; + } diff --git a/policy/notice.bro b/policy/notice.bro index a8b1907627..9a3b12adf3 100644 --- a/policy/notice.bro +++ b/policy/notice.bro @@ -21,13 +21,15 @@ export { }; type Info: record { + ts: time &log &optional; + id: conn_id &log &optional; # connection-ID, if we don't have a connection handy + note: Type &log; msg: string &default="" &log; sub: string &log &optional; # sub-message conn: connection &log &optional; # connection associated with notice iconn: icmp_conn &log &optional; # associated ICMP "connection" - id: conn_id &log &optional; # connection-ID, if we don't have a connection handy src: addr &log &optional; # source address, if we don't have a connection dst: addr &log &optional; # destination address p: port &log &optional; # associated port, if we don't have a conn. @@ -210,6 +212,8 @@ function execute_with_notice(cmd: string, n: Notice::Info) function notice(n: Notice::Info) { # Fill in some defaults. + n$ts = network_time(); + if ( ! n?$id && n?$conn ) n$id = n$conn$id; diff --git a/policy/smtp/base-extended.bro b/policy/smtp/base-extended.bro index e69de29bb2..d0ad3d76f1 100644 --- a/policy/smtp/base-extended.bro +++ b/policy/smtp/base-extended.bro @@ -0,0 +1,7 @@ + +@load smtp/base + +export { + +} + diff --git a/policy/smtp/utils.bro b/policy/smtp/utils.bro index eb6ea5fae2..1e51d940cd 100644 --- a/policy/smtp/utils.bro +++ b/policy/smtp/utils.bro @@ -1,3 +1,6 @@ + +module SMTP; + function find_address_in_smtp_header(header: string): string { local ips = find_ip_addresses(header); diff --git a/policy/software.bro b/policy/software.bro index 2a18a84b5a..d732eb30b7 100644 --- a/policy/software.bro +++ b/policy/software.bro @@ -118,6 +118,7 @@ function parse_mozilla(unparsed_version: string, if ( 2 in parts ) v = parse(parts[2], host, software_type)$version; } + return [$ts=network_time(), $host=host, $name=software_name, $version=v, $unparsed_version=unparsed_version]; } diff --git a/policy/ssh.bro b/policy/ssh.bro index 3760708256..043acb8c4c 100644 --- a/policy/ssh.bro +++ b/policy/ssh.bro @@ -1,274 +1,2 @@ -@load functions -@load notice -@load software - -module SSH; - -redef enum Notice::Type += { - SSH_Login, - SSH_PasswordGuessing, - SSH_LoginByPasswordGuesser, - SSH_Login_From_Interesting_Hostname, - SSH_Bytecount_Inconsistency, -}; - -redef enum Log::ID += { SSH }; - -redef enum Software::Type += { - SSH_SERVER, - SSH_CLIENT, -}; - -# Configure DPD and the packet filter -redef capture_filters += { ["ssh"] = "tcp port 22" }; -redef dpd_config += { [ANALYZER_SSH] = [$ports = set(22/tcp)] }; - -export { - type Log: record { - ts: time; - id: conn_id; - status: string &default=""; - direction: string &default=""; - remote_location: geo_location &optional; - client: string &default=""; - server: string &default=""; - resp_size: count &default=0; - }; - # This is the prototype for the event that the logging framework tries - # to generate if there is a handler for it. - global log: event(rec: Log); - - const password_guesses_limit = 30 &redef; - - # The size in bytes at which the SSH connection is presumed to be - # successful. - const authentication_data_size = 5500 &redef; - - # The amount of time to remember presumed non-successful logins to build - # model of a password guesser. - const guessing_timeout = 30 mins &redef; - - # If you want to lookup and log geoip data in the event of a failed login. - const log_geodata_on_failure = F &redef; - - # The set of countries for which you'd like to throw notices upon successful login - # requires Bro compiled with libGeoIP support - const watched_countries: set[string] = {"RO"} &redef; - - # Strange/bad host names to originate successful SSH logins - const interesting_hostnames = - /^d?ns[0-9]*\./ | - /^smtp[0-9]*\./ | - /^mail[0-9]*\./ | - /^pop[0-9]*\./ | - /^imap[0-9]*\./ | - /^www[0-9]*\./ | - /^ftp[0-9]*\./ &redef; - - # This is a table with orig subnet as the key, and subnet as the value. - const ignore_guessers: table[subnet] of subnet &redef; - - # If true, we tell the event engine to not look at further data - # packets after the initial SSH handshake. Helps with performance - # (especially with large file transfers) but precludes some - # kinds of analyses (e.g., tracking connection size). - const skip_processing_after_detection = F &redef; - - # Keeps count of how many rejections a host has had - global password_rejections: table[addr] of TrackCount - &default=default_track_count - &write_expire=guessing_timeout; - - # Keeps track of hosts identified as guessing passwords - # TODO: guessing_timeout doesn't work correctly here. If a user redefs - # the variable, it won't take effect. - global password_guessers: set[addr] &read_expire=guessing_timeout+1hr; - - # The list of active SSH connections and the associated session info. - global active_conns: table[conn_id] of Log &read_expire=2mins; - - global log_ssh: event(rec: Log); -} - -function local_filter(rec: record { id: conn_id; } ): bool - { - return is_local_addr(rec$id$resp_h); - } - -event bro_init() -{ - # Create the stream. - # First argument is the ID for the stream. - # Second argument is the log record type. - Log::create_stream(SSH, [$columns=SSH::Log, $ev=log_ssh]); - # Add a default filter that simply logs everything to "ssh.log" using the default writer. - Log::add_default_filter(SSH); -} - -event check_ssh_connection(c: connection, done: bool) - { - # If this is no longer a known SSH connection, just return. - if ( c$id !in active_conns ) - return; - - # If this is still a live connection and the byte count has not - # crossed the threshold, just return and let the resheduled check happen later. - if ( !done && c$resp$size < authentication_data_size ) - return; - - # Make sure the server has sent back more than 50 bytes to filter out - # hosts that are just port scanning. Nothing is ever logged if the server - # doesn't send back at least 50 bytes. - if ( c$resp$size < 50 ) - return; - - local ssh_log = active_conns[c$id]; - local status = "failure"; - local direction = is_local_addr(c$id$orig_h) ? "to" : "from"; - local location: geo_location; - - if ( done && c$resp$size < authentication_data_size ) - { - # presumed failure - if ( log_geodata_on_failure ) - location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h); - - if ( c$id$orig_h !in password_rejections ) - password_rejections[c$id$orig_h] = default_track_count(c$id$orig_h); - - # Track the number of rejections - if ( !(c$id$orig_h in ignore_guessers && - c$id$resp_h in ignore_guessers[c$id$orig_h]) ) - ++password_rejections[c$id$orig_h]$n; - - if ( default_check_threshold(password_rejections[c$id$orig_h]) ) - { - add password_guessers[c$id$orig_h]; - NOTICE([$note=SSH_PasswordGuessing, - $conn=c, - $msg=fmt("SSH password guessing by %s", c$id$orig_h), - $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n), - $n=password_rejections[c$id$orig_h]$n]); - } - } - # TODO: This is to work around a quasi-bug in Bro which occasionally - # causes the byte count to be oversized. - # Watch for Gregors work that adds an actual counter of bytes transferred. - else if ( c$resp$size < 20000000 ) - { - # presumed successful login - status = "success"; - location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h); - - if ( password_rejections[c$id$orig_h]$n > password_guesses_limit && - c$id$orig_h !in password_guessers) - { - add password_guessers[c$id$orig_h]; - NOTICE([$note=SSH_LoginByPasswordGuesser, - $conn=c, - $n=password_rejections[c$id$orig_h]$n, - $msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h), - $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n)]); - } - - local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)", - direction, location$country_code, location$region, location$city, - location$latitude, location$longitude, - id_string(c$id), c$resp$size); - # TODO: rewrite the message once a location variable can be put in notices - NOTICE([$note=SSH_Login, - $conn=c, - $msg=message, - $sub=location$country_code]); - - # Check to see if this login came from an interesting hostname - when( local hostname = lookup_addr(c$id$orig_h) ) - { - if ( interesting_hostnames in hostname ) - { - NOTICE([$note=SSH_Login_From_Interesting_Hostname, - $conn=c, - $msg=fmt("Strange login from %s", hostname), - $sub=hostname]); - } - } - } - else if ( c$resp$size >= 200000000 ) - { - NOTICE([$note=SSH_Bytecount_Inconsistency, - $conn=c, - $msg="During byte counting in SSH analysis, an overly large value was seen.", - $sub=fmt("%d",c$resp$size)]); - } - - ssh_log$remote_location = location; - ssh_log$status = status; - ssh_log$direction = direction; - ssh_log$resp_size = c$resp$size; - - Log::write(SSH, ssh_log); - - delete active_conns[c$id]; - # Stop watching this connection, we don't care about it anymore. - if ( skip_processing_after_detection ) - { - skip_further_processing(c$id); - set_record_packets(c$id, F); - } - } - -event connection_state_remove(c: connection) - { - event check_ssh_connection(c, T); - } - -event ssh_watcher(c: connection) - { - local id = c$id; - # don't go any further if this connection is gone already! - if ( !connection_exists(id) ) - { - delete active_conns[id]; - return; - } - - event check_ssh_connection(c, F); - if ( c$id in active_conns ) - schedule +15secs { ssh_watcher(c) }; - } - -event ssh_client_version(c: connection, version: string) - { - if ( c$id in active_conns ) - active_conns[c$id]$client = version; - else - { - active_conns[c$id] = [$ts=c$start_time, $id=c$id]; - schedule +15secs { ssh_watcher(c) }; - } - - # Get rid of the protocol information when passing to the software framework. - local cleaned_version = sub(version, /^SSH[0-9\.\-]+/, ""); - local si = Software::parse(cleaned_version, c$id$orig_h, SSH_CLIENT); - Software::found(c, si); - } - -event ssh_server_version(c: connection, version: string) - { - if ( c$id in active_conns ) - active_conns[c$id]$server = version; - - # Get rid of the protocol information when passing to the software framework. - local cleaned_version = sub(version, /SSH[0-9\.\-]{2,}/, ""); - local si = Software::parse(cleaned_version, c$id$resp_h, SSH_SERVER); - Software::found(c, si); - } - -event protocol_confirmation(c: connection, atype: count, aid: count) - { - if ( atype == ANALYZER_SSH ) - { - active_conns[c$id] = [$ts=c$start_time, $id=c$id]; - schedule +15secs { ssh_watcher(c) }; - } - } \ No newline at end of file +@load ssh/base +@load ssh/software \ No newline at end of file diff --git a/policy/ssh/base.bro b/policy/ssh/base.bro new file mode 100644 index 0000000000..d4cf810c86 --- /dev/null +++ b/policy/ssh/base.bro @@ -0,0 +1,263 @@ +@load functions +@load notice + +module SSH; + +redef enum Notice::Type += { + SSH_Login, + SSH_PasswordGuessing, + SSH_LoginByPasswordGuesser, + SSH_Login_From_Interesting_Hostname, + SSH_Bytecount_Inconsistency, +}; + +redef enum Log::ID += { SSH }; + +# Configure DPD and the packet filter +redef capture_filters += { ["ssh"] = "tcp port 22" }; +redef dpd_config += { [ANALYZER_SSH] = [$ports = set(22/tcp)] }; + +export { + type Info: record { + ts: time &log; + id: conn_id &log; + status: string &log &optional; + direction: string &log &optional; + remote_location: geo_location &log &optional; + client: string &log &optional; + server: string &log &optional; + resp_size: count &log &default=0; + + ## Indicate if the SSH session is done being watched. + done: bool &default=F; + }; + + const password_guesses_limit = 30 &redef; + + # The size in bytes at which the SSH connection is presumed to be + # successful. + const authentication_data_size = 5500 &redef; + + # The amount of time to remember presumed non-successful logins to build + # model of a password guesser. + const guessing_timeout = 30 mins &redef; + + # If you want to lookup and log geoip data in the event of a failed login. + const log_geodata_on_failure = F &redef; + + # The set of countries for which you'd like to throw notices upon successful login + # requires Bro compiled with libGeoIP support + const watched_countries: set[string] = {"RO"} &redef; + + # Strange/bad host names to originate successful SSH logins + const interesting_hostnames = + /^d?ns[0-9]*\./ | + /^smtp[0-9]*\./ | + /^mail[0-9]*\./ | + /^pop[0-9]*\./ | + /^imap[0-9]*\./ | + /^www[0-9]*\./ | + /^ftp[0-9]*\./ &redef; + + # This is a table with orig subnet as the key, and subnet as the value. + const ignore_guessers: table[subnet] of subnet &redef; + + # If true, we tell the event engine to not look at further data + # packets after the initial SSH handshake. Helps with performance + # (especially with large file transfers) but precludes some + # kinds of analyses (e.g., tracking connection size). + const skip_processing_after_detection = F &redef; + + # Keeps count of how many rejections a host has had + global password_rejections: table[addr] of TrackCount + &default=default_track_count + &write_expire=guessing_timeout; + + # Keeps track of hosts identified as guessing passwords + # TODO: guessing_timeout doesn't work correctly here. If a user redefs + # the variable, it won't take effect. + global password_guessers: set[addr] &read_expire=guessing_timeout+1hr; + + global log_ssh: event(rec: Info); +} + +# TODO: move this elsewhere +function local_filter(rec: record { id: conn_id; } ): bool + { + return is_local_addr(rec$id$resp_h); + } + +redef record connection += { + ssh: Info &optional; +}; + +event bro_init() +{ + Log::create_stream(SSH, [$columns=Info, $ev=log_ssh]); +} + +function set_session(c: connection) + { + if ( ! c?$ssh ) + { + local info: Info; + info$ts=network_time(); + info$id=c$id; + c$ssh = info; + } + } + +function check_ssh_connection(c: connection, done: bool) + { + # If done watching this connection, just return. + if ( c$ssh$done ) + return; + + # If this is still a live connection and the byte count has not + # crossed the threshold, just return and let the resheduled check happen later. + if ( !done && c$resp$size < authentication_data_size ) + return; + + # Make sure the server has sent back more than 50 bytes to filter out + # hosts that are just port scanning. Nothing is ever logged if the server + # doesn't send back at least 50 bytes. + if ( c$resp$size < 50 ) + return; + + local status = "failure"; + local direction = is_local_addr(c$id$orig_h) ? "to" : "from"; + local location: geo_location; + + if ( done && c$resp$size < authentication_data_size ) + { + # presumed failure + if ( log_geodata_on_failure ) + location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h); + + if ( c$id$orig_h !in password_rejections ) + password_rejections[c$id$orig_h] = default_track_count(c$id$orig_h); + + # Track the number of rejections + if ( !(c$id$orig_h in ignore_guessers && + c$id$resp_h in ignore_guessers[c$id$orig_h]) ) + ++password_rejections[c$id$orig_h]$n; + + if ( default_check_threshold(password_rejections[c$id$orig_h]) ) + { + add password_guessers[c$id$orig_h]; + NOTICE([$note=SSH_PasswordGuessing, + $conn=c, + $msg=fmt("SSH password guessing by %s", c$id$orig_h), + $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n), + $n=password_rejections[c$id$orig_h]$n]); + } + } + # TODO: This is to work around a quasi-bug in Bro which occasionally + # causes the byte count to be oversized. + # Watch for Gregors work that adds an actual counter of bytes transferred. + else if ( c$resp$size < 20000000 ) + { + # presumed successful login + status = "success"; + location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h); + + if ( password_rejections[c$id$orig_h]$n > password_guesses_limit && + c$id$orig_h !in password_guessers) + { + add password_guessers[c$id$orig_h]; + NOTICE([$note=SSH_LoginByPasswordGuesser, + $conn=c, + $n=password_rejections[c$id$orig_h]$n, + $msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h), + $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n)]); + } + + local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)", + direction, location$country_code, location$region, location$city, + location$latitude, location$longitude, + id_string(c$id), c$resp$size); + # TODO: rewrite the message once a location variable can be put in notices + NOTICE([$note=SSH_Login, + $conn=c, + $msg=message, + $sub=location$country_code]); + + # Check to see if this login came from an interesting hostname + when( local hostname = lookup_addr(c$id$orig_h) ) + { + if ( interesting_hostnames in hostname ) + { + NOTICE([$note=SSH_Login_From_Interesting_Hostname, + $conn=c, + $msg=fmt("Strange login from %s", hostname), + $sub=hostname]); + } + } + } + else if ( c$resp$size >= 200000000 ) + { + NOTICE([$note=SSH_Bytecount_Inconsistency, + $conn=c, + $msg="During byte counting in SSH analysis, an overly large value was seen.", + $sub=fmt("%d",c$resp$size)]); + } + + c$ssh$remote_location = location; + c$ssh$status = status; + c$ssh$direction = direction; + c$ssh$resp_size = c$resp$size; + + Log::write(SSH, c$ssh); + + # Set the "done" flag to prevent the watching event from rescheduling + # after detection is done. + c$ssh$done; + + # Stop watching this connection, we don't care about it anymore. + if ( skip_processing_after_detection ) + { + skip_further_processing(c$id); + set_record_packets(c$id, F); + } + } + +event connection_state_remove(c: connection) + { + if ( c?$ssh ) + check_ssh_connection(c, T); + } + +event ssh_watcher(c: connection) + { + local id = c$id; + # don't go any further if this connection is gone already! + if ( !connection_exists(id) ) + return; + + check_ssh_connection(c, F); + if ( ! c$ssh$done ) + schedule +15secs { ssh_watcher(c) }; + } + +event ssh_server_version(c: connection, version: string) &priority=5 + { + set_session(c); + c$ssh$server = version; + } + +event ssh_client_version(c: connection, version: string) &priority=5 + { + set_session(c); + c$ssh$client = version; + schedule +15secs { ssh_watcher(c) }; + } + +#event protocol_confirmation(c: connection, atype: count, aid: count) &priority=5 +# { +# if ( atype == ANALYZER_SSH ) +# { +# if ( ! c?$ssh ) +# schedule +15secs { ssh_watcher(c) }; +# set_session(c); +# } +# } \ No newline at end of file diff --git a/policy/ssh/software.bro b/policy/ssh/software.bro new file mode 100644 index 0000000000..4dd795ea2c --- /dev/null +++ b/policy/ssh/software.bro @@ -0,0 +1,25 @@ +@load ssh/base +@load software + +module SSH; + +redef enum Software::Type += { + SSH_SERVER, + SSH_CLIENT, +}; + +event ssh_client_version(c: connection, version: string) &priority=4 + { + # Get rid of the protocol information when passing to the software framework. + local cleaned_version = sub(version, /^SSH[0-9\.\-]+/, ""); + local si = Software::parse(cleaned_version, c$id$orig_h, SSH_CLIENT); + Software::found(c$id, si); + } + +event ssh_server_version(c: connection, version: string) &priority=4 + { + # Get rid of the protocol information when passing to the software framework. + local cleaned_version = sub(version, /SSH[0-9\.\-]{2,}/, ""); + local si = Software::parse(cleaned_version, c$id$resp_h, SSH_SERVER); + Software::found(c$id, si); + } diff --git a/policy/ssl-ciphers.bro b/policy/ssl-ciphers.bro index b7929d2f19..6b196bebcb 100644 --- a/policy/ssl-ciphers.bro +++ b/policy/ssl-ciphers.bro @@ -465,7 +465,7 @@ const ssl_cipher_desc: table[count] of string = { [SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA", [SSL_RSA_FIPS_WITH_DES_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_DES_CBC_SHA_2", [SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2", -}; +} &default="UNKNOWN"; # --- the following sets are provided for convenience diff --git a/policy/ssl.bro b/policy/ssl.bro index f41a7668cc..982dc19bc3 100644 --- a/policy/ssl.bro +++ b/policy/ssl.bro @@ -9,7 +9,7 @@ module SSL; redef enum Notice::Type += { - # Blanket X509 error + ## Blanket X509 error SSL_X509Violation, ## Session data not consistent with connection SSL_SessConIncon, @@ -18,33 +18,31 @@ redef enum Notice::Type += { redef enum Log::ID += { SSL }; export { - type Log: record { - ts: time; - id: conn_id; - ## This is the session ID. It's optional because SSLv2 doesn't have it. - sid: string &optional; - # TODO: dga 3/11 The following 2 fields are not yet picked up - #not_valid_before: time; # certificate valid time constraint - #not_valid_after: time; # certificate valid time constraint - version: string &default="UNKNOWN"; # version number - weak_client_cipher: bool &default = F; # true if client offered insecure ciphers - weak_server_cipher: bool &default = F; # true if server offered insecure ciphers - weak_cipher_agreed: bool &default = F; # true if insecure cipher agreed upon for use - - version: string &default=""; # version associated with connection - client_cert: X509 &optional; # client certificate - server_cert: X509 &optional; # server certificate - handshake_cipher: string &default=""; # agreed-upon cipher for session/conn. + type Tags: enum { + WEAK_CLIENT_CIPHER, + WEAK_SERVER_CIPHER, + WEAK_CIPHER_AGREED }; - type ConnectionInfo: record { - log: Log; + type Info: record { + ts: time &log; + id: conn_id &log; + ## This is the session ID. It's optional because SSLv2 doesn't have it. + sid: string &log &optional; + # TODO: dga 3/11 The following 2 fields are not yet picked up + #not_valid_before: time &log &optional; ##< certificate valid time constraint + #not_valid_after: time &log &optional; ##< certificate valid time constraint + version: string &log &default="UNKNOWN"; ##< SSL/TLS version number + client_cert: X509 &log &optional; ##< client certificate + server_cert: X509 &log &optional; ##< server certificate + handshake_cipher: string &log &optional; ##< agreed-upon cipher for session/conn. + tags: set[Tags] &log; }; type SessionInfo: record { - ## This tracks the number of times this session has been reused. - num_reuse: count &default=1; + ## This tracks the number of times this session has been used. + num_use: count &default=1; version: string &default=""; # version associated with connection client_cert: X509 &optional; # client certificate @@ -89,13 +87,10 @@ export { ## The list of all detected X509 certs. global certs: set[addr, port, string] &create_expire=1day &synchronized; - ## All active SSL/TLS connections - global active_conns: table[conn_id] of ConnectionInfo &read_expire=1hr; - ## Recent TLS session IDs global recent_sessions: table[string] of SessionInfo &read_expire=1hr; - global log_ssl: event(rec: Log); + global log_ssl: event(rec: Info); ## This is the set of SSL/TLS ciphers are are seen as weak to attack. const weak_ciphers: set[count] = { @@ -140,6 +135,10 @@ export { } +redef record connection += { + ssl: Info &optional; +}; + # NOTE: this is a 'local' port format for your site # --- well-known ports for ssl --------- redef capture_filters += { @@ -167,8 +166,7 @@ redef dpd_config += { event bro_init() { - Log::create_stream(SSL, [$columns=SSL::Log, $ev=log_ssl] ); - Log::add_default_filter(SSL); + Log::create_stream(SSL, [$columns=Info, $ev=log_ssl] ); # The event engine will generate a run-time if this fails for # reasons other than that the directory already exists. @@ -199,24 +197,16 @@ const x509_hot_errors: set[int] = { }; @endif -function ssl_get_cipher_name(cipherSuite: count): string - { - return cipherSuite in ssl_cipher_desc ? - ssl_cipher_desc[cipherSuite] : "UNKNOWN"; - } - -function get_connection_info(c: connection): ConnectionInfo +function set_session(c: connection) { local id = c$id; - if ( id in active_conns ) - return active_conns[id]; - else + if ( ! c?$ssl ) { - local log: Log = [$ts=network_time(), $id=id]; - local conn_info: ConnectionInfo = [$log=log]; - active_conns[id] = conn_info; - return conn_info; + local info: Info; + info$ts=network_time(); + info$id=id; + c$ssl = info; } } @@ -231,44 +221,43 @@ function get_session_info(s: SSL_sessionID): SessionInfo event ssl_certificate(c: connection, cert: X509, is_server: bool) { - #if ( is_server ) - # event protocol_confirmation(c, ANALYZER_SSL, 0); - local conn = get_connection_info(c); - + print "hello?"; + set_session(c); + if ( [c$id$resp_h, c$id$resp_p, cert$subject] !in certs ) add certs[c$id$resp_h, c$id$resp_p, cert$subject]; if( is_server ) { - conn$log$server_cert = cert; + c$ssl$server_cert = cert; # We have not filled in the field for the master session # for this connection. Do it now, but only if this is not a # SSLv2 connection (no session information in that case). - if ( conn$log$sid in recent_sessions && - recent_sessions[conn$log$sid]?$server_cert ) - recent_sessions[conn$log$sid]$server_cert$subject = cert$subject; + if ( c$ssl$sid in recent_sessions && + recent_sessions[c$ssl$sid]?$server_cert ) + recent_sessions[c$ssl$sid]$server_cert$subject = cert$subject; } else { - conn$log$client_cert = cert; + c$ssl$client_cert = cert; } } event ssl_conn_attempt(c: connection, version: count, ciphers: cipher_suites_list) { - local conn = get_connection_info(c); + set_session(c); - conn$log$version = version_strings[version]; + c$ssl$version = version_strings[version]; for ( cs in ciphers ) { if ( cs in weak_ciphers ) { - conn$log$weak_client_cipher = T; + add c$ssl$tags[WEAK_CLIENT_CIPHER]; #event ssl_conn_weak( # fmt("SSL client supports weak cipher: %s (0x%x)", - # ssl_get_cipher_name(cs), cs), c); + # ssl_cipher_desc[cs], cs), c); } } } @@ -276,7 +265,7 @@ event ssl_conn_attempt(c: connection, version: count, ciphers: cipher_suites_lis event ssl_conn_server_reply(c: connection, version: count, ciphers: cipher_suites_list) { - local conn = get_connection_info(c); + set_session(c); #conn$log$version = version_strings[version]; @@ -284,32 +273,28 @@ event ssl_conn_server_reply(c: connection, version: count, { if ( cs in weak_ciphers ) { - conn$log$weak_server_cipher = T; - #event ssl_conn_weak( - # fmt("SSLv2 server supports weak cipher: %s (0x%x)", - # ssl_get_cipher_name(cs), cs), c); + add c$ssl$tags[WEAK_SERVER_CIPHER]; } } } event ssl_conn_established(c: connection, version: count, cipher_suite: count) &priority=1 { - local conn = get_connection_info(c); + set_session(c); - conn$log$ts = network_time(); - #conn$log$version = version_strings[version]; + c$ssl$version = version_strings[version]; if ( cipher_suite in weak_ciphers ) - conn$log$weak_cipher_agreed = T; + add c$ssl$tags[WEAK_CIPHER_AGREED]; # log the connection - Log::write(SSL, conn$log); + Log::write(SSL, c$ssl); } event process_X509_extensions(c: connection, ex: X509_extension) { - local conn = get_connection_info(c); - + set_session(c); + #local msg = fmt( "%.6f X.509 extensions: ", network_time() ); #for ( i in ex ) # msg = fmt("%s, %s", msg, ex[i]); @@ -317,33 +302,34 @@ event process_X509_extensions(c: connection, ex: X509_extension) event ssl_session_insertion(c: connection, id: SSL_sessionID) { + set_session(c); + local cid = c$id; - local conn = get_connection_info(c); - conn$log$sid=md5_hash(id); + c$ssl$sid=md5_hash(id); # This will create a new session if one doesn't already exist. local session = get_session_info(id); - session$version=conn$log$version; - if ( conn$log?$client_cert ) session$client_cert=conn$log$client_cert; - if ( conn$log?$server_cert ) session$server_cert=conn$log$server_cert; - session$handshake_cipher=conn$log$handshake_cipher; + session$version=c$ssl$version; + if ( c$ssl?$client_cert ) session$client_cert=c$ssl$client_cert; + if ( c$ssl?$server_cert ) session$server_cert=c$ssl$server_cert; + if ( c$ssl?$handshake_cipher )session$handshake_cipher=c$ssl$handshake_cipher; } event ssl_conn_reused(c: connection, session_id: SSL_sessionID) { - local conn = get_connection_info(c); - + set_session(c); + # We cannot track sessions with SSLv2. - if ( conn$log$version == version_strings[SSLv2] ) + if ( c$ssl$version == version_strings[SSLv2] ) return; local session = get_session_info(session_id); - ++session$num_reuse; + ++session$num_use; # At this point, the connection values have been set. We can then # compare session and connection values with some confidence. - if ( session$version != conn$log$version || - session$handshake_cipher != conn$log$handshake_cipher ) + if ( session$version != c$ssl$version || + session$handshake_cipher != c$ssl$handshake_cipher ) { NOTICE([$note=SSL_SessConIncon, $conn=c, $msg="session violation"]); } @@ -353,8 +339,9 @@ event ssl_X509_error(c: connection, err: int, err_string: string) { if ( err in x509_ignore_errors ) return; + + set_session(c); - local conn = get_connection_info(c); local error = err in x509_errors ? x509_errors[err] : "unknown X.509 error"; @@ -362,13 +349,7 @@ event ssl_X509_error(c: connection, err: int, err_string: string) if ( err in x509_hot_errors ) { NOTICE([$note=SSL_X509Violation, $conn=c, $msg=error]); - ++c$hot; severity = "error"; } } -event connection_state_remove(c: connection) - { - delete active_conns[c$id]; - } - diff --git a/testing/btest/policy/software-known-version-parsing.bro b/testing/btest/policy/software-known-version-parsing.bro index cdbd1252c8..3516c50060 100644 --- a/testing/btest/policy/software-known-version-parsing.bro +++ b/testing/btest/policy/software-known-version-parsing.bro @@ -47,6 +47,16 @@ global matched_software: table[string] of Software::Info = { [$name="Zope/(Zope", $version=[$major=2,$minor=7,$minor2=8,$addl="final"], $host=0.0.0.0, $ts=ts], ["The Bat! (v2.00.9) Personal"] = [$name="The Bat!", $version=[$major=2,$minor=0,$minor2=9,$addl="Personal"], $host=0.0.0.0, $ts=ts], + ["Flash/10,2,153,1"] = + [$name="Flash", $version=[$major=10,$minor=2,$minor2=153,$addl="1"], $host=0.0.0.0, $ts=ts], + + ["Apache/2.0.46 (Win32) mod_ssl/2.0.46 OpenSSL/0.9.7b mod_jk2/2.0.4"] = + [$name="Apache", $version=[$major=2,$minor=0,$minor2=46,$addl="Win32"], $host=0.0.0.0, $ts=ts], + + ["Apple iPhone v4.3.1 Weather v1.0.0.8G4"] = + [$name="Apple iPhone", $version=[$major=4,$minor=3,$minor2=1,$addl="Weather"], $host=0.0.0.0, $ts=ts], + ["Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"] = + [$name="Safari", $version=[$major=5,$minor=0,$minor2=2,$addl="Mobile"], $host=0.0.0.0, $ts=ts], }; event bro_init()