From: Akhilesh MY (amuttuva) Date: Fri, 21 Nov 2025 06:49:10 +0000 (+0000) Subject: Pull request #4960: ssh: support fields for extractor X-Git-Tag: 3.10.0.0~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=427f01a0874d710154dfc75f2a7bba6a5efcee3d;p=thirdparty%2Fsnort3.git Pull request #4960: ssh: support fields for extractor Merge in SNORT/snort3 from ~AMUTTUVA/snort3:ssh_ext to master Squashed commit of the following: commit 0179324498cb13d08a1a23b44eee55ce1fa92e19 Author: Akhilesh MY Date: Mon Oct 27 05:37:19 2025 -0400 ssh: support fields for extractor --- diff --git a/src/pub_sub/CMakeLists.txt b/src/pub_sub/CMakeLists.txt index a6b0c5582..3d4ec80aa 100644 --- a/src/pub_sub/CMakeLists.txt +++ b/src/pub_sub/CMakeLists.txt @@ -53,6 +53,7 @@ add_library( pub_sub OBJECT http_transaction_end_event.cc quic_events.cc sip_events.cc + ssh_events.cc ) install (FILES ${PUB_SUB_INCLUDES} diff --git a/src/pub_sub/ssh_events.cc b/src/pub_sub/ssh_events.cc new file mode 100644 index 000000000..2be6f5d9e --- /dev/null +++ b/src/pub_sub/ssh_events.cc @@ -0,0 +1,73 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- +// ssh_events.cc author Cisco + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "ssh_events.h" + +SshEventType SshEvent::get_event_type() const +{ return event_type; } + +SshValidationResult SshEvent::get_validation_result() const +{ return result; } + +const std::string& SshEvent::get_version_str() const +{ return version_str; } + +uint8_t SshEvent::get_direction() const +{ return direction; } + +const snort::Packet* SshEvent::get_packet() const +{ return packet; } + +const char* SshEvent::get_login_direction() const +{ return login_direction; } + +uint8_t SshEvent::get_ssh_version() const +{ return ssh_version; } + +const char* SshAlgoEvent::get_kex_algorithms() const +{ return algos.named.kex_algorithms; } + +const char* SshAlgoEvent::get_server_host_key_algorithms() const +{ return algos.named.server_host_key_algorithms; } + +const char* SshAlgoEvent::get_encryption_algorithms_client_to_server() const +{ return algos.named.encryption_algorithms_client_to_server; } + +const char* SshAlgoEvent::get_encryption_algorithms_server_to_client() const +{ return algos.named.encryption_algorithms_server_to_client; } + +const char* SshAlgoEvent::get_mac_algorithms_client_to_server() const +{ return algos.named.mac_algorithms_client_to_server; } + +const char* SshAlgoEvent::get_mac_algorithms_server_to_client() const +{ return algos.named.mac_algorithms_server_to_client; } + +const char* SshAlgoEvent::get_compression_algorithms_client_to_server() const +{ return algos.named.compression_algorithms_client_to_server; } + +const char* SshAlgoEvent::get_compression_algorithms_server_to_client() const +{ return algos.named.compression_algorithms_server_to_client; } + +uint8_t SshAlgoEvent::get_direction() const +{ return direction; } + diff --git a/src/pub_sub/ssh_events.h b/src/pub_sub/ssh_events.h index cc83f154c..fa778a940 100644 --- a/src/pub_sub/ssh_events.h +++ b/src/pub_sub/ssh_events.h @@ -24,9 +24,9 @@ // for use by data bus subscribers #include "framework/data_bus.h" -#include "service_inspectors/ssh/ssh.h" +#include "service_inspectors/ssh/ssh_types.h" -struct SshEventIds { enum : unsigned { STATE_CHANGE, num_ids }; }; +struct SshEventIds { enum : unsigned { STATE_CHANGE, ALGORITHM, num_ids }; }; const snort::PubKey ssh_pub_key { "ssh", SshEventIds::num_ids }; @@ -44,28 +44,24 @@ enum SshValidationResult SSH_INVALID_KEXINIT }; -class SshEvent : public snort::DataEvent +class SO_PUBLIC SshEvent : public snort::DataEvent { public: SshEvent(const SshEventType event_type, const SshValidationResult result, - const std::string& version_str, const uint8_t direction, const snort::Packet* packet) : - event_type(event_type), result(result), version_str(version_str), direction(direction), packet(packet) + const std::string& version_str, const uint8_t direction, + const snort::Packet* packet, const char* login_direction, uint8_t ssh_version) : + event_type(event_type), result(result), version_str(version_str), + direction(direction), packet(packet), login_direction(login_direction), + ssh_version(ssh_version) { } - SshEventType get_event_type() const - { return event_type; } - - SshValidationResult get_validation_result() const - { return result; } - - const std::string& get_version_str() const - { return version_str; } - - uint8_t get_direction() const - { return direction; } - - const snort::Packet* get_packet() const override - { return packet; } + SshEventType get_event_type() const; + SshValidationResult get_validation_result() const; + const std::string& get_version_str() const; + uint8_t get_direction() const; + const snort::Packet* get_packet() const override; + const char* get_login_direction() const; + uint8_t get_ssh_version() const; private: const SshEventType event_type; @@ -73,6 +69,45 @@ private: const std::string version_str; const uint8_t direction; const snort::Packet* packet; + const char* login_direction; + const uint8_t ssh_version; +}; + +class SO_PUBLIC SshAlgoEvent : public snort::DataEvent +{ +public: + union Algorithms + { + struct + { + const char* kex_algorithms; + const char* server_host_key_algorithms; + const char* encryption_algorithms_client_to_server; + const char* encryption_algorithms_server_to_client; + const char* mac_algorithms_client_to_server; + const char* mac_algorithms_server_to_client; + const char* compression_algorithms_client_to_server; + const char* compression_algorithms_server_to_client; + } named; + const char* unnamed[NUM_KEXINIT_LISTS]; + }; + + SshAlgoEvent(const Algorithms& algos, uint8_t dir) : algos(algos), direction(dir) + { } + + const char* get_kex_algorithms() const; + const char* get_server_host_key_algorithms() const; + const char* get_encryption_algorithms_client_to_server() const; + const char* get_encryption_algorithms_server_to_client() const; + const char* get_mac_algorithms_client_to_server() const; + const char* get_mac_algorithms_server_to_client() const; + const char* get_compression_algorithms_client_to_server() const; + const char* get_compression_algorithms_server_to_client() const; + uint8_t get_direction() const; + +private: + const Algorithms& algos; + uint8_t direction; }; #endif diff --git a/src/pub_sub/test/CMakeLists.txt b/src/pub_sub/test/CMakeLists.txt index e1047026e..0e2eb58a4 100644 --- a/src/pub_sub/test/CMakeLists.txt +++ b/src/pub_sub/test/CMakeLists.txt @@ -21,6 +21,11 @@ add_cpputest( pub_sub_eve_process_event_test SOURCES ../eve_process_event.h ) +add_cpputest( pub_sub_ssh_events_test + SOURCES + ../ssh_events.cc +) + add_cpputest( pub_sub_http_transaction_end_event_test SOURCES ../http_transaction_end_event.cc diff --git a/src/pub_sub/test/pub_sub_ssh_events_test.cc b/src/pub_sub/test/pub_sub_ssh_events_test.cc new file mode 100644 index 000000000..d398bf52e --- /dev/null +++ b/src/pub_sub/test/pub_sub_ssh_events_test.cc @@ -0,0 +1,117 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// pub_sub_ssh_events_test.cc author Cisco + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "pub_sub/ssh_events.h" +#include "service_inspectors/ssh/ssh.h" +#include "protocols/packet.h" + +#include +#include + +using namespace snort; + +namespace snort +{ +Packet::Packet(bool) + : flow(nullptr), packet_flags(0), xtradata_mask(0), proto_bits(0), alt_dsize(0), num_layers(0), + ip_proto_next(IpProtocol::PROTO_NOT_SET), disable_inspect(true), sect(PS_NONE), active_inst(nullptr), pkth(nullptr), + pkt(nullptr), layers(nullptr), user_inspection_policy_id(0), user_ips_policy_id(0), user_network_policy_id(0), + inspection_started_timestamp(0), vlan_idx(0), ts_packet_flags(0), allocated(false), daq_msg(nullptr) +{ } +Packet::~Packet() = default; +} + +TEST_GROUP(pub_sub_ssh_events_test) { }; + +TEST(pub_sub_ssh_events_test, ssh_algo_event_complete_access) +{ + const char* kex_algos = "Diffie-sha256,Diffie-sha512"; + const char* host_key_algos = "SSH2-sha512,SSH1-sha256,SSHFP"; + const char* cipher_c2s = "ctrk-SHA256,ctrl-sha512"; + const char* cipher_s2c = "XSHA256,XSHA1"; + const char* mac_c2s = "HMAC-SHA256,HMAC-sha512"; + const char* mac_s2c = "HMAC-sha512,HMAC-SHA256"; + const char* comp_c2s = "none,OpenSSH"; + const char* comp_s2c = "none,OpenSSH"; + + SshAlgoEvent::Algorithms algos; + memset(&algos, 0, sizeof(algos)); + + algos.named.kex_algorithms = kex_algos; + algos.named.server_host_key_algorithms = host_key_algos; + algos.named.encryption_algorithms_client_to_server = cipher_c2s; + algos.named.encryption_algorithms_server_to_client = cipher_s2c; + algos.named.mac_algorithms_client_to_server = mac_c2s; + algos.named.mac_algorithms_server_to_client = mac_s2c; + algos.named.compression_algorithms_client_to_server = comp_c2s; + algos.named.compression_algorithms_server_to_client = comp_s2c; + + SshAlgoEvent event(algos, PKT_FROM_CLIENT); + + CHECK(event.get_direction() == PKT_FROM_CLIENT); + + CHECK(event.get_kex_algorithms() == kex_algos); + CHECK(event.get_server_host_key_algorithms() == host_key_algos); + CHECK(event.get_encryption_algorithms_client_to_server() == cipher_c2s); + CHECK(event.get_encryption_algorithms_server_to_client() == cipher_s2c); + CHECK(event.get_mac_algorithms_client_to_server() == mac_c2s); + CHECK(event.get_mac_algorithms_server_to_client() == mac_s2c); + CHECK(event.get_compression_algorithms_client_to_server() == comp_c2s); + CHECK(event.get_compression_algorithms_server_to_client() == comp_s2c); +} + +TEST(pub_sub_ssh_events_test, ssh_state_change_event) +{ + Packet test_packet; + const std::string version_str = "SSH-2.0-OpenSSH_8.0"; + + SshEvent inbound_event(SSH_VERSION_STRING, SSH_NOT_FINISHED, version_str, + PKT_FROM_CLIENT, &test_packet, "inbound", SSH_VERSION_2); + + CHECK(inbound_event.get_event_type() == SSH_VERSION_STRING); + CHECK(inbound_event.get_validation_result() == SSH_NOT_FINISHED); + CHECK(inbound_event.get_version_str() == version_str); + CHECK(inbound_event.get_direction() == PKT_FROM_CLIENT); + CHECK(inbound_event.get_packet() == &test_packet); + STRCMP_EQUAL("inbound", inbound_event.get_login_direction()); + CHECK(inbound_event.get_ssh_version() == SSH_VERSION_2); + + SshEvent outbound_event(SSH_VALIDATION, SSH_VALID_KEXINIT, version_str, + PKT_FROM_SERVER, &test_packet, "outbound", SSH_VERSION_1); + + CHECK(outbound_event.get_event_type() == SSH_VALIDATION); + CHECK(outbound_event.get_validation_result() == SSH_VALID_KEXINIT); + CHECK(outbound_event.get_direction() == PKT_FROM_SERVER); + CHECK(outbound_event.get_packet() == &test_packet); + STRCMP_EQUAL("outbound", outbound_event.get_login_direction()); + CHECK(outbound_event.get_ssh_version() == SSH_VERSION_1); +} + +int main(int argc, char** argv) +{ + return CommandLineTestRunner::RunAllTests(argc, argv); +} diff --git a/src/service_inspectors/ssh/CMakeLists.txt b/src/service_inspectors/ssh/CMakeLists.txt index b73bf25bf..255141767 100644 --- a/src/service_inspectors/ssh/CMakeLists.txt +++ b/src/service_inspectors/ssh/CMakeLists.txt @@ -1,4 +1,8 @@ +set (SSH_INCLUDES + ssh_types.h +) + set( FILE_LIST ssh.cc ssh.h @@ -7,6 +11,7 @@ set( FILE_LIST ssh_module.h ssh_splitter.cc ssh_splitter.h + ${SSH_INCLUDES} ) if (STATIC_INSPECTORS) @@ -16,3 +21,7 @@ else (STATIC_INSPECTORS) add_dynamic_module(ssh inspectors ${FILE_LIST}) endif (STATIC_INSPECTORS) + +install(FILES ${SSH_INCLUDES} + DESTINATION "${INCLUDE_INSTALL_PATH}/service_inspectors/ssh/" +) diff --git a/src/service_inspectors/ssh/ssh.cc b/src/service_inspectors/ssh/ssh.cc index c410cfd3f..be23d90a8 100644 --- a/src/service_inspectors/ssh/ssh.cc +++ b/src/service_inspectors/ssh/ssh.cc @@ -136,20 +136,31 @@ static void snort_ssh(SSH_PROTO_CONF* config, Packet* p) if (!(sessp->state_flags & SSH_FLG_SESS_ENCRYPTED)) { + const char* login_direction = ""; // If server and client have not performed the protocol // version exchange yet, must look for version strings. if (!(sessp->state_flags & search_dir_ver)) { + const SfIp* server_ip = &p->flow->server_ip; + const SfIp* client_ip = &p->flow->client_ip; + + if (server_ip->is_private() != client_ip->is_private()) + { + login_direction = server_ip->is_private() ? "inbound" : "outbound"; + } + bool valid_version = process_ssh_version_string(config, sessp, p, direction); if (valid_version) { std::string proto_string((const char *)(p->data), p->dsize); - SshEvent event(SSH_VERSION_STRING, SSH_NOT_FINISHED, proto_string, pkt_direction, p); + SshEvent event(SSH_VERSION_STRING, SSH_NOT_FINISHED, proto_string, pkt_direction, p, login_direction, + sessp->version); DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow); } else { - SshEvent event(SSH_VALIDATION, SSH_INVALID_VERSION, "", pkt_direction, p); + SshEvent event(SSH_VALIDATION, SSH_INVALID_VERSION, "", pkt_direction, p, login_direction, + sessp->version); DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow); if (sessp->version == NON_SSH_TRAFFIC) sessp->ssh_aborted = true; @@ -173,12 +184,14 @@ static void snort_ssh(SSH_PROTO_CONF* config, Packet* p) } if (keyx_valid) { - SshEvent event(SSH_VALIDATION, SSH_VALID_KEXINIT, "", pkt_direction, p); + SshEvent event(SSH_VALIDATION, SSH_VALID_KEXINIT, "", pkt_direction, p, login_direction, + sessp->version); DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow); } else { - SshEvent event(SSH_VALIDATION, SSH_INVALID_KEXINIT, "", pkt_direction, p); + SshEvent event(SSH_VALIDATION, SSH_INVALID_KEXINIT, "", pkt_direction, p, login_direction, + sessp->version); DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow); sessp->state_flags |= SSH_FLG_SESS_ENCRYPTED; } @@ -423,18 +436,27 @@ static bool process_ssh2_kexinit(SSHData *sessionp, Packet *p, uint8_t direction default: return false; } + + SshAlgoEvent::Algorithms algos; + uint16_t total_length = sizeof(SSH2KeyExchange); const uint8_t *data = p->data + sizeof(SSH2KeyExchange); for (int i = 0; i < NUM_KEXINIT_LISTS; i++) { - uint32_t list_length = ntohl(*((const uint32_t*)data)) + sizeof(uint32_t); - if (list_length > ssh_length or total_length + list_length > ssh_length) + uint32_t list_length = ntohl(*((const uint32_t*)data)); + uint32_t total_list_length = list_length + sizeof(uint32_t); + + if (total_list_length > ssh_length or total_length + total_list_length > ssh_length) { DetectionEngine::queue_event(GID_SSH, SSH_EVENT_PAYLOAD_SIZE); return false; } - total_length += list_length; - data += list_length; + + if (list_length) + algos.unnamed[i] = (const char*)(data + sizeof(uint32_t)); + + total_length += total_list_length; + data += total_list_length; } total_length += sizeof(SSHKeyExchangeFinal) + ssh_pkt->msg.plen; if (total_length != ssh_length) @@ -448,6 +470,14 @@ static bool process_ssh2_kexinit(SSHData *sessionp, Packet *p, uint8_t direction // using an unsupported future version return false; } + + if (ssh_pkt->msg.code == SSH_MSG_KEXINIT) + { + uint8_t pkt_direction = direction == SSH_DIR_FROM_CLIENT ? PKT_FROM_CLIENT : PKT_FROM_SERVER; + SshAlgoEvent event(algos, pkt_direction); + DataBus::publish(pub_id, SshEventIds::ALGORITHM, event, p->flow); + } + return true; } diff --git a/src/service_inspectors/ssh/ssh.h b/src/service_inspectors/ssh/ssh.h index 5e1003a14..bca0a93bc 100644 --- a/src/service_inspectors/ssh/ssh.h +++ b/src/service_inspectors/ssh/ssh.h @@ -32,6 +32,7 @@ #include "flow/flow.h" #include "protocols/packet.h" +#include "ssh_types.h" // FIXIT-L move these to ssh.cc // Session state flags for SSHData::state_flags @@ -124,7 +125,6 @@ public: #define SSH_PACKET_MAX_SIZE 35000 #define SSH_MAX_BANNER_LEN 255 #define SSH2_COOKIE_SIZE 16 -#define NUM_KEXINIT_LISTS 10 #define SSH_MIN_BANNER_LEN 9 //SSH-2.0-*\n #define SSH1_KEYX_MIN_SIZE (4 + 8 + 1) // length + padding + message diff --git a/src/service_inspectors/ssh/ssh_types.h b/src/service_inspectors/ssh/ssh_types.h new file mode 100644 index 000000000..28b26ae7f --- /dev/null +++ b/src/service_inspectors/ssh/ssh_types.h @@ -0,0 +1,26 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ssh_types.h - author Cisco + +#ifndef SSH_TYPES_H +#define SSH_TYPES_H + +constexpr int NUM_KEXINIT_LISTS = 10; + +#endif