diff --git a/scripts/base/init-bare.bro b/scripts/base/init-bare.bro index efce524fc5..f71ebe7718 100644 --- a/scripts/base/init-bare.bro +++ b/scripts/base/init-bare.bro @@ -2222,6 +2222,43 @@ export { const heartbeat_interval = 1.0 secs &redef; } +module SSH; + +export { + ## SSH Capability record + type Capabilities: record { + ## Key exchange algorithms + kex_algorithms : string_vec; + ## The algorithms supported for the server host key + server_host_key_algorithms : string_vec; + ## Acceptable symmetric encryption algorithms for c->s, + ## in order of preference + encryption_algorithms_client_to_server : string_vec; + ## Acceptable symmetric encryption algorithms for s->c, + ## in order of preference + encryption_algorithms_server_to_client : string_vec; + ## Acceptable MAC algorithms for c->s, + ## in order of preference + mac_algorithms_client_to_server : string_vec; + + ## Acceptable MAC algorithms for s->c, + ## in order of preference + mac_algorithms_server_to_client : string_vec; + ## Acceptable compression algorithms for c->s, + ## in order of preference + compression_algorithms_client_to_server : string_vec; + ## Acceptable compression algorithms for c->s, + ## in order of preference + compression_algorithms_server_to_client : string_vec; + ## Language tags in order of preference for c->s + languages_client_to_server : string_vec &optional; + ## Language tags in order of preference for s->c + languages_server_to_client : string_vec &optional; + ## Are these the capabilities of the server? + is_server : bool; + }; +} + module GLOBAL; ## An NTP message. diff --git a/scripts/base/protocols/ssh/dpd.sig b/scripts/base/protocols/ssh/dpd.sig index e56878275c..816e7929b3 100644 --- a/scripts/base/protocols/ssh/dpd.sig +++ b/scripts/base/protocols/ssh/dpd.sig @@ -1,6 +1,13 @@ -signature dpd_ssh { +signature dpd_ssh_client { ip-proto == tcp - payload /^[sS][sS][hH]-[12]./ + payload /^[sS][sS][hH]-[12]\./ + requires-reverse-signature dpd_ssh_server enable "ssh" + tcp-state originator } +signature dpd_ssh_server { + ip-proto == tcp + payload /^[sS][sS][hH]-[12]\./ + tcp-state responder +} \ No newline at end of file diff --git a/scripts/base/protocols/ssh/main.bro b/scripts/base/protocols/ssh/main.bro index 24e11779b7..8db2e4d023 100644 --- a/scripts/base/protocols/ssh/main.bro +++ b/scripts/base/protocols/ssh/main.bro @@ -15,22 +15,40 @@ export { ## SSH major version (1 or 2) version: count &log; ## Auth result - result: string &log &optional; - ## Auth method (password, pubkey, etc.) - method: string &log &optional; + auth_success: bool &log &optional; + + ## Auth details + auth_details: string &log &optional; ## Direction of the connection. If the client was a local host ## logging into an external host, this would be OUTBOUND. INBOUND ## would be set for the opposite situation. ## TODO: handle local-local and remote-remote better. direction: Direction &log &optional; + ## The encryption algorithm in use + cipher_alg: string &log &optional; + ## The signing (MAC) algorithm in use + mac_alg: string &log &optional; + ## The compression algorithm in use + compression_alg: string &log &optional; + ## The key exchange algorithm in use + kex_alg: string &log &optional; + + ## The server host key's algorithm + host_key_alg: string &log &optional; + ## The server's key fingerprint + host_key: string &log &optional; ## The client's version string client: string &log &optional; ## The server's version string server: string &log &optional; - ## The server's key fingerprint - host_key: string &log &optional; + ## This connection has been logged (internal use) logged: bool &default=F; + ## Number of failures seen (internal use) + num_failures: count &default=0; + ## Store capabilities from the first host for + ## comparison with the second (internal use) + capabilities: Capabilities &optional; }; ## If true, we tell the event engine to not look at further data @@ -48,146 +66,167 @@ redef record connection += { ssh: Info &optional; }; -const ports = { 22/tcp }; - event bro_init() &priority=5 { Log::create_stream(SSH::LOG, [$columns=Info, $ev=log_ssh]); - Analyzer::register_for_ports(Analyzer::ANALYZER_SSH, ports); } -function determine_auth_method(version: int, last_pkt_len: int, middle_pkt_len: int, first_pkt_len: int): string - { - # This is still being tested. - # Based on "Analysis for Identifying User Authentication Methods on SSH Connections" - # by Satoh, Nakamura, Ikenaga. - if ( version == 2 ) - { - if ( first_pkt_len == 0 ) - return "none"; - if ( middle_pkt_len == 96 ) - return "password"; - if ( middle_pkt_len == 16 ) - return "gssapi"; - if ( ( middle_pkt_len == 32 ) && ( first_pkt_len == 0 || first_pkt_len == 48 ) ) - return "challenge-response"; - if ( middle_pkt_len < 256 ) - return fmt("unknown (mid=%d, first=%d)", middle_pkt_len, first_pkt_len); - if ( first_pkt_len == 16 ) - return "host-based"; - return fmt("pubkey (~%d bits)", (first_pkt_len - 16)*8); - } - else if ( version == 1 ) - { - if ( first_pkt_len == 0 ) - return "password"; - if ( first_pkt_len >= 96 && first_pkt_len <= 256 ) - return fmt("pubkey (~%d bits)", first_pkt_len * 8); - return fmt("%d %d %d", first_pkt_len, middle_pkt_len, last_pkt_len); - } - } +function init_record(c: connection) + { + local s: SSH::Info; + s$ts = network_time(); + s$uid = c$uid; + s$id = c$id; + c$ssh = s; + } + event ssh_server_version(c: connection, version: string) { if ( !c?$ssh ) - { - local s: SSH::Info; - s$ts = network_time(); - s$uid = c$uid; - s$id = c$id; - c$ssh = s; - } + init_record(c); + c$ssh$server = version; } event ssh_client_version(c: connection, version: string) { if ( !c?$ssh ) - { - local s: SSH::Info; - s$ts = network_time(); - s$uid = c$uid; - s$id = c$id; - c$ssh = s; - } + init_record(c); + c$ssh$client = version; + if ( version[4] == "1" ) c$ssh$version = 1; if ( version[4] == "2" ) c$ssh$version = 2; } -event ssh_auth_successful(c: connection, last_pkt_len: int, middle_pkt_len: int, first_pkt_len: int) +event ssh_auth_successful(c: connection, auth_method_none: bool) { - print "ssh_auth_successful"; - if ( !c?$ssh || ( c$ssh?$result && c$ssh$result == "success" ) ) + if ( !c?$ssh || ( c$ssh?$auth_success && c$ssh$auth_success ) ) return; - c$ssh$result = "success"; - c$ssh$method = determine_auth_method(c$ssh$version, last_pkt_len, middle_pkt_len, first_pkt_len); - } -event ssh_auth_successful(c: connection, last_pkt_len: int, middle_pkt_len: int, first_pkt_len: int) &priority=-5 - { - c$ssh$logged = T; - Log::write(SSH::LOG, c$ssh); - } - -event ssh_auth_failed(c: connection, last_pkt_len: int, middle_pkt_len: int, first_pkt_len: int) - { - print "ssh_auth_failed"; - if ( !c?$ssh || ( c$ssh?$result && c$ssh$result == "success" ) ) + # We can't accurately tell for compressed streams + if ( c$ssh?$compression_alg && ( c$ssh$compression_alg == "zlib@openssh.com" || + c$ssh$compression_alg == "zlib" ) ) return; - c$ssh$result = "failure"; - c$ssh$method = determine_auth_method(c$ssh$version, last_pkt_len, middle_pkt_len, first_pkt_len); - } - -event ssh_auth_failed(c: connection, last_pkt_len: int, middle_pkt_len: int, first_pkt_len: int) &priority=-5 - { - c$ssh$logged = T; - Log::write(SSH::LOG, c$ssh); - } + + c$ssh$auth_success = T; -event connection_state_remove(c: connection) - { - if ( c?$ssh && !c$ssh?$result ) + if ( auth_method_none ) + c$ssh$auth_details = "method: none"; + + if ( skip_processing_after_detection) { - c$ssh$result = "unknown"; + skip_further_processing(c$id); + set_record_packets(c$id, F); + } + } + +event ssh_auth_successful(c: connection, auth_method_none: bool) &priority=-5 + { + if ( c?$ssh && !c$ssh$logged ) + { + c$ssh$logged = T; + Log::write(SSH::LOG, c$ssh); } } - -event ssh_server_capabilities(c: connection, kex_algorithms: string, server_host_key_algorithms: string, encryption_algorithms_client_to_server: string, encryption_algorithms_server_to_client: string, mac_algorithms_client_to_server: string, mac_algorithms_server_to_client: string, compression_algorithms_client_to_server: string, compression_algorithms_server_to_client: string, languages_client_to_server: string, languages_server_to_client: string) + +event ssh_auth_failed(c: connection) { - # print "kex_algorithms", kex_algorithms; - # print ""; - # print "server_host_key_algorithms", server_host_key_algorithms; - # print ""; - # print "encryption_algorithms_client_to_server", encryption_algorithms_client_to_server; - # print ""; - # print "encryption_algorithms_server_to_client", encryption_algorithms_server_to_client; - # print ""; - # print "mac_algorithms_client_to_server", mac_algorithms_client_to_server; - # print ""; - # print "mac_algorithms_server_to_client", mac_algorithms_server_to_client; - # print ""; - # print "compression_algorithms_client_to_server", compression_algorithms_client_to_server; - # print ""; - # print "compression_algorithms_server_to_client", compression_algorithms_server_to_client; - # print ""; - # print "languages_client_to_server", languages_client_to_server; - # print ""; - # print "languages_server_to_client", languages_server_to_client; - # print ""; + if ( !c?$ssh || ( c$ssh?$auth_success && !c$ssh$auth_success ) ) + return; + + # We can't accurately tell for compressed streams + if ( c$ssh?$compression_alg && ( c$ssh$compression_alg == "zlib@openssh.com" || + c$ssh$compression_alg == "zlib" ) ) + return; + + c$ssh$auth_success = F; + c$ssh$num_failures += 1; + } + +function array_to_vec(s: string_array): vector of string + { + local r: vector of string; + + for (i in s) + r[i] = s[i]; + return r; + } + +function find_client_preferred_algorithm(client_algorithms: vector of string, server_algorithms: vector of string): string + { + for ( i in client_algorithms ) + for ( j in server_algorithms ) + if ( client_algorithms[i] == server_algorithms[j] ) + return client_algorithms[i]; + } + +function find_client_preferred_algorithm_bidirectional(client_algorithms_c_to_s: vector of string, + server_algorithms_c_to_s: vector of string, + client_algorithms_s_to_c: vector of string, + server_algorithms_s_to_c: vector of string): string + { + local c_to_s = find_client_preferred_algorithm(client_algorithms_c_to_s, server_algorithms_c_to_s); + local s_to_c = find_client_preferred_algorithm(client_algorithms_s_to_c, server_algorithms_s_to_c); + + return c_to_s == s_to_c ? c_to_s : fmt("To server: %s, to client: %s", c_to_s, s_to_c); + } + +event ssh_capabilities(c: connection, cookie: string, capabilities: Capabilities) + { + if ( !c?$ssh || ( c$ssh?$capabilities && c$ssh$capabilities$is_server == capabilities$is_server ) ) + return; + + if ( !c$ssh?$capabilities ) + { + c$ssh$capabilities = capabilities; + return; + } + + local client_caps = capabilities$is_server ? c$ssh$capabilities : capabilities; + local server_caps = capabilities$is_server ? capabilities : c$ssh$capabilities; + + c$ssh$cipher_alg = find_client_preferred_algorithm_bidirectional(client_caps$encryption_algorithms_client_to_server, + server_caps$encryption_algorithms_client_to_server, + client_caps$encryption_algorithms_server_to_client, + server_caps$encryption_algorithms_server_to_client); + + c$ssh$mac_alg = find_client_preferred_algorithm_bidirectional(client_caps$mac_algorithms_client_to_server, + server_caps$mac_algorithms_client_to_server, + client_caps$mac_algorithms_server_to_client, + server_caps$mac_algorithms_server_to_client); + + c$ssh$compression_alg = find_client_preferred_algorithm_bidirectional(client_caps$compression_algorithms_client_to_server, + server_caps$compression_algorithms_client_to_server, + client_caps$compression_algorithms_server_to_client, + server_caps$compression_algorithms_server_to_client); + + c$ssh$kex_alg = find_client_preferred_algorithm(client_caps$kex_algorithms, server_caps$kex_algorithms); + c$ssh$host_key_alg = find_client_preferred_algorithm(client_caps$server_host_key_algorithms, + server_caps$server_host_key_algorithms); } event connection_state_remove(c: connection) &priority=-5 { - if ( c?$ssh && !c$ssh$logged ) + if ( c?$ssh && !c$ssh$logged && c$ssh?$client && c$ssh?$server ) + { + if ( c$ssh?$auth_success && !c$ssh$auth_success ) + c$ssh$auth_details = fmt("%d failure%s", c$ssh$num_failures, c$ssh$num_failures == 1 ? "" : "s"); + + c$ssh$logged = T; Log::write(SSH::LOG, c$ssh); + } } function generate_fingerprint(c: connection, key: string) { + if ( !c?$ssh ) + return; + local lx = str_split(md5_hash(key), vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30)); lx[0] = ""; c$ssh$host_key = sub(join_string_vec(lx, ":"), /:/, ""); @@ -195,15 +234,11 @@ function generate_fingerprint(c: connection, key: string) event ssh1_server_host_key(c: connection, p: string, e: string) { - if ( !c?$ssh ) - return; generate_fingerprint(c, e + p); } event ssh_server_host_key(c: connection, key: string) { - if ( !c?$ssh ) - return; generate_fingerprint(c, key); } diff --git a/src/analyzer/protocol/ssh/CMakeLists.txt b/src/analyzer/protocol/ssh/CMakeLists.txt index 1266e4f496..de9abeb31f 100644 --- a/src/analyzer/protocol/ssh/CMakeLists.txt +++ b/src/analyzer/protocol/ssh/CMakeLists.txt @@ -6,6 +6,7 @@ include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DI bro_plugin_begin(Bro SSH) bro_plugin_cc(SSH.cc Plugin.cc) + bro_plugin_bif(types.bif) bro_plugin_bif(events.bif) bro_plugin_pac(ssh.pac ssh-analyzer.pac ssh-protocol.pac) bro_plugin_end() \ No newline at end of file diff --git a/src/analyzer/protocol/ssh/SSH.cc b/src/analyzer/protocol/ssh/SSH.cc index 19ca99417c..4cf05c8c54 100644 --- a/src/analyzer/protocol/ssh/SSH.cc +++ b/src/analyzer/protocol/ssh/SSH.cc @@ -6,6 +6,7 @@ #include "Reporter.h" +#include "types.bif.h" #include "events.bif.h" using namespace analyzer::SSH; @@ -18,10 +19,10 @@ SSH_Analyzer::SSH_Analyzer(Connection* c) interp = new binpac::SSH::SSH_Conn(this); had_gap = false; auth_decision_made = false; - num_encrypted_packets_seen = 0; - initial_client_packet_size = 0; - initial_server_packet_size = 0; - } + skipped_banner = false; + service_accept_size = 0; + userauth_failure_size = 0; + } SSH_Analyzer::~SSH_Analyzer() { @@ -59,7 +60,13 @@ void SSH_Analyzer::DeliverStream(int len, const u_char* data, bool orig) if ( interp->get_state(orig) == binpac::SSH::ENCRYPTED ) { - ProcessEncrypted(len, orig); + if ( ssh_encrypted_packet ) + BifEvent::generate_ssh_encrypted_packet(interp->bro_analyzer(), interp->bro_analyzer()->Conn(), + orig, len); + + if ( !auth_decision_made ) + ProcessEncrypted(len, orig); + return; } @@ -69,7 +76,6 @@ void SSH_Analyzer::DeliverStream(int len, const u_char* data, bool orig) } catch ( const binpac::Exception& e ) { - printf("Binpac exception: %s\n", e.c_msg()); ProtocolViolation(fmt("Binpac exception: %s", e.c_msg())); } } @@ -83,84 +89,65 @@ void SSH_Analyzer::Undelivered(uint64 seq, int len, bool orig) void SSH_Analyzer::ProcessEncrypted(int len, bool orig) { - if (orig && !initial_client_packet_size) - initial_client_packet_size = len; - if (!orig && !initial_server_packet_size) - initial_server_packet_size = len; - - int relative_len; - if (orig) - relative_len = len - initial_client_packet_size; - else - relative_len = len - initial_server_packet_size; - - if ( !auth_decision_made && ( num_encrypted_packets_seen > 3 ) ) + // We're interested in messages from the server for SSH2 + if (!orig && (interp->get_version() == binpac::SSH::SSH2)) { - int auth_result = AuthResult(relative_len, orig, interp->get_version()); - if ( auth_result > 0 ) + // The first thing we see and want to know is the length of + // SSH_MSG_SERVICE_REQUEST, which has a fixed (decrypted) size + // of 24 bytes (17 for content pad-aligned to 8-byte + // boundaries) + if (!service_accept_size) + { + service_accept_size = len; + return; + } + + // If our user can authenticate via the "none" method, this + // packet will be a SSH_MSG_USERAUTH_SUCCESS, which has a + // fixed (decrypted) size of 8 bytes (1 for content + // pad-aligned to 8-byte boundaries). relative_len would be + // -16. + if (!userauth_failure_size && (len + 16 == service_accept_size)) { auth_decision_made = true; - if ( auth_result == 1 ) - BifEvent::generate_ssh_auth_successful(interp->bro_analyzer(), - interp->bro_analyzer()->Conn(), - len, - packet_n_1_size, - packet_n_2_size); - if ( auth_result == 2 ) - BifEvent::generate_ssh_auth_failed(interp->bro_analyzer(), - interp->bro_analyzer()->Conn(), - len, - packet_n_1_size, - packet_n_2_size); + if ( ssh_auth_successful ) + BifEvent::generate_ssh_auth_successful(interp->bro_analyzer(), interp->bro_analyzer()->Conn(), true); + return; + } + + // Normally, this packet would be a SSH_MSG_USERAUTH_FAILURE + // message, with a variable length, depending on the + // authentication methods the server supports. If it's too + // big, it might contain a pre-auth MOTD/banner, so we'll just + // skip it. + if (!userauth_failure_size) + { + if ( !skipped_banner && (len - service_accept_size) > 256 ) + { + skipped_banner = true; + return; + } + userauth_failure_size = len; + return; + } + + // If we've already seen a failure, let's see if this is + // another packet of the same size. + if (len == userauth_failure_size) + { + if ( ssh_auth_failed ) + BifEvent::generate_ssh_auth_failed(interp->bro_analyzer(), interp->bro_analyzer()->Conn()); + return; + } + + // ...or a success packet. + if (len - service_accept_size == -16) + { + auth_decision_made = true; + if ( ssh_auth_successful ) + BifEvent::generate_ssh_auth_successful(interp->bro_analyzer(), interp->bro_analyzer()->Conn(), false); + return; } } - if ( ( num_encrypted_packets_seen >= 2 ) && - ( orig != packet_n_1_is_orig ) ) - { - packet_n_2_is_orig = packet_n_1_is_orig; - packet_n_2_size = packet_n_1_size; - } - if ( num_encrypted_packets_seen == 0 ) - num_encrypted_packets_seen = 1; - else if ( orig == packet_n_1_is_orig ) - packet_n_1_size += len; - else - { - packet_n_1_is_orig = orig; - packet_n_1_size = relative_len; - if ( ! ( ( interp->get_version() == binpac::SSH::SSH1 ) && len > 90 ) ) - num_encrypted_packets_seen++; - } - } - - -int SSH_Analyzer::AuthResult(int len, bool orig, int version) - { - if ( version == binpac::SSH::SSH2 ) - { - if ( !orig && packet_n_1_is_orig && !packet_n_2_is_orig ) - { - if ( len == -16 ) - return 1; - else if ( len >= 16 && len <= 32 ) - return 2; - return 0; - } - } - else if ( version == binpac::SSH::SSH1 ) - { - // On a successful login, the server sends a longer message - if ( !orig && len > 0 ) - { - // To verify a public key, the server sends back a message of the same size - // as the previous one. Ignore that occurrence here. - if ( ! ( packet_n_1_is_orig && ( len == packet_n_1_size ) ) ) - return 1; - } - // If we've seen too many messages without a longer message, treat it as a failure - if ( num_encrypted_packets_seen > 7 ) - return 2; - } - return -1; } diff --git a/src/analyzer/protocol/ssh/SSH.h b/src/analyzer/protocol/ssh/SSH.h index 185e83d4de..af6dcbf090 100644 --- a/src/analyzer/protocol/ssh/SSH.h +++ b/src/analyzer/protocol/ssh/SSH.h @@ -31,22 +31,16 @@ protected: binpac::SSH::SSH_Conn* interp; void ProcessEncrypted(int len, bool orig); - int AuthResult(int len, bool orig, int version); bool had_gap; // Packet analysis stuff bool auth_decision_made; + bool skipped_banner; - int initial_client_packet_size; - int initial_server_packet_size; - int num_encrypted_packets_seen; + int service_accept_size; + int userauth_failure_size; - bool packet_n_1_is_orig; - int packet_n_1_size; - bool packet_n_2_is_orig; - int packet_n_2_size; - }; } } // namespace analyzer::* diff --git a/src/analyzer/protocol/ssh/events.bif b/src/analyzer/protocol/ssh/events.bif index 7505dfd6db..4a8d959de4 100644 --- a/src/analyzer/protocol/ssh/events.bif +++ b/src/analyzer/protocol/ssh/events.bif @@ -2,11 +2,13 @@ event ssh_server_version%(c: connection, version: string%); event ssh_client_version%(c: connection, version: string%); -event ssh_auth_successful%(c: connection, last_packet_len: int, middle_packet_len: int, first_packet_len: int%); +event ssh_auth_successful%(c: connection, auth_method_none: bool%); -event ssh_auth_failed%(c: connection, last_packet_len: int, middle_packet_len: int, first_packet_len: int%); +event ssh_auth_failed%(c: connection%); -event ssh_server_capabilities%(c: connection, kex_algorithms: string, server_host_key_algorithms: string, encryption_algorithms_client_to_server: string, encryption_algorithms_server_to_client: string, mac_algorithms_client_to_server: string, mac_algorithms_server_to_client: string, compression_algorithms_client_to_server: string, compression_algorithms_server_to_client: string, languages_client_to_server: string, languages_server_to_client: string%); +event ssh_encrypted_packet%(c: connection, orig: bool, len: count%); + +event ssh_capabilities%(c: connection, cookie: string, capabilities: SSH::Capabilities%); event ssh_server_host_key%(c: connection, key: string%); diff --git a/src/analyzer/protocol/ssh/ssh-analyzer.pac b/src/analyzer/protocol/ssh/ssh-analyzer.pac index 1af96a4c5c..0a76b7c8f4 100644 --- a/src/analyzer/protocol/ssh/ssh-analyzer.pac +++ b/src/analyzer/protocol/ssh/ssh-analyzer.pac @@ -1,3 +1,53 @@ +%extern{ +#include +#include +#include +%} + +%header{ +VectorVal* name_list_to_vector(const bytestring nl); +%} + +%code{ +// Copied from IRC_Analyzer::SplitWords +VectorVal* name_list_to_vector(const bytestring nl) + { + VectorVal* vv = new VectorVal(internal_type("string_vec")->AsVectorType()); + + string name_list = std_str(nl); + if ( name_list.size() < 1 ) + return vv; + + unsigned int start = 0; + unsigned int split_pos = 0; + + while ( name_list[start] == ',' ) + { + ++start; + ++split_pos; + } + + string word; + while ( (split_pos = name_list.find(',', start)) < name_list.size() ) + { + word = name_list.substr(start, split_pos - start); + if ( word.size() > 0 && word[0] != ',' ) + vv->Assign(vv->Size(), new StringVal(word)); + + start = split_pos + 1; + } + + // Add line end if needed. + if ( start < name_list.size() ) + { + word = name_list.substr(start, name_list.size() - start); + vv->Assign(vv->Size(), new StringVal(word)); + } + + return vv; + } +%} + refine flow SSH_Flow += { function proc_ssh_version(msg: SSH_Version): bool %{ @@ -18,20 +68,26 @@ refine flow SSH_Flow += { function proc_ssh_kexinit(msg: SSH_KEXINIT): bool %{ - if ( ssh_server_capabilities ) + if ( ssh_capabilities ) { - BifEvent::generate_ssh_server_capabilities(connection()->bro_analyzer(), - connection()->bro_analyzer()->Conn(), - bytestring_to_val(${msg.kex_algorithms.val}), - bytestring_to_val(${msg.server_host_key_algorithms.val}), - bytestring_to_val(${msg.encryption_algorithms_client_to_server.val}), - bytestring_to_val(${msg.encryption_algorithms_server_to_client.val}), - bytestring_to_val(${msg.mac_algorithms_client_to_server.val}), - bytestring_to_val(${msg.mac_algorithms_server_to_client.val}), - bytestring_to_val(${msg.compression_algorithms_client_to_server.val}), - bytestring_to_val(${msg.compression_algorithms_server_to_client.val}), - bytestring_to_val(${msg.languages_client_to_server.val}), - bytestring_to_val(${msg.languages_server_to_client.val})); + RecordVal* result = new RecordVal(BifType::Record::SSH::Capabilities); + result->Assign(0, name_list_to_vector(${msg.kex_algorithms.val})); + result->Assign(1, name_list_to_vector(${msg.server_host_key_algorithms.val})); + result->Assign(2, name_list_to_vector(${msg.encryption_algorithms_client_to_server.val})); + result->Assign(3, name_list_to_vector(${msg.encryption_algorithms_server_to_client.val})); + result->Assign(4, name_list_to_vector(${msg.mac_algorithms_client_to_server.val})); + result->Assign(5, name_list_to_vector(${msg.mac_algorithms_server_to_client.val})); + result->Assign(6, name_list_to_vector(${msg.compression_algorithms_client_to_server.val})); + result->Assign(7, name_list_to_vector(${msg.compression_algorithms_server_to_client.val})); + if ( ${msg.languages_client_to_server.len} ) + result->Assign(8, name_list_to_vector(${msg.languages_client_to_server.val})); + if ( ${msg.languages_server_to_client.len} ) + result->Assign(9, name_list_to_vector(${msg.languages_server_to_client.val})); + result->Assign(10, new Val(${msg.is_orig}, TYPE_BOOL)); + + BifEvent::generate_ssh_capabilities(connection()->bro_analyzer(), + connection()->bro_analyzer()->Conn(), bytestring_to_val(${msg.cookie}), + result); } return true; %} @@ -49,7 +105,7 @@ refine flow SSH_Flow += { function proc_ssh1_server_host_key(p: bytestring, e: bytestring): bool %{ - if ( ssh_server_host_key ) + if ( ssh1_server_host_key ) { BifEvent::generate_ssh1_server_host_key(connection()->bro_analyzer(), connection()->bro_analyzer()->Conn(), @@ -75,10 +131,6 @@ refine typeattr SSH_KEXINIT += &let { proc: bool = $context.flow.proc_ssh_kexinit(this); }; -refine typeattr SSH_DH_GEX_REPLY += &let { - proc: bool = $context.flow.proc_ssh_server_host_key(k_s.val); -}; - refine typeattr SSH1_Message += &let { proc_newkeys: bool = $context.flow.proc_newkeys() &if(msg_type == SSH_CMSG_SESSION_KEY); }; @@ -87,6 +139,14 @@ refine typeattr SSH2_Message += &let { proc_newkeys: bool = $context.flow.proc_newkeys() &if(msg_type == MSG_NEWKEYS); }; +refine typeattr SSH_DH_GEX_REPLY += &let { + proc: bool = $context.flow.proc_ssh_server_host_key(k_s.val); +}; + +refine typeattr SSH_ECC_REPLY += &let { + proc: bool = $context.flow.proc_ssh_server_host_key(k_s.val); +}; + refine typeattr SSH1_PUBLIC_KEY += &let { - proc: bool = $context.flow.proc_ssh1_server_host_key(host_key_p.val, host_key_e.val); + proc: bool = $context.flow.proc_ssh1_server_host_key(host_key_p.val, host_key_e.val); }; \ No newline at end of file diff --git a/src/analyzer/protocol/ssh/ssh-protocol.pac b/src/analyzer/protocol/ssh/ssh-protocol.pac index 3e1ec95ecc..36bfcaedf3 100644 --- a/src/analyzer/protocol/ssh/ssh-protocol.pac +++ b/src/analyzer/protocol/ssh/ssh-protocol.pac @@ -5,104 +5,143 @@ enum version { }; enum state { - VERSION_EXCHANGE = 0, - KEY_EXCHANGE_CLEARTEXT = 1, - ENCRYPTED = 2, + VERSION_EXCHANGE = 0, + KEX_INIT = 1, + KEX_DH_GEX = 2, + KEX_DH = 3, + KEX_ECC = 4, + KEX_GSS = 5, + KEX_RSA = 6, + ENCRYPTED = 7, +}; + +# diffie-hellman-group1-sha1 [RFC4253] Section 8.1 +# diffie-hellman-group14-sha1 [RFC4253] Section 8.2 +enum KEX_DH_message_id { + SSH_MSG_KEXDH_INIT = 30, + SSH_MSG_KEXDH_REPLY = 31, +}; + +# diffie-hellman-group-exchange-sha1 [RFC4419] Section 4.1 +# diffie-hellman-group-exchange-sha256 [RFC4419] Section 4.2 +enum KEX_DH_GEX_message_id { + SSH_MSG_KEX_DH_GEX_REQUEST_OLD = 30, + SSH_MSG_KEX_DH_GEX_GROUP = 31, + SSH_MSG_KEX_DH_GEX_INIT = 32, + SSH_MSG_KEX_DH_GEX_REPLY = 33, + SSH_MSG_KEX_DH_GEX_REQUEST = 34, +}; + +# rsa1024-sha1 [RFC4432] +# rsa2048-sha256 [RFC4432] +enum KEX_RSA_message_id { + SSH_MSG_KEXRSA_PUBKEY = 30, + SSH_MSG_KEXRSA_SECRET = 31, + SSH_MSG_KEXRSA_DONE = 32, +}; + +# gss-group1-sha1-* [RFC4462] Section 2.3 +# gss-group14-sha1-* [RFC4462] Section 2.4 +# gss-gex-sha1-* [RFC4462] Section 2.5 +# gss-* [RFC4462] Section 2.6 +enum KEX_GSS_message_id { + SSH_MSG_KEXGSS_INIT = 30, + SSH_MSG_KEXGSS_CONTINUE = 31, + SSH_MSG_KEXGSS_COMPLETE = 32, + SSH_MSG_KEXGSS_HOSTKEY = 33, + SSH_MSG_KEXGSS_ERROR = 34, + SSH_MSG_KEXGSS_GROUPREQ = 40, + SSH_MSG_KEXGSS_GROUP = 41, +}; + +# ecdh-sha2-* [RFC5656] +enum KEX_ECDH_message_id { + SSH_MSG_KEX_ECDH_INIT = 30, + SSH_MSG_KEX_ECDH_REPLY = 31, +}; + +# ecmqv-sha2 [RFC5656] +enum KEX_ECMQV_message_id { + SSH_MSG_ECMQV_INIT = 30, + SSH_MSG_ECMQV_REPLY = 31, }; enum ssh1_message_id { - SSH_MSG_NONE = 0, - SSH_MSG_DISCONNECT = 1, - SSH_SMSG_PUBLIC_KEY = 2, - SSH_CMSG_SESSION_KEY = 3, - SSH_CMSG_USER = 4, - SSH_CMSG_AUTH_RHOSTS = 5, - SSH_CMSG_AUTH_RSA = 6, - SSH_SMSG_AUTH_RSA_CHALLENGE = 7, - SSH_CMSG_AUTH_RSA_RESPONSE = 8, - SSH_CMSG_AUTH_PASSWORD = 9, - SSH_CMSG_REQUEST_PTY = 10, - SSH_CMSG_WINDOW_SIZE = 11, - SSH_CMSG_EXEC_SHELL = 12, - SSH_CMSG_EXEC_CMD = 13, - SSH_SMSG_SUCCESS = 14, - SSH_SMSG_FAILURE = 15, - SSH_CMSG_STDIN_DATA = 16, - SSH_SMSG_STDOUT_DATA = 17, - SSH_SMSG_STDERR_DATA = 18, - SSH_CMSG_EOF = 19, - SSH_SMSG_EXITSTATUS = 20, + SSH_MSG_NONE = 0, + SSH_MSG_DISCONNECT = 1, + SSH_SMSG_PUBLIC_KEY = 2, + SSH_CMSG_SESSION_KEY = 3, + SSH_CMSG_USER = 4, + SSH_CMSG_AUTH_RHOSTS = 5, + SSH_CMSG_AUTH_RSA = 6, + SSH_SMSG_AUTH_RSA_CHALLENGE = 7, + SSH_CMSG_AUTH_RSA_RESPONSE = 8, + SSH_CMSG_AUTH_PASSWORD = 9, + SSH_CMSG_REQUEST_PTY = 10, + SSH_CMSG_WINDOW_SIZE = 11, + SSH_CMSG_EXEC_SHELL = 12, + SSH_CMSG_EXEC_CMD = 13, + SSH_SMSG_SUCCESS = 14, + SSH_SMSG_FAILURE = 15, + SSH_CMSG_STDIN_DATA = 16, + SSH_SMSG_STDOUT_DATA = 17, + SSH_SMSG_STDERR_DATA = 18, + SSH_CMSG_EOF = 19, + SSH_SMSG_EXITSTATUS = 20, SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 21, SSH_MSG_CHANNEL_OPEN_FAILURE = 22, - SSH_MSG_CHANNEL_DATA = 23, - SSH_MSG_CHANNEL_CLOSE = 24, + SSH_MSG_CHANNEL_DATA = 23, + SSH_MSG_CHANNEL_CLOSE = 24, SSH_MSG_CHANNEL_CLOSE_CONFIRMATION = 25, SSH_CMSG_X11_REQUEST_FORWARDING_OLD = 26, - SSH_SMSG_X11_OPEN = 27, + SSH_SMSG_X11_OPEN = 27, SSH_CMSG_PORT_FORWARD_REQUEST = 28, - SSH_MSG_PORT_OPEN = 29, + SSH_MSG_PORT_OPEN = 29, SSH_CMSG_AGENT_REQUEST_FORWARDING = 30, - SSH_SMSG_AGENT_OPEN = 31, - SSH_MSG_IGNORE = 32, - SSH_CMSG_EXIT_CONFIRMATION = 33, + SSH_SMSG_AGENT_OPEN = 31, + SSH_MSG_IGNORE = 32, + SSH_CMSG_EXIT_CONFIRMATION = 33, SSH_CMSG_X11_REQUEST_FORWARDING = 34, - SSH_CMSG_AUTH_RHOSTS_RSA = 35, - SSH_MSG_DEBUG = 36, + SSH_CMSG_AUTH_RHOSTS_RSA = 35, + SSH_MSG_DEBUG = 36, SSH_CMSG_REQUEST_COMPRESSION = 37, - SSH_CMSG_MAX_PACKET_SIZE = 38, - SSH_CMSG_AUTH_TIS = 39, - SSH_SMSG_AUTH_TIS_CHALLENGE = 40, - SSH_CMSG_AUTH_TIS_RESPONSE = 41, - SSH_CMSG_AUTH_KERBEROS = 42, + SSH_CMSG_MAX_PACKET_SIZE = 38, + SSH_CMSG_AUTH_TIS = 39, + SSH_SMSG_AUTH_TIS_CHALLENGE = 40, + SSH_CMSG_AUTH_TIS_RESPONSE = 41, + SSH_CMSG_AUTH_KERBEROS = 42, SSH_SMSG_AUTH_KERBEROS_RESPONSE = 43, - SSH_CMSG_HAVE_KERBEROS_TGT = 44, + SSH_CMSG_HAVE_KERBEROS_TGT = 44, }; enum ssh2_message_id { - MSG_DISCONNECT = 1, - MSG_IGNORE = 2, - MSG_UNIMPLEMENTED = 3, - MSG_DEBUG = 4, - MSG_SERVICE_REQUEST = 5, - MSG_SERVICE_ACCEPT = 6, - MSG_KEXINIT = 20, - MSG_NEWKEYS = 21, - MSG_KEX_DH_GEX_REQUEST_OLD = 30, - MSG_KEX_DH_GEX_GROUP = 31, - MSG_KEX_DH_GEX_INIT = 32, - MSG_KEX_DH_GEX_REPLY = 33, - MSG_KEX_DH_GEX_REQUEST = 34, - MSG_USERAUTH_REQUEST = 50, - MSG_USERAUTH_FAILURE = 51, - MSG_USERAUTH_SUCCESS = 52, - MSG_USERAUTH_BANNER = 53, - MSG_GLOBAL_REQUEST = 80, - MSG_REQUEST_SUCCESS = 81, - MSG_REQUEST_FAILURE = 82, - MSG_CHANNEL_OPEN = 90, - MSG_CHANNEL_OPEN_CONFIRMATION = 91, - MSG_CHANNEL_OPEN_FAILURE = 92, - MSG_CHANNEL_WINDOW_ADJUST = 93, - MSG_CHANNEL_DATA = 94, - MSG_CHANNEL_EXTENDED_DATA = 95, - MSG_CHANNEL_EOF = 96, - MSG_CHANNEL_CLOSE = 97, - MSG_CHANNEL_REQUEST = 98, - MSG_CHANNEL_SUCCESS = 99, - MSG_CHANNEL_FAILURE = 100, + MSG_DISCONNECT = 1, + MSG_IGNORE = 2, + MSG_UNIMPLEMENTED = 3, + MSG_DEBUG = 4, + MSG_SERVICE_REQUEST = 5, + MSG_SERVICE_ACCEPT = 6, + MSG_KEXINIT = 20, + MSG_NEWKEYS = 21, }; +## SSH Generic + type SSH_PDU(is_orig: bool) = case $context.connection.get_state(is_orig) of { - VERSION_EXCHANGE -> version: SSH_Version(is_orig); - KEY_EXCHANGE_CLEARTEXT -> kex: SSH_Key_Exchange(is_orig); - ENCRYPTED -> ciphertext: bytestring &length=1 &transient; + VERSION_EXCHANGE -> version: SSH_Version(is_orig); + KEX_INIT -> kex: SSH_Key_Exchange(is_orig); + KEX_DH_GEX -> kex_dh_gex: SSH_Key_Exchange_DH_GEX(is_orig); + KEX_DH -> kex_dh: SSH_Key_Exchange_DH(is_orig); + KEX_ECC -> kex_ecc: SSH_Key_Exchange_ECC(is_orig); + KEX_GSS -> kex_gss: SSH_Key_Exchange_GSS(is_orig); + KEX_RSA -> kex_rsa: SSH_Key_Exchange_RSA(is_orig); } &byteorder=bigendian; type SSH_Version(is_orig: bool) = record { version: bytestring &oneline; pad: bytestring &length=0 &transient; } &let { - update_state : bool = $context.connection.update_state(KEY_EXCHANGE_CLEARTEXT, is_orig); + update_state : bool = $context.connection.update_state(KEX_INIT, is_orig); update_version: bool = $context.connection.update_version(version, is_orig); }; @@ -111,6 +150,8 @@ type SSH_Key_Exchange(is_orig: bool) = case $context.connection.get_version() of SSH2 -> ssh2_msg: SSH2_Key_Exchange(is_orig); }; +## SSH1 + type SSH1_Key_Exchange(is_orig: bool) = record { packet_length: uint32; pad_fill : bytestring &length = 8 - (packet_length % 8); @@ -119,20 +160,6 @@ type SSH1_Key_Exchange(is_orig: bool) = record { crc : uint32; } &length = packet_length + 4 + 8 - (packet_length % 8); -type SSH2_Key_Exchange_Header = record { - packet_length : uint32; - padding_length: uint8; - msg_type : uint8; -} &let { - payload_length: uint32 = packet_length - padding_length - 2; -} &length=6; - -type SSH2_Key_Exchange(is_orig: bool) = record { - header : SSH2_Key_Exchange_Header; - payload : SSH2_Message(is_orig, header.msg_type, header.payload_length); - pad : bytestring &length=header.padding_length; -} &length=header.packet_length + 4; - type SSH1_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of { SSH_SMSG_PUBLIC_KEY -> public_key: SSH1_PUBLIC_KEY(length); SSH_CMSG_SESSION_KEY -> session_key: SSH1_SESSION_KEY(length); @@ -160,19 +187,35 @@ type SSH1_SESSION_KEY(length: uint32) = record { flags : uint32; } &length=length; +type ssh1_mp_int = record { + len: uint16; + val: bytestring &length=(len+7)/8; +}; + +## SSH2 + +type SSH2_Key_Exchange_Header = record { + packet_length : uint32; + padding_length: uint8; + msg_type : uint8; +} &let { + payload_length: uint32 = packet_length - padding_length - 2; +} &length=6; + +type SSH2_Key_Exchange(is_orig: bool) = record { + header : SSH2_Key_Exchange_Header; + payload : SSH2_Message(is_orig, header.msg_type, header.payload_length); + pad : bytestring &length=header.padding_length; +} &length=header.packet_length + 4; + type SSH2_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of { - MSG_KEXINIT -> kexinit: SSH_KEXINIT(length); - MSG_KEX_DH_GEX_REQUEST -> dh_gex_request: SSH_DH_GEX_REQUEST(length); - MSG_KEX_DH_GEX_REQUEST_OLD -> dh_gex_request_old: SSH_DH_GEX_REQUEST_OLD(length); - MSG_KEX_DH_GEX_GROUP -> dh_gex_group: SSH_DH_GEX_GROUP(length); - MSG_KEX_DH_GEX_INIT -> dh_gex_init: SSH_DH_GEX_INIT(length); - MSG_KEX_DH_GEX_REPLY -> dh_gex_reply: SSH_DH_GEX_REPLY(length); - MSG_NEWKEYS -> new_keys: bytestring &length=length; + MSG_KEXINIT -> kexinit: SSH_KEXINIT(length, is_orig); + default -> unknown: bytestring &length=length; } &let { detach: bool = $context.connection.update_state(ENCRYPTED, is_orig) &if(msg_type == MSG_NEWKEYS); }; -type SSH_KEXINIT(length: uint32) = record { +type SSH_KEXINIT(length: uint32, is_orig: bool) = record { cookie : bytestring &length=16; kex_algorithms : ssh_string; server_host_key_algorithms : ssh_string; @@ -182,21 +225,52 @@ type SSH_KEXINIT(length: uint32) = record { mac_algorithms_server_to_client : ssh_string; compression_algorithms_client_to_server : ssh_string; compression_algorithms_server_to_client : ssh_string; - languages_client_to_server : ssh_string; - languages_server_to_client : ssh_string; - first_kex_packet_follows : uint8; - reserved : uint32; + languages_client_to_server : ssh_string; + languages_server_to_client : ssh_string; + first_kex_packet_follows : uint8; + reserved : uint32; +} &let { + proc_kex : bool = $context.connection.update_kex(kex_algorithms.val, is_orig); } &length=length; -type SSH_DH_GEX_REQUEST(length: uint32) = record { +# KEX_DH exchanges + +type SSH_Key_Exchange_DH(is_orig: bool) = record { + header : SSH2_Key_Exchange_Header; + payload : SSH_Key_Exchange_DH_Message(is_orig, header.msg_type, header.payload_length); + pad : bytestring &length=header.padding_length; +} &length=header.packet_length + 4; + +type SSH_Key_Exchange_DH_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of { + SSH_MSG_KEXDH_INIT -> init : SSH_DH_GEX_INIT(length); + SSH_MSG_KEXDH_REPLY -> reply : SSH_DH_GEX_REPLY(length); +}; + +# KEX_DH_GEX exchanges + +type SSH_Key_Exchange_DH_GEX(is_orig: bool) = record { + header : SSH2_Key_Exchange_Header; + payload : SSH_Key_Exchange_DH_GEX_Message(is_orig, header.msg_type, header.payload_length); + pad : bytestring &length=header.padding_length; +} &length=header.packet_length + 4; + +type SSH_Key_Exchange_DH_GEX_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of { + SSH_MSG_KEX_DH_GEX_REQUEST_OLD -> request_old : SSH_DH_GEX_REQUEST_OLD; + SSH_MSG_KEX_DH_GEX_REQUEST -> request : SSH_DH_GEX_REQUEST; + SSH_MSG_KEX_DH_GEX_GROUP -> group : SSH_DH_GEX_GROUP(length); + SSH_MSG_KEX_DH_GEX_INIT -> init : SSH_DH_GEX_INIT(length); + SSH_MSG_KEX_DH_GEX_REPLY -> reply : SSH_DH_GEX_REPLY(length); +}; + +type SSH_DH_GEX_REQUEST = record { min: uint32; n : uint32; max: uint32; } &length=12; -type SSH_DH_GEX_REQUEST_OLD(length: uint32) = record { - payload: bytestring &length=length; -} &length=length; +type SSH_DH_GEX_REQUEST_OLD = record { + n: uint32; +} &length=4; type SSH_DH_GEX_GROUP(length: uint32) = record { p: ssh_string; @@ -213,9 +287,106 @@ type SSH_DH_GEX_REPLY(length: uint32) = record { signature: ssh_string; } &length=length; -type ssh1_mp_int = record { - len: uint16; - val: bytestring &length=(len+7)/8; +# KEX_RSA exchanges + +type SSH_Key_Exchange_RSA(is_orig: bool) = record { + header : SSH2_Key_Exchange_Header; + payload : SSH_Key_Exchange_RSA_Message(is_orig, header.msg_type, header.payload_length); + pad : bytestring &length=header.padding_length; +} &length=header.packet_length + 4; + +type SSH_Key_Exchange_RSA_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of { + SSH_MSG_KEXRSA_PUBKEY -> pubkey : SSH_RSA_PUBKEY(length); + SSH_MSG_KEXRSA_SECRET -> secret : SSH_RSA_SECRET(length); + SSH_MSG_KEXRSA_DONE -> done : SSH_RSA_DONE(length); +}; + +type SSH_RSA_PUBKEY(length: uint32) = record { + k_s: ssh_string; + k_t: ssh_string; +} &length=length; + +type SSH_RSA_SECRET(length: uint32) = record { + encrypted_payload: ssh_string; +} &length=length; + +type SSH_RSA_DONE(length: uint32) = record { + signature: ssh_string; +} &length=length; + +# KEX_GSS exchanges + +type SSH_Key_Exchange_GSS(is_orig: bool) = record { + header : SSH2_Key_Exchange_Header; + payload : SSH_Key_Exchange_GSS_Message(is_orig, header.msg_type, header.payload_length); + pad : bytestring &length=header.padding_length; +} &length=header.packet_length + 4; + +type SSH_Key_Exchange_GSS_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of { + SSH_MSG_KEXGSS_INIT -> init : SSH_GSS_INIT(length); + SSH_MSG_KEXGSS_CONTINUE -> cont : SSH_GSS_CONTINUE(length); + SSH_MSG_KEXGSS_COMPLETE -> complete : SSH_GSS_COMPLETE(length); + SSH_MSG_KEXGSS_HOSTKEY -> hostkey : SSH_GSS_HOSTKEY(length); + SSH_MSG_KEXGSS_ERROR -> error : SSH_GSS_ERROR(length); + SSH_MSG_KEXGSS_GROUPREQ -> groupreq : SSH_DH_GEX_REQUEST; + SSH_MSG_KEXGSS_GROUP -> group : SSH_DH_GEX_GROUP(length); +}; + +type SSH_GSS_INIT(length: uint32) = record { + output_token: ssh_string; + e : ssh_string; +} &length=length; + +type SSH_GSS_CONTINUE(length: uint32) = record { + output_token: ssh_string; +} &length=length; + +type SSH_GSS_COMPLETE(length: uint32) = record { + f : ssh_string; + per_msg_token : ssh_string; + have_token : uint8; + parse_token : case have_token of { + 0 -> no_token: empty; + default -> token: ssh_string; + }; +} &length=length; + +type SSH_GSS_HOSTKEY(length: uint32) = record { + k_s: ssh_string; +} &length=length; + +type SSH_GSS_ERROR(length: uint32) = record { + major_status: uint32; + minor_status: uint32; + message : ssh_string; + language : ssh_string; +} &length=length; + +# KEX_ECDH and KEX_ECMQV exchanges + +type SSH_Key_Exchange_ECC(is_orig: bool) = record { + header : SSH2_Key_Exchange_Header; + payload : SSH_Key_Exchange_ECC_Message(is_orig, header.msg_type, header.payload_length); + pad : bytestring &length=header.padding_length; +} &length=header.packet_length + 4; + +type SSH_Key_Exchange_ECC_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of { + SSH_MSG_KEX_ECDH_INIT -> init : SSH_ECC_INIT(length); + SSH_MSG_KEX_ECDH_REPLY -> reply : SSH_ECC_REPLY(length); +}; + +# This deviates from the RFC. SSH_MSG_KEX_ECDH_INIT and +# SSH_MSG_KEX_ECMQV_INIT can be parsed the same way. +type SSH_ECC_INIT(length: uint32) = record { + q_c: ssh_string; +}; + +# This deviates from the RFC. SSH_MSG_KEX_ECDH_REPLY and +# SSH_MSG_KEX_ECMQV_REPLY can be parsed the same way. +type SSH_ECC_REPLY(length: uint32) = record { + k_s : ssh_string; + q_s : ssh_string; + signature : ssh_string; }; type ssh_string = record { @@ -223,17 +394,35 @@ type ssh_string = record { val: bytestring &length=len; }; +type ssh_host_key = record { + len: uint32; + key_type: ssh_string; + key: ssh_string; +} &length=(len + 4); + +## Done with types + refine connection SSH_Conn += { %member{ int state_up_; int state_down_; int version_; + + bool kex_orig_; + bool kex_seen_; + bytestring kex_algs_cache_; + bytestring kex_algorithm_; %} %init{ state_up_ = VERSION_EXCHANGE; state_down_ = VERSION_EXCHANGE; version_ = UNK; + + kex_seen_ = false; + kex_orig_ = false; + kex_algs_cache_ = bytestring(); + kex_algorithm_ = bytestring(); %} function get_state(is_orig: bool): int @@ -274,4 +463,103 @@ refine connection SSH_Conn += { return true; %} + function update_kex_state_if_equal(s: string, to_state: state): bool + %{ + if ( strcmp(c_str(kex_algorithm_), s.c_str()) == 0 ) + { + update_state(to_state, true); + update_state(to_state, false); + return true; + } + return false; + %} + + function update_kex_state_if_startswith(s: string, to_state: state): bool + %{ + if ( (uint) kex_algorithm_.length() < s.length() ) + return false; + + if ( strcmp(std_str(kex_algorithm_).substr(0, s.length()).c_str(), s.c_str()) == 0 ) + { + update_state(to_state, true); + update_state(to_state, false); + return true; + } + return false; + %} + + function update_kex(algs: bytestring, orig: bool): bool + %{ + if ( !kex_seen_ ) + { + kex_seen_ = true; + kex_orig_ = orig; + kex_algs_cache_.init(${algs}.data(), ${algs}.length()); + + return false; + } + else if ( kex_orig_ == orig ) + return false; + + VectorVal* client_list = name_list_to_vector(orig ? algs : kex_algs_cache_); + VectorVal* server_list = name_list_to_vector(orig ? kex_algs_cache_ : algs); + + for ( unsigned int i = 0; i < client_list->Size(); ++i) + { + for ( unsigned int j = 0; j < server_list->Size(); ++j) + { + if ( strcmp((const char *) client_list->Lookup(i)->AsStringVal()->Bytes(), + (const char *) server_list->Lookup(j)->AsStringVal()->Bytes()) == 0 ) + { + kex_algorithm_.init((const uint8 *) client_list->Lookup(i)->AsStringVal()->Bytes(), + client_list->Lookup(i)->AsStringVal()->Len()); + + // UNTESTED + if ( update_kex_state_if_equal("rsa1024-sha1", KEX_RSA) ) + return true; + // UNTESTED + if ( update_kex_state_if_equal("rsa2048-sha256", KEX_RSA) ) + return true; + + // UNTESTED + if ( update_kex_state_if_equal("diffie-hellman-group1-sha1", KEX_DH) ) + return true; + // UNTESTED + if ( update_kex_state_if_equal("diffie-hellman-group14-sha1", KEX_DH) ) + return true; + + if ( update_kex_state_if_equal("diffie-hellman-group-exchange-sha1", KEX_DH_GEX) ) + return true; + if ( update_kex_state_if_equal("diffie-hellman-group-exchange-sha256", KEX_DH_GEX) ) + return true; + + if ( update_kex_state_if_startswith("gss-group1-sha1-", KEX_GSS) ) + return true; + if ( update_kex_state_if_startswith("gss-group14-sha1-", KEX_GSS) ) + return true; + if ( update_kex_state_if_startswith("gss-gex-sha1-", KEX_GSS) ) + return true; + if ( update_kex_state_if_startswith("gss-", KEX_GSS) ) + return true; + + if ( update_kex_state_if_startswith("ecdh-sha2-", KEX_ECC) ) + return true; + if ( update_kex_state_if_equal("ecmqv-sha2", KEX_ECC) ) + return true; + if ( update_kex_state_if_equal("curve25519-sha256@libssh.org", KEX_ECC) ) + return true; + + + bro_analyzer()->Weird(fmt("ssh_unknown_kex_algorithm=%s", c_str(kex_algorithm_))); + return true; + + } + } + } + + return true; + + %} + + }; \ No newline at end of file diff --git a/src/analyzer/protocol/ssh/ssh.pac b/src/analyzer/protocol/ssh/ssh.pac index b3181c4fa1..2358f056da 100644 --- a/src/analyzer/protocol/ssh/ssh.pac +++ b/src/analyzer/protocol/ssh/ssh.pac @@ -8,6 +8,7 @@ %include bro.pac %extern{ + #include "types.bif.h" #include "events.bif.h" %} diff --git a/src/analyzer/protocol/ssh/types.bif b/src/analyzer/protocol/ssh/types.bif new file mode 100644 index 0000000000..38e51600f3 --- /dev/null +++ b/src/analyzer/protocol/ssh/types.bif @@ -0,0 +1,5 @@ +module SSH; + +type Capabilities: record; + +module GLOBAL; \ No newline at end of file