From: Shibin K V (shikv) Date: Tue, 7 Oct 2025 13:38:28 +0000 (+0000) Subject: Pull request #4896: Doh initial X-Git-Tag: 3.9.7.0~25 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=417c9346b133b1008896084971df4a640ed9f2ea;p=thirdparty%2Fsnort3.git Pull request #4896: Doh initial Merge in SNORT/snort3 from ~SHIKV/snort3:doh_initial to master Squashed commit of the following: commit bf26dd87ba5532b379784ff8f4c8b7dee26b8001 Author: shibin k v Date: Thu Sep 18 11:44:41 2025 -0500 stream_tcp: copy all layers from original packet during pseudo packet creation commit b16a92f10481ad99d4196e80c8bed0fb67262e96 Author: shibin k v Date: Wed Sep 3 07:56:16 2025 -0500 appid, http_inspect, dns: add support for DNS over HTTPS and DNS over QUIC --- diff --git a/src/network_inspectors/appid/CMakeLists.txt b/src/network_inspectors/appid/CMakeLists.txt index 2d84c1936..58b2b0799 100644 --- a/src/network_inspectors/appid/CMakeLists.txt +++ b/src/network_inspectors/appid/CMakeLists.txt @@ -213,7 +213,9 @@ set ( APPID_SOURCES appid_cpu_profile_table.cc appid_cpu_profile_table.h user_data_map.h - user_data_map.cc + user_data_map.cc + appid_dns_payload_event_handler.cc + appid_dns_payload_event_handler.h ) #if (STATIC_INSPECTORS) diff --git a/src/network_inspectors/appid/appid_dns_payload_event_handler.cc b/src/network_inspectors/appid/appid_dns_payload_event_handler.cc new file mode 100644 index 000000000..2309f5a14 --- /dev/null +++ b/src/network_inspectors/appid/appid_dns_payload_event_handler.cc @@ -0,0 +1,126 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2025-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. +//-------------------------------------------------------------------------- + +// appid_dns_payload_event_handler.cc author Shibin K V + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "appid_dns_payload_event_handler.h" + +#include "flow/stream_flow.h" +#include "profiler/profiler_defs.h" +#include "pub_sub/dns_payload_event.h" + +#include "appid_dns_session.h" +#include "appid_inspector.h" +#include "detector_plugins/detector_dns.h" + +using namespace snort; + +void AppIdDnsPayloadEventHandler::handle(DataEvent& event, Flow* flow) +{ + Packet *p = DetectionEngine::get_current_packet(); + if(!flow or !p) + return; + DnsPayloadEvent* dns_payload_event = (DnsPayloadEvent*)&event; + int32_t payload_length = 0; + const uint8_t* dns_payload = dns_payload_event->get_payload(payload_length); + bool is_udp = dns_payload_event->is_dns_udp(); + AppIdSession* asd = appid_api.get_appid_session(*flow); + if (!dns_payload or payload_length <= 0 or !dns_payload_event->is_last_piece() or !asd) + return; + AppidChangeBits change_bits; + // Skip sessions using old odp context after reload detectors + if (!pkt_thread_odp_ctxt or + (pkt_thread_odp_ctxt->get_version() != asd->get_odp_ctxt_version())) + return; + + bool is_appid_cpu_profiling_running = (asd->get_odp_ctxt().is_appid_cpu_profiler_running()); + Stopwatch per_appid_event_cpu_timer; + + if (is_appid_cpu_profiling_running) + per_appid_event_cpu_timer.start(); + + AppidSessionDirection dir = dns_payload_event->is_from_client() ? APP_ID_FROM_INITIATOR : APP_ID_FROM_RESPONDER; + AppIdDiscoveryArgs args(dns_payload, payload_length, dir, *asd, nullptr, change_bits); + APPID_LOG(p, TRACE_DEBUG_LEVEL, "Processing DNS-%s payload event\n", is_udp ? "UDP" : "TCP"); + int rval; + if (is_udp) + { + if (!dns_udp_detector) + return; + rval = static_cast(dns_udp_detector)->validate_doh(args); + } + else + { + if (!dns_tcp_detector) + return; + rval = static_cast(dns_tcp_detector)->validate_doq(args); + } + APPID_LOG(p, TRACE_DEBUG_LEVEL, "DNS-%s detector returned %d\n", + is_udp ? "UDP" : "TCP", rval); + + if (rval == APPID_SUCCESS || rval == APPID_INPROCESS) + { + AppId service_id = asd->pick_service_app_id(); + if (service_id == APP_ID_HTTP2 || service_id == APP_ID_HTTP3) + { + if (flow->stream_intf) + { + int64_t stream_id = -1; + flow->stream_intf->get_stream_id(flow, stream_id); + if (stream_id != -1) + { + AppIdHttpSession* hsession = asd->get_matching_http_session(stream_id); + if (hsession) + hsession->set_payload(APP_ID_DNS, change_bits, "body"); + } + else + { + assert(false); + return; // we should not reach here + } + } + } + else + { + AppIdHttpSession* hsession = asd->get_http_session(); + if (hsession) + hsession->set_payload(APP_ID_DNS, change_bits, "body"); + if (asd->get_payload_id() != APP_ID_DNS) + asd->set_payload_appid_data(APP_ID_DNS, nullptr); + asd->set_ss_application_ids_payload(APP_ID_DNS, change_bits); + } + AppIdDnsSession* dsession = asd->get_dns_session(); + assert(dsession); + if (!dsession) + return; + dsession->set_doh(true); + asd->publish_appid_event(change_bits, *p); + // TODO: add DNS hostname based appid detection + } + + if (is_appid_cpu_profiling_running) + { + per_appid_event_cpu_timer.stop(); + asd->stats.processing_time += TO_USECS(per_appid_event_cpu_timer.get()); + } +} + diff --git a/src/network_inspectors/appid/appid_dns_payload_event_handler.h b/src/network_inspectors/appid/appid_dns_payload_event_handler.h new file mode 100644 index 000000000..5e4ddf17d --- /dev/null +++ b/src/network_inspectors/appid/appid_dns_payload_event_handler.h @@ -0,0 +1,58 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2025-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. +//-------------------------------------------------------------------------- + +// appid_doh_event_handler.h author Shibin K V + +#ifndef APPID_DNS_PAYLOAD_EVENT_HANDLER_H +#define APPID_DNS_PAYLOAD_EVENT_HANDLER_H + +#include "appid_detector.h" +#include "appid_inspector.h" +#include "appid_module.h" + +class AppIdDnsPayloadEventHandler : public snort::DataHandler +{ +public: + AppIdDnsPayloadEventHandler(AppIdInspector& inspector) : + DataHandler(MOD_NAME) + { + order = 100; // to make sure appid receives events before DNS inspector + AppIdDetectors *appid_udp_detectors = nullptr; + AppIdDetectors *appid_tcp_detectors = nullptr; + appid_udp_detectors = inspector.get_ctxt().get_odp_ctxt().get_service_disco_mgr().get_udp_detectors(); + appid_tcp_detectors = inspector.get_ctxt().get_odp_ctxt().get_service_disco_mgr().get_tcp_detectors(); + assert(appid_udp_detectors); + if (!appid_udp_detectors) + return; + auto udp_detector = appid_udp_detectors->find("DNS-UDP"); + if (udp_detector != appid_udp_detectors->end()) + dns_udp_detector = udp_detector->second; + auto tcp_detector = appid_tcp_detectors->find("DNS-TCP"); + if (tcp_detector != appid_tcp_detectors->end()) + dns_tcp_detector = tcp_detector->second; + } + + void handle(snort::DataEvent& event, snort::Flow* flow) override; + +private: + AppIdDetector *dns_udp_detector = nullptr; + AppIdDetector *dns_tcp_detector = nullptr; +}; + +#endif // APPID_DNS_PAYLOAD_EVENT_HANDLER_H + diff --git a/src/network_inspectors/appid/appid_dns_session.h b/src/network_inspectors/appid/appid_dns_session.h index eeeccc43f..b20e1cef8 100644 --- a/src/network_inspectors/appid/appid_dns_session.h +++ b/src/network_inspectors/appid/appid_dns_session.h @@ -23,6 +23,7 @@ #define APPID_DNS_SESSION_H #include + #include "pub_sub/appid_events.h" #define DNS_GOT_QUERY 0x01 @@ -44,6 +45,7 @@ public: record_type = 0; ttl = 0; options_offset = 0; + doh = false; } uint8_t get_state() const @@ -109,6 +111,12 @@ public: void set_options_offset(uint16_t optionsOffset) { options_offset = optionsOffset; } + void set_doh(bool Doh) + { doh = Doh; } + + bool is_doh() const + { return doh; } + protected: uint8_t state = 0; uint8_t response_type = 0; @@ -118,5 +126,6 @@ protected: std::string host; uint16_t host_offset = 0; uint16_t options_offset = 0; + bool doh = false; }; #endif diff --git a/src/network_inspectors/appid/appid_inspector.cc b/src/network_inspectors/appid/appid_inspector.cc index 5e5113691..0c1f28b9f 100644 --- a/src/network_inspectors/appid/appid_inspector.cc +++ b/src/network_inspectors/appid/appid_inspector.cc @@ -35,6 +35,7 @@ #include "packet_io/packet_tracer.h" #include "profiler/profiler.h" #include "pub_sub/appid_event_ids.h" +#include "pub_sub/dns_events.h" #include "pub_sub/intrinsic_event_ids.h" #include "appid_cip_event_handler.h" @@ -42,6 +43,7 @@ #include "appid_dcerpc_event_handler.h" #include "appid_debug.h" #include "appid_discovery.h" +#include "appid_dns_payload_event_handler.h" #include "appid_eve_process_event_handler.h" #include "appid_ha.h" #include "appid_http_event_handler.h" @@ -150,6 +152,7 @@ bool AppIdInspector::configure(SnortConfig* sc) new HttpEventHandler(HttpEventHandler::RESPONSE_EVENT, *this), *sc); DataBus::subscribe_global(http_pub_key, HttpEventIds::REQUEST_BODY, new AppIdHttpXReqBodyEventHandler(), *sc); + DataBus::subscribe_global(intrinsic_pub_key, IntrinsicEventIds::DNS_PAYLOAD, new AppIdDnsPayloadEventHandler(*this), *sc); DataBus::subscribe_global(sip_pub_key, SipEventIds::DIALOG, new SipEventHandler(*this), *sc); DataBus::subscribe_global(dce_tcp_pub_key, DceTcpEventIds::EXP_SESSION, new DceExpSsnEventHandler(), *sc); DataBus::subscribe_global(ssh_pub_key, SshEventIds::STATE_CHANGE, new SshEventHandler(), *sc); diff --git a/src/network_inspectors/appid/detector_plugins/detector_dns.cc b/src/network_inspectors/appid/detector_plugins/detector_dns.cc index 851997b23..f850bb8dd 100644 --- a/src/network_inspectors/appid/detector_plugins/detector_dns.cc +++ b/src/network_inspectors/appid/detector_plugins/detector_dns.cc @@ -567,6 +567,27 @@ int DnsValidator::validate_packet(const uint8_t* data, uint16_t size, const int, return APPID_SUCCESS; } +int DnsUdpServiceDetector::validate_doh(AppIdDiscoveryArgs& args) +{ + int rval; + + if (!args.size) + return APPID_INPROCESS; + + if (args.size < sizeof(DNSHeader)) + return APPID_NOMATCH; + + if ((rval = dns_validate_header(args.dir, (const DNSHeader*)args.data, + args.asd.get_odp_ctxt().dns_host_reporting, args.asd)) != APPID_SUCCESS) + return rval; + + // Coverity doesn't realize that validate_packet() checks the packet data for valid values + // coverity[tainted_scalar] + rval = validate_packet(args.data, args.size, args.dir, + args.asd.get_odp_ctxt().dns_host_reporting, args.asd, args.change_bits); + return rval; +} + int DnsUdpServiceDetector::validate(AppIdDiscoveryArgs& args) { int rval; @@ -652,6 +673,125 @@ inprocess: return rval; } } +int DnsTcpServiceDetector::validate_doq(AppIdDiscoveryArgs& args) +{ + int rval; + uint8_t* reallocated_data = nullptr; + const uint8_t* data = args.data; + uint16_t size = args.size; + ServiceDNSData* dd = static_cast(data_get(args.asd)); + { + if (!args.size) + goto inprocess; + + if (args.size < sizeof(DNSTCPHeader)) + { + if (args.dir == APP_ID_FROM_INITIATOR) + goto not_compatible; + else + goto fail; + } + + if (!dd) + { + dd = new ServiceDNSData; + data_add(args.asd, dd); + } + + if (dd->cached_data and dd->cached_len and args.dir == APP_ID_FROM_INITIATOR) + { + reallocated_data = static_cast(snort_calloc(dd->cached_len + args.size, sizeof(uint8_t))); + memcpy(reallocated_data, dd->cached_data, dd->cached_len); + memcpy(reallocated_data + dd->cached_len, args.data, args.size); + size = dd->cached_len + args.size; + dd->free_dns_cache(); + data = reallocated_data; + } + + const DNSTCPHeader* hdr = (const DNSTCPHeader*)data; + data = data + sizeof(DNSTCPHeader); + size = size - sizeof(DNSTCPHeader); + uint16_t tmp = ntohs(hdr->length); + + if (tmp > size and args.dir == APP_ID_FROM_INITIATOR) + { + dd->save_dns_cache(args.size, args.data); + goto inprocess; + } else if (tmp > size and args.dir == APP_ID_FROM_RESPONDER) { + goto not_compatible; + } + + if (tmp < sizeof(DNSHeader) || dns_validate_header(args.dir, (const DNSHeader*)data, + args.asd.get_odp_ctxt().dns_host_reporting, args.asd)) + { + if (args.dir == APP_ID_FROM_INITIATOR) + goto not_compatible; + else + goto fail; + } + + // Coverity doesn't realize that validate_packet() checks the packet data for valid values + // coverity[tainted_scalar] + rval = validate_packet(data, size, args.dir, + args.asd.get_odp_ctxt().dns_host_reporting, args.asd, args.change_bits); + if (rval != APPID_SUCCESS) + goto tcp_done; + + if (dd->state == DNS_STATE_QUERY || dd->state == DNS_STATE_MULTI_QUERY) + { + if (args.dir != APP_ID_FROM_INITIATOR) + goto fail; + dd->id = ((const DNSHeader*)data)->id; + DNSState current_state = dd->state; + dd->state = DNS_STATE_RESPONSE; + if (current_state == DNS_STATE_QUERY) + goto inprocess; + goto success; + } + else if (args.dir == APP_ID_FROM_RESPONDER && dd->id == ((const DNSHeader*)data)->id) + dd->state = DNS_STATE_MULTI_QUERY; + else + goto fail; + } + +tcp_done: + switch (rval) + { + case APPID_SUCCESS: + goto success; + case APPID_INVALID_CLIENT: + goto not_compatible; + case APPID_NOMATCH: + goto fail; + case APPID_INPROCESS: + goto inprocess; + default: + dd->free_dns_cache(); + return rval; + } + +success: + if (reallocated_data) + snort_free(reallocated_data); + args.asd.set_session_flags(APPID_SESSION_CONTINUE); + return APPID_SUCCESS; + +not_compatible: + if (reallocated_data) + snort_free(reallocated_data); + return APPID_NOT_COMPATIBLE; + +fail: + if (reallocated_data) + snort_free(reallocated_data); + return APPID_NOMATCH; + +inprocess: + if (reallocated_data) + snort_free(reallocated_data); + return APPID_INPROCESS; + +} int DnsTcpServiceDetector::validate(AppIdDiscoveryArgs& args) { diff --git a/src/network_inspectors/appid/detector_plugins/detector_dns.h b/src/network_inspectors/appid/detector_plugins/detector_dns.h index 5053892f4..529cf5ca4 100644 --- a/src/network_inspectors/appid/detector_plugins/detector_dns.h +++ b/src/network_inspectors/appid/detector_plugins/detector_dns.h @@ -49,6 +49,7 @@ public: DnsTcpServiceDetector(ServiceDiscovery*); int validate(AppIdDiscoveryArgs&) override; + int validate_doq(AppIdDiscoveryArgs& args); }; class DnsUdpServiceDetector : public ServiceDetector, public DnsValidator @@ -57,6 +58,7 @@ public: DnsUdpServiceDetector(ServiceDiscovery*); int validate(AppIdDiscoveryArgs&) override; + int validate_doh(AppIdDiscoveryArgs&); }; #endif diff --git a/src/network_inspectors/appid/detector_plugins/test/CMakeLists.txt b/src/network_inspectors/appid/detector_plugins/test/CMakeLists.txt index 4852e9173..20f357260 100644 --- a/src/network_inspectors/appid/detector_plugins/test/CMakeLists.txt +++ b/src/network_inspectors/appid/detector_plugins/test/CMakeLists.txt @@ -17,3 +17,7 @@ add_cpputest( detector_sip_test add_cpputest( detector_pop3_test SOURCES ../../../../utils/util_cstring.cc ) + +add_cpputest( detector_dns_test + SOURCES + ../../../../utils/util_cstring.cc ) \ No newline at end of file diff --git a/src/network_inspectors/appid/detector_plugins/test/detector_dns_test.cc b/src/network_inspectors/appid/detector_plugins/test/detector_dns_test.cc new file mode 100644 index 000000000..9c080c6e6 --- /dev/null +++ b/src/network_inspectors/appid/detector_plugins/test/detector_dns_test.cc @@ -0,0 +1,238 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2025-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. +//-------------------------------------------------------------------------- + +// detector_dns_test.cc author Shibin k v + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "appid_inspector.h" +#include "appid_session.h" +#include "detector_plugins_mock.h" + +#include "../detector_dns.h" +#include "../detector_dns.cc" + +#include +#include +#include + +using namespace snort; + +static ServiceDiscovery test_discovery; + +static AppIdDnsSession static_dns_session; + +AppIdDnsSession* AppIdSession::get_dns_session() const +{ + return &static_dns_session; +} + +AppIdDnsSession* AppIdSession::create_dns_session() +{ + return &static_dns_session; +} + +int ServiceDetector::incompatible_data(AppIdSession&, const snort::Packet*, AppidSessionDirection) +{ + return 0; +} +// Stubs for AppIdInspector +static ServiceDNSData dd; +AppIdConfig test_app_config; + +AppIdInspector::AppIdInspector(AppIdModule&) : config(&test_app_config), ctxt(test_app_config) { } + +void AppIdDetector::add_user(AppIdSession&, const char*, AppId, bool, AppidChangeBits&){} + +int AppIdDetector::data_add(AppIdSession&, AppIdFlowData*) { return 1; } + +AppIdFlowData* AppIdDetector::data_get(const AppIdSession&) +{ + return ⅆ +} + +int ServiceDetector::fail_service(AppIdSession& asd, const Packet* pkt, AppidSessionDirection dir) { return 1; } + +TEST_GROUP(detector_dns_doq_tests) +{ + DnsTcpServiceDetector* test_detector = nullptr; + void setup() override + { + test_detector = new DnsTcpServiceDetector(&test_discovery); + } + + void teardown() override + { + delete test_detector; + test_detector = nullptr; + dd.free_dns_cache(); + } +}; + +TEST(detector_dns_doq_tests, doq_validator_match_full_session) +{ + OdpContext test_odp_ctxt(test_app_config, nullptr); + AppIdModule test_module; + AppIdInspector test_inspector(test_module); + dd.state = DNS_STATE_QUERY; + AppIdSession test_asd(IpProtocol::TCP, nullptr, (uint16_t)0, test_inspector, test_odp_ctxt, (uint32_t)0, 0); + uint8_t dns_tcp_packet[] = { + 0x00, 0x1c, // TCP length (28 bytes) + 0x12, 0x34, // id + 0x01, 0x00, // flags + 0x00, 0x01, // QDCount + 0x00, 0x00, // ANCount + 0x00, 0x00, // NSCount + 0x00, 0x00, // ARCount + 0x03, 'w', 'w', 'w', 0x00, // "www" + 0x00, 0x01, // QType A + 0x00, 0x01 // QClass IN + }; + + AppidChangeBits change_bits; + AppIdDiscoveryArgs args(dns_tcp_packet, sizeof(dns_tcp_packet), APP_ID_FROM_INITIATOR, test_asd, nullptr, change_bits); + auto result = test_detector->validate_doq(args); + dd.free_dns_cache(); + CHECK_EQUAL(APPID_INPROCESS, result); + + // Now send a response packet + dd.state = DNS_STATE_RESPONSE; + dd.id = 13330; + uint8_t dns_tcp_response[] = { + 0x00, 0x25, // TCP length (37 bytes) + 0x12, 0x34, // id + 0x81, 0x80, // flags: standard response, no error + 0x00, 0x01, // QDCount: 1 + 0x00, 0x01, // ANCount: 1 + 0x00, 0x00, // NSCount: 0 + 0x00, 0x00, // ARCount: 0 + // Query: www + 0x03, 'w', 'w', 'w', 0x00, // QNAME: "www" + 0x00, 0x01, // QType: A + 0x00, 0x01, // QClass: IN + // Answer + 0xc0, 0x0c, // Name (pointer to offset 12) + 0x00, 0x01, // Type: A + 0x00, 0x01, // Class: IN + 0x00, 0x00, 0x00, 0x3c, // TTL: 60 + 0x00, 0x04, // RDLENGTH: 4 + 0x7f, 0x00, 0x00, 0x01 // RDATA: 127.0.0.1 + }; + + AppIdDiscoveryArgs response_args(dns_tcp_response, sizeof(dns_tcp_response), APP_ID_FROM_RESPONDER, test_asd, nullptr, change_bits); + result = test_detector->validate_doq(response_args); + + CHECK_EQUAL(APPID_SUCCESS, result); +} + +TEST(detector_dns_doq_tests, doq_validator_in_process_cached) +{ + OdpContext test_odp_ctxt(test_app_config, nullptr); + AppIdModule test_module; + AppIdInspector test_inspector(test_module); + dd.state = DNS_STATE_QUERY; + AppIdSession test_asd(IpProtocol::TCP, nullptr, (uint16_t)0, test_inspector, test_odp_ctxt, (uint32_t)0, 0); + // partial data + uint8_t dns_tcp_packet_1[] = { + 0x00, 0x1c, // TCP length (28 bytes) + 0x12, 0x34, // id + 0x01, 0x00, // flags + }; + + AppidChangeBits change_bits; + AppIdDiscoveryArgs args(dns_tcp_packet_1, sizeof(dns_tcp_packet_1), APP_ID_FROM_INITIATOR, test_asd, nullptr, change_bits); + auto result = test_detector->validate_doq(args); + CHECK_EQUAL(6, dd.cached_len); + CHECK_EQUAL(APPID_INPROCESS, result); + + uint8_t dns_tcp_packet_2[]= { + 0x00, 0x01, // QDCount + 0x00, 0x00, // ANCount + 0x00, 0x00, // NSCount + 0x00, 0x00, // ARCount + 0x03, 'w', 'w', 'w', 0x00, // "www" + 0x00, 0x01, // QType A + 0x00, 0x01 // QClass IN + }; + AppIdDiscoveryArgs args2(dns_tcp_packet_2, sizeof(dns_tcp_packet_2), APP_ID_FROM_INITIATOR, test_asd, nullptr, change_bits); + result = test_detector->validate_doq(args2); + CHECK_EQUAL(APPID_INPROCESS, result); +} + +TEST(detector_dns_doq_tests, doq_validator_not_compatible) +{ + OdpContext test_odp_ctxt(test_app_config, nullptr); + AppIdModule test_module; + AppIdInspector test_inspector(test_module); + dd.state = DNS_STATE_QUERY; + AppIdSession test_asd(IpProtocol::TCP, nullptr, (uint16_t)0, test_inspector, test_odp_ctxt, (uint32_t)0, 0); + uint8_t dns_tcp_packet[] = { + 0x00, 0x01, // TCP length (1 bytes) + 0x12, 0x34, // id + 0x01, 0x00, // flags + 0x00, 0x01, // QDCount + 0x00, 0x00, // ANCount + 0x00, 0x00, // NSCount + 0x00, 0x00, // ARCount + 0x03, 'w', 'w', 'w', 0x00, // "www" + 0x00, 0x01, // QType A + 0x00, 0x01 // QClass IN + }; + + AppidChangeBits change_bits; + AppIdDiscoveryArgs args(dns_tcp_packet, sizeof(dns_tcp_packet), APP_ID_FROM_INITIATOR, test_asd, nullptr, change_bits); + auto result = test_detector->validate_doq(args); + CHECK_EQUAL(APPID_NOT_COMPATIBLE, result); + +} + +TEST(detector_dns_doq_tests, doq_validator_no_match) +{ + OdpContext test_odp_ctxt(test_app_config, nullptr); + AppIdModule test_module; + AppIdInspector test_inspector(test_module); + dd.state = DNS_STATE_QUERY; + AppIdSession test_asd(IpProtocol::TCP, nullptr, (uint16_t)0, test_inspector, test_odp_ctxt, (uint32_t)0, 0); + uint8_t dns_tcp_packet[] = { + 0x00, 0x10, // TCP length (28 bytes) + 0x12, 0x34, // id + 0x01, 0x00, // flags + 0x00, 0x01, // QDCount + 0x00, 0x00, // ANCount + 0x00, 0x00, // NSCount + 0x00, 0x00, // ARCount + 0x03, 'w', 'w', 'w', 0x00, // "www" + 0x00, 0x01, // QType A + 0x00, 0x01 // QClass IN + }; + + AppidChangeBits change_bits; + AppIdDiscoveryArgs args(dns_tcp_packet, sizeof(dns_tcp_packet), APP_ID_FROM_RESPONDER, test_asd, nullptr, change_bits); + auto result = test_detector->validate_doq(args); + CHECK_EQUAL(APPID_NOMATCH, result); +} + + + +int main(int argc, char** argv) +{ + int return_value = CommandLineTestRunner::RunAllTests(argc, argv); + return return_value; +} diff --git a/src/pub_sub/CMakeLists.txt b/src/pub_sub/CMakeLists.txt index d5bcf597f..a6b0c5582 100644 --- a/src/pub_sub/CMakeLists.txt +++ b/src/pub_sub/CMakeLists.txt @@ -38,6 +38,7 @@ set (PUB_SUB_INCLUDES ssh_events.h ssl_events.h stream_event_ids.h + dns_payload_event.h ) add_library( pub_sub OBJECT diff --git a/src/pub_sub/dns_payload_event.h b/src/pub_sub/dns_payload_event.h new file mode 100644 index 000000000..b5538e0c7 --- /dev/null +++ b/src/pub_sub/dns_payload_event.h @@ -0,0 +1,75 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2022-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. +//-------------------------------------------------------------------------- +// dns_payload_event.h author Shibin k v +#ifndef DNS_PAYLOAD_EVENT_H +#define DNS_PAYLOAD_EVENT_H + +#include "flow/flow_stash.h" +#include "framework/data_bus.h" + +#define STASH_DNS_DATA "dns_data" + +class DnsDataStash : public snort::StashGenericObject +{ +public: + DnsDataStash(const uint8_t* data, int32_t length) + : dns_data(data), dns_data_length(length) + { } + DnsDataStash() = delete; + const uint8_t* get_data(uint32_t& length) const + { + length = dns_data_length; + return dns_data; + } + ~DnsDataStash() override = default; + +private: + const uint8_t* dns_data; + int32_t dns_data_length; +}; + +class SO_PUBLIC DnsPayloadEvent : public snort::DataEvent +{ +public: + DnsPayloadEvent(const uint8_t* dns_data, const int32_t dns_data_length, + const bool from_client, const bool is_udp, const bool last_piece) + : dns_data(dns_data), dns_data_length(dns_data_length), + from_client(from_client), is_udp(is_udp), last_piece(last_piece) + { } + DnsPayloadEvent() = delete; + const uint8_t* get_payload(int32_t& length) const + { + length = dns_data_length; + return dns_data; + } + bool is_from_client() const + { return from_client; } + bool is_last_piece() const + { return last_piece; } + bool is_dns_udp() const + { return is_udp; } + +private: + const uint8_t* dns_data; + const int32_t dns_data_length; + const bool from_client; + const bool is_udp; + const bool last_piece; +}; + +#endif // DNS_PAYLOAD_EVENT_H diff --git a/src/pub_sub/http_event_ids.h b/src/pub_sub/http_event_ids.h index febbb55e5..2b286f7e6 100644 --- a/src/pub_sub/http_event_ids.h +++ b/src/pub_sub/http_event_ids.h @@ -36,6 +36,7 @@ struct HttpEventIds RESPONSE_HEADER, REQUEST_BODY, BODY, + DOH_BODY, END_OF_TRANSACTION, HTTP_PUBLISH_LENGTH, diff --git a/src/pub_sub/intrinsic_event_ids.h b/src/pub_sub/intrinsic_event_ids.h index 9fd95fc04..aab4c206a 100644 --- a/src/pub_sub/intrinsic_event_ids.h +++ b/src/pub_sub/intrinsic_event_ids.h @@ -61,6 +61,8 @@ struct IntrinsicEventIds AUXILIARY_IP, FILE_VERDICT, + DNS_PAYLOAD, + num_ids }; }; diff --git a/src/pub_sub/test/pub_sub_http_request_body_event_test.cc b/src/pub_sub/test/pub_sub_http_request_body_event_test.cc index 5c23539a7..96eb4507d 100644 --- a/src/pub_sub/test/pub_sub_http_request_body_event_test.cc +++ b/src/pub_sub/test/pub_sub_http_request_body_event_test.cc @@ -151,7 +151,7 @@ TEST(pub_sub_http_request_body_event_test, last_event) { int32_t msg_len = 500; int32_t max_pub_len = 2000; - int32_t in_offset = REQUEST_PUBLISH_DEPTH - msg_len; + int32_t in_offset = BODY_PUBLISH_DEPTH - msg_len; int32_t length, offset; uint32_t stream_id = 3; mock().setData("stream_id", stream_id); diff --git a/src/service_inspectors/dns/CMakeLists.txt b/src/service_inspectors/dns/CMakeLists.txt index cc32d6af8..1fb89ae6b 100644 --- a/src/service_inspectors/dns/CMakeLists.txt +++ b/src/service_inspectors/dns/CMakeLists.txt @@ -9,6 +9,8 @@ set( FILE_LIST dns_rr_decoder.cc dns_splitter.cc dns_splitter.h + dns_payload_event_handler.cc + dns_payload_event_handler.h ) # if (STATIC_INSPECTORS) diff --git a/src/service_inspectors/dns/dns.cc b/src/service_inspectors/dns/dns.cc index 57c51a2e0..c86e71521 100644 --- a/src/service_inspectors/dns/dns.cc +++ b/src/service_inspectors/dns/dns.cc @@ -28,14 +28,16 @@ #include "dns.h" #include "detection/detection_engine.h" -#include "dns_config.h" #include "log/messages.h" #include "profiler/profiler.h" +#include "pub_sub/dns_events.h" +#include "pub_sub/intrinsic_event_ids.h" #include "stream/stream.h" +#include "dns_config.h" #include "dns_module.h" +#include "dns_payload_event_handler.h" #include "dns_splitter.h" -#include "pub_sub/dns_events.h" using namespace snort; @@ -56,11 +58,6 @@ const PegInfo dns_peg_names[] = { CountType::END, nullptr, nullptr } }; -/* - * Function prototype(s) - */ -static void snort_dns(Packet* p, const DnsConfig* dns_config); - unsigned DnsFlowData::inspector_id = 0; DnsFlowData::DnsFlowData() : FlowData(inspector_id) @@ -90,11 +87,11 @@ bool DNSData::has_events() const return !dns_events.empty(); } -static DNSData* SetNewDNSData(Packet* p) +static DNSData* SetNewDNSData(Packet* p, bool udp) { DnsFlowData* fd; - if (p->is_udp()) + if (udp) return nullptr; fd = new DnsFlowData; @@ -126,10 +123,10 @@ bool DNSData::valid_dns(const DNSHdr& dns_header) const return true; } -DNSData* get_dns_session_data(Packet* p, bool from_server, DNSData& udpSessionData) +DNSData* get_dns_session_data(Packet* p, bool from_server, DNSData& udpSessionData, bool udp) { DnsFlowData* fd; - if (p->is_udp()) + if (udp) { if(p->dsize > MAX_UDP_PAYLOAD) return nullptr; @@ -622,6 +619,53 @@ static uint16_t ParseDNSAnswer( return bytes_unused; } +// Get the DNS transaction ID from a UDP packet's data field +static inline uint16_t get_udp_trans_id(Packet* p) +{ + // The length of packet's data field should have already been validated + return (static_cast(p->data[0]) << 8) | static_cast(p->data[1]); +} + +// Check if the DNS transaction ID is found in the UDP packet's flow data object +static bool is_in_udp_flow(Packet* p, uint16_t trans_id) +{ + bool found = false; + DnsUdpFlowData* udp_flow_data = (DnsUdpFlowData*)((p->flow)->get_flow_data(DnsUdpFlowData::inspector_id)); + if (udp_flow_data) + found = udp_flow_data->trans_ids.find(trans_id) != udp_flow_data->trans_ids.end(); + return found; +} + +// Add DNS transaction ID to the UDP packet's flow data object +static void add_to_udp_flow(Packet* p, uint16_t trans_id) +{ + DnsUdpFlowData* udp_flow_data = (DnsUdpFlowData*)((p->flow)->get_flow_data(DnsUdpFlowData::inspector_id)); + if (!udp_flow_data) + { + udp_flow_data = new DnsUdpFlowData(); + p->flow->set_flow_data(udp_flow_data); + } + udp_flow_data->trans_ids.emplace(trans_id); +} + +// Remove DNS transaction ID from the UDP packet's flow data object +static void rm_from_udp_flow(Packet* p, uint16_t trans_id) +{ + DnsUdpFlowData* udp_flow_data = (DnsUdpFlowData*)((p->flow)->get_flow_data(DnsUdpFlowData::inspector_id)); + bool should_close = true; + if (udp_flow_data) + { + udp_flow_data->trans_ids.erase(trans_id); + should_close = udp_flow_data->trans_ids.empty(); + } + if (should_close && !p->flow->is_proxied()) + { + // Mark the UDP flow as "closed" only when all trans_ids are matched + // and removed by DNS-reply packets, or if the flow data object is not found + p->flow->session_state |= STREAM_STATE_CLOSED; + } +} + /* The following check is to look for an attempt to exploit * a vulnerability in the DNS client, per MS 06-041. * @@ -806,7 +850,7 @@ static uint16_t ParseDNSRData( return bytes_unused; } -static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& needNextPacket) +static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& needNextPacket, bool udp) { uint16_t bytes_unused = p->dsize; int i; @@ -839,7 +883,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne * if at beginning of a UDP Response. */ if ((dnsSessionData->state == DNS_RESP_STATE_LENGTH) && - p->is_udp()) + udp) { dnsSessionData->state = DNS_RESP_STATE_HDR_ID; } @@ -1082,26 +1126,6 @@ void DnsResponseFqdn::update_ttl(uint32_t ttl) // class stuff //------------------------------------------------------------------------- -class Dns : public Inspector -{ -public: - Dns(DnsModule*); - ~Dns() override; - - void eval(Packet*) override; - StreamSplitter* get_splitter(bool) override; - bool configure(snort::SnortConfig*) override; - void show(const snort::SnortConfig*) const override; - static unsigned get_pub_id() { return pub_id; } - - bool supports_no_ips() const override - { return true; } - -private: - const DnsConfig* config = nullptr; - static unsigned pub_id; -}; - unsigned Dns::pub_id = 0; Dns::Dns(DnsModule* m) @@ -1120,83 +1144,14 @@ void Dns::show(const SnortConfig*) const config->show(); } -void Dns::eval(Packet* p) -{ - // precondition - what we registered for - assert((p->is_udp() and p->dsize and p->data) or p->has_tcp_data() or p->has_udp_quic_data()); - assert(p->flow); - - ++dnsstats.packets; - snort_dns(p, config); -} - -bool Dns::configure(snort::SnortConfig*) -{ - if ( !pub_id ) - pub_id = DataBus::get_id(dns_pub_key); - - return true; -} - -StreamSplitter* Dns::get_splitter(bool c2s) -{ - return new DnsSplitter(c2s); -} - -// Get the DNS transaction ID from a UDP packet's data field -static inline uint16_t get_udp_trans_id(Packet* p) -{ - // The length of packet's data field should have already been validated - return (static_cast(p->data[0]) << 8) | static_cast(p->data[1]); -} - -// Add DNS transaction ID to the UDP packet's flow data object -static void add_to_udp_flow(Packet* p, uint16_t trans_id) -{ - DnsUdpFlowData* udp_flow_data = (DnsUdpFlowData*)((p->flow)->get_flow_data(DnsUdpFlowData::inspector_id)); - if (!udp_flow_data) - { - udp_flow_data = new DnsUdpFlowData(); - p->flow->set_flow_data(udp_flow_data); - } - udp_flow_data->trans_ids.emplace(trans_id); -} - -// Check if the DNS transaction ID is found in the UDP packet's flow data object -static bool is_in_udp_flow(Packet* p, uint16_t trans_id) -{ - bool found = false; - DnsUdpFlowData* udp_flow_data = (DnsUdpFlowData*)((p->flow)->get_flow_data(DnsUdpFlowData::inspector_id)); - if (udp_flow_data) - found = udp_flow_data->trans_ids.find(trans_id) != udp_flow_data->trans_ids.end(); - return found; -} - -// Remove DNS transaction ID from the UDP packet's flow data object -static void rm_from_udp_flow(Packet* p, uint16_t trans_id) -{ - DnsUdpFlowData* udp_flow_data = (DnsUdpFlowData*)((p->flow)->get_flow_data(DnsUdpFlowData::inspector_id)); - bool should_close = true; - if (udp_flow_data) - { - udp_flow_data->trans_ids.erase(trans_id); - should_close = udp_flow_data->trans_ids.empty(); - } - if (should_close) - { - // Mark the UDP flow as "closed" only when all trans_ids are matched - // and removed by DNS-reply packets, or if the flow data object is not found - p->flow->session_state |= STREAM_STATE_CLOSED; - } -} - -static void snort_dns(Packet* p, const DnsConfig* dns_config) +void Dns::snort_dns(Packet* p, bool udp) { // cppcheck-suppress unreadVariable Profile profile(dnsPerfStats); + ++dnsstats.packets; // For TCP, do a few extra checks... - if ( p->has_tcp_data() ) + if (!udp) { // If session picked up mid-stream, do not process further. // Would be almost impossible to tell where we are in the @@ -1213,13 +1168,13 @@ static void snort_dns(Packet* p, const DnsConfig* dns_config) DNSData udp_session_data; // Attempt to get a previously allocated DNS block. - DNSData* dnsSessionData = get_dns_session_data(p, from_server, udp_session_data); + DNSData* dnsSessionData = get_dns_session_data(p, from_server, udp_session_data, udp); if (dnsSessionData == nullptr) { // Check the stream session. If it does not currently // have our DNS data-block attached, create one. - dnsSessionData = SetNewDNSData(p); + dnsSessionData = SetNewDNSData(p, udp); if ( !dnsSessionData ) // Could not get/create the session data for this packet. @@ -1229,13 +1184,13 @@ static void snort_dns(Packet* p, const DnsConfig* dns_config) if (dnsSessionData->flags & DNS_FLAG_NOT_DNS) return; - dnsSessionData->dns_config = dns_config; + dnsSessionData->dns_config = config; if ( from_server ) { uint16_t trans_id = 0; // Always parse the response packet for TCP flows bool should_parse_response = true; - if (p->is_udp()) + if (udp) { // If this is a DNS-over-UDP flow then parse the response packet and publish events // only when the response packet's DNS transaction-ID is found in the flow data object @@ -1246,7 +1201,7 @@ static void snort_dns(Packet* p, const DnsConfig* dns_config) if (should_parse_response) { bool needNextPacket = false; - ParseDNSResponseMessage(p, dnsSessionData, needNextPacket); + ParseDNSResponseMessage(p, dnsSessionData, needNextPacket, udp); trans_id = dnsSessionData->hdr.id; if (!dnsSessionData->valid_dns(dnsSessionData->hdr)) @@ -1262,17 +1217,42 @@ static void snort_dns(Packet* p, const DnsConfig* dns_config) DataBus::publish(Dns::get_pub_id(), DnsEventIds::DNS_RESPONSE, dns_response_event, p->flow); } - if (p->is_udp()) + if (udp) rm_from_udp_flow(p, trans_id); } else { dnsstats.requests++; - if (p->is_udp()) + if (udp) add_to_udp_flow(p, get_udp_trans_id(p)); } } +void Dns::eval(Packet* p) +{ + // precondition - what we registered for + assert((p->is_udp() and p->dsize and p->data) or p->has_tcp_data() or p->has_udp_quic_data()); + assert(p->flow); + if (p->has_udp_quic_data()) // DNS over QUIC follows DNS over TCP + snort_dns(p, false); + else + snort_dns(p, p->is_udp()); +} + +bool Dns::configure(snort::SnortConfig*) +{ + if ( !pub_id ) + pub_id = DataBus::get_id(dns_pub_key); + + DataBus::subscribe(intrinsic_pub_key, IntrinsicEventIds::DNS_PAYLOAD, new DnsPayloadEventHandler(*this)); + return true; +} + +StreamSplitter* Dns::get_splitter(bool c2s) +{ + return new DnsSplitter(c2s); +} + //------------------------------------------------------------------------- // api stuff //------------------------------------------------------------------------- diff --git a/src/service_inspectors/dns/dns.h b/src/service_inspectors/dns/dns.h index 5a954607f..916a29c04 100644 --- a/src/service_inspectors/dns/dns.h +++ b/src/service_inspectors/dns/dns.h @@ -25,10 +25,11 @@ #include #include "flow/flow.h" - #include "protocols/packet.h" #include "pub_sub/dns_events.h" +#include "dns_module.h" + // Implementation header with definitions, datatypes and flowdata class for // DNS service inspector. @@ -183,9 +184,12 @@ struct DNSNameState #define DNS_RESP_STATE_AUTH_RR 0x50 #define DNS_RESP_STATE_ADD_RR 0x60 + class DnsConfig; struct DNSData; +DNSData* get_dns_session_data(snort::Packet* p, bool from_server, DNSData& udpSessionData, bool udp); + class DnsResponseFqdn { public: @@ -246,8 +250,6 @@ struct DNSData static const std::string& qtype_name(uint16_t query_type, bool* is_unknown = nullptr); }; -DNSData* get_dns_session_data(snort::Packet* p, bool from_server, DNSData& udpSessionData); - class DnsResponseIp { public: @@ -294,5 +296,31 @@ public: std::set trans_ids; }; +class Dns : public snort::Inspector +{ +public: + Dns(DnsModule*); + ~Dns() override; + + void eval(snort::Packet*) override; + snort::StreamSplitter* get_splitter(bool) override; + bool configure(snort::SnortConfig*) override; + void show(const snort::SnortConfig*) const override; + static unsigned get_pub_id() + { return pub_id; } + + bool supports_no_ips() const override + { return true; } + + void snort_dns(snort::Packet* p, bool udp); + + const DnsConfig* get_config() const + { return config; } + +private: + const DnsConfig* config = nullptr; + static unsigned pub_id; +}; + #endif diff --git a/src/service_inspectors/dns/dns_payload_event_handler.cc b/src/service_inspectors/dns/dns_payload_event_handler.cc new file mode 100644 index 000000000..65e2c5871 --- /dev/null +++ b/src/service_inspectors/dns/dns_payload_event_handler.cc @@ -0,0 +1,66 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2025-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. +//-------------------------------------------------------------------------- + +// dns_payload_event_handler.cc author Shibin K V + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "dns_payload_event_handler.h" + +#include "detection/detection_engine.h" +#include "pub_sub/dns_payload_event.h" + +#include "dns.h" + +using namespace snort; + +void DnsPayloadEventHandler::handle(DataEvent& event, Flow* flow) +{ + Packet *p = DetectionEngine::get_current_wire_packet(); + if (!flow or !p ) + return; + DnsPayloadEvent* dns_payload_event = (DnsPayloadEvent*)&event; + int32_t payload_length = 0; + const uint8_t* dns_payload = dns_payload_event->get_payload(payload_length); + bool is_udp = dns_payload_event->is_dns_udp(); + + if (!dns_payload or payload_length <= 0) + return; + + const uint8_t* old_data = p->data; + const uint32_t old_dsize = p->dsize; + SnortProtocolId old_protocol_id = p->flow->ssn_state.snort_protocol_id; + + { + p->data = dns_payload; + p->dsize = payload_length; + p->flow->ssn_state.snort_protocol_id = inspector.get_service(); + p->packet_flags |= PKT_ALLOW_MULTIPLE_DETECT; + DetectionEngine::detect(p); + } + + if (is_udp) + static_cast(inspector).snort_dns(p, true); + + p->data = old_data; + p->dsize = old_dsize; + p->flow->ssn_state.snort_protocol_id = old_protocol_id; + +} diff --git a/src/service_inspectors/dns/dns_payload_event_handler.h b/src/service_inspectors/dns/dns_payload_event_handler.h new file mode 100644 index 000000000..e064deeb4 --- /dev/null +++ b/src/service_inspectors/dns/dns_payload_event_handler.h @@ -0,0 +1,42 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2025-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. +//-------------------------------------------------------------------------- + +// dns_payload_event_handler.h author Shibin K V + +#ifndef DNS_PAYLOAD_EVENT_HANDLER_H +#define DNS_PAYLOAD_EVENT_HANDLER_H + +#include "framework/data_bus.h" + +#include "dns.h" +#include "dns_module.h" + +class DnsPayloadEventHandler : public snort::DataHandler +{ +public: + DnsPayloadEventHandler(snort::Inspector& inspector_) : + snort::DataHandler(DNS_NAME), inspector(inspector_) + { + order = 200; + } + void handle(snort::DataEvent& event, snort::Flow* flow) override; +private: + snort::Inspector& inspector; +}; + +#endif // DNS_PAYLOAD_EVENT_HANDLER_H diff --git a/src/service_inspectors/dns/dns_splitter.cc b/src/service_inspectors/dns/dns_splitter.cc index 97133d804..78678b27c 100644 --- a/src/service_inspectors/dns/dns_splitter.cc +++ b/src/service_inspectors/dns/dns_splitter.cc @@ -39,11 +39,9 @@ StreamSplitter::Status DnsSplitter::scan( { assert(len > 0); - DNSData udp_session_data; - bool from_server = p->is_from_server(); - DNSData* dnsSessionData = get_dns_session_data(p, from_server, udp_session_data); - - if ( dnsSessionData and ( dnsSessionData->flags & DNS_FLAG_NOT_DNS ) ) + DnsFlowData* fd; + fd = (DnsFlowData*)((p->flow)->get_flow_data(DnsFlowData::inspector_id)); + if (fd && (fd->session.flags & DNS_FLAG_NOT_DNS)) { dnsstats.aborted_sessions++; return ABORT; diff --git a/src/service_inspectors/http_inspect/http_enum.h b/src/service_inspectors/http_inspect/http_enum.h index cae6c4643..edbeb4db8 100755 --- a/src/service_inspectors/http_inspect/http_enum.h +++ b/src/service_inspectors/http_inspect/http_enum.h @@ -29,7 +29,7 @@ namespace HttpEnums static const int MAX_OCTETS = 63780; static const int GZIP_BLOCK_SIZE = 2048; static const int MAX_SECTION_STRETCH = 1460; -static const int REQUEST_PUBLISH_DEPTH = 2000; +static const int BODY_PUBLISH_DEPTH = 2000; static const uint32_t HTTP_GID = 119; static const int GZIP_WINDOW_BITS = 31; @@ -128,7 +128,7 @@ enum Contentcoding { CONTENTCODE__OTHER=1, CONTENTCODE_GZIP, CONTENTCODE_DEFLATE CONTENTCODE_XPRESS, CONTENTCODE_XZ }; // Content media-types (MIME types) -enum ContentType { CT__OTHER=1, CT_APPLICATION_PDF, CT_APPLICATION_OCTET_STREAM, +enum ContentType { CT__OTHER=1, CT_APPLICATION_DNS, CT_APPLICATION_PDF, CT_APPLICATION_OCTET_STREAM, CT_APPLICATION_JAVASCRIPT, CT_APPLICATION_ECMASCRIPT, CT_APPLICATION_X_JAVASCRIPT, CT_APPLICATION_X_ECMASCRIPT, CT_APPLICATION_XHTML_XML, CT_TEXT_JAVASCRIPT, CT_TEXT_JAVASCRIPT_1_0, CT_TEXT_JAVASCRIPT_1_1, CT_TEXT_JAVASCRIPT_1_2, CT_TEXT_JAVASCRIPT_1_3, diff --git a/src/service_inspectors/http_inspect/http_msg_body.cc b/src/service_inspectors/http_inspect/http_msg_body.cc index 79c976ef8..8948001e2 100644 --- a/src/service_inspectors/http_inspect/http_msg_body.cc +++ b/src/service_inspectors/http_inspect/http_msg_body.cc @@ -30,8 +30,10 @@ #include "helpers/buffer_data.h" #include "http_module.h" #include "js_norm/js_enum.h" +#include "pub_sub/dns_payload_event.h" #include "pub_sub/http_request_body_event.h" #include "pub_sub/http_body_event.h" +#include "pub_sub/intrinsic_event_ids.h" #include "http_api.h" #include "http_common.h" @@ -112,20 +114,20 @@ void HttpMsgBody::publish(unsigned pub_id) } // Publish request body limited statically - if (params->publish_request_body and is_request and (publish_octets < REQUEST_PUBLISH_DEPTH)) + if (params->publish_request_body and is_request and (publish_octets < BODY_PUBLISH_DEPTH)) { // Exclude already published octets (publish_octets): - const auto request_publish_depth_remaining = REQUEST_PUBLISH_DEPTH - publish_octets; + const auto request_publish_depth_remaining = BODY_PUBLISH_DEPTH - publish_octets; // We should not publish more than the remaining publish depth: auto request_publish_length = (publish_length > request_publish_depth_remaining) ? request_publish_depth_remaining : publish_length; - // If it is not the last piece of the request, it should be marked as such because of REQUEST_PUBLISH_DEPTH limit: + // If it is not the last piece of the request, it should be marked as such because of BODY_PUBLISH_DEPTH limit: // if sum of already published octets (publish_octets) and current publishing length (request_publish_length) // is greater than the request publish depth. auto request_last_piece = last_piece ? - true : (publish_octets + request_publish_length >= REQUEST_PUBLISH_DEPTH); + true : (publish_octets + request_publish_length >= BODY_PUBLISH_DEPTH); HttpRequestBodyEvent http_request_body_event(this, request_publish_length, publish_octets, request_last_piece, session_data); DataBus::publish(pub_id, HttpEventIds::REQUEST_BODY, http_request_body_event, flow); @@ -140,6 +142,30 @@ void HttpMsgBody::publish(unsigned pub_id) #endif } + // publish DOH body if applicable + if (get_header(source_id) and (get_header(source_id)->get_content_type() == CT_APPLICATION_DNS) + and (publish_octets < BODY_PUBLISH_DEPTH)) + { + const auto doh_publish_depth_remaining = BODY_PUBLISH_DEPTH - publish_octets; + auto doh_publish_length = (publish_length > doh_publish_depth_remaining) ? + doh_publish_depth_remaining : publish_length; + auto doh_last_piece = last_piece ? true : (publish_octets + doh_publish_length >= BODY_PUBLISH_DEPTH); + DnsPayloadEvent dns_payload_event(msg_text_new.start(), publish_length, is_request, + true, doh_last_piece); + DnsDataStash* stash = new DnsDataStash(msg_text_new.start(), publish_length); + flow->set_attr(STASH_DNS_DATA, stash); + DataBus::publish(intrinsic_pub_id, IntrinsicEventIds::DNS_PAYLOAD, dns_payload_event, flow); + #ifdef REG_TEST + if (HttpTestManager::use_test_output(HttpTestManager::IN_HTTP)) + { + fprintf(HttpTestManager::get_output_file(), + "Published %" PRId32 " bytes of DOH body. last: %s\n", doh_publish_length, + (doh_last_piece ? "true" : "false")); + fflush(HttpTestManager::get_output_file()); + } + #endif + } + publish_octets += publish_length; } diff --git a/src/service_inspectors/http_inspect/http_msg_header.cc b/src/service_inspectors/http_inspect/http_msg_header.cc index 393636713..008843364 100755 --- a/src/service_inspectors/http_inspect/http_msg_header.cc +++ b/src/service_inspectors/http_inspect/http_msg_header.cc @@ -46,6 +46,11 @@ #include "http_msg_body.h" #include "http_normalizers.h" +#include "mime/decode_b64.h" +#include "pub_sub/dns_payload_event.h" +#include "pub_sub/intrinsic_event_ids.h" + + using namespace snort; using namespace HttpCommon; using namespace HttpEnums; @@ -72,6 +77,74 @@ void HttpMsgHeader::publish(unsigned pub_id) HttpEventIds::REQUEST_HEADER : HttpEventIds::RESPONSE_HEADER; DataBus::publish(pub_id, evid, http_header_event, flow); + + // Look for DNS over HTTPS (DOH) and if found publish a DNS payload event + int32_t uri_length = 0; + const uint8_t* uri = http_header_event.get_uri_query(uri_length); + if (uri_length == 0) + return; + constexpr char doh_pattern[] = "dns="; + constexpr int8_t doh_pattern_len = sizeof(doh_pattern) - 1; + // For DOH, get request will look like GET /dns-query?dns=BASE64_ENCODED_DNS_QUERY + if (uri_length >= doh_pattern_len && memcmp(uri, doh_pattern, doh_pattern_len) == 0 && get_method_id() == METH_GET) + { + size_t b64_start = doh_pattern_len; + if (b64_start < (size_t)uri_length) + { + const uint8_t* b64_data_start = uri + b64_start; + const uint8_t* b64_data_end = uri + uri_length; + size_t b64_len = b64_data_end - b64_data_start; + + // Copy base64 data to a vector for padding and conversion from base64url to base64 + std::vector b64_data(b64_data_start, b64_data_end); + + for (auto& c : b64_data) + { + if (c == '-') + c = '+'; + else if (c == '_') + c = '/'; + } + // DOH RFC requires base64url encoding which omits padding(rfc8484:4.1.1). Add it back if needed. + // Base64 length must be a multiple of 4 + size_t remainder = b64_len % 4; + if (remainder == 1) + { + // Invalid base64 length, do not decode + return; + } + else if (remainder == 2) + { + b64_data.push_back('='); + b64_data.push_back('='); + } + else if (remainder == 3) + b64_data.push_back('='); + + std::vector decode_buf(1024); + B64Decode decoder = B64Decode(1024, 1024); + DecodeResult decode_result = decoder.decode_data(b64_data.data(), b64_data.data() + b64_data.size(), + decode_buf.data()); + if (decode_result == DECODE_SUCCESS) + { + const uint8_t* decoded_data = nullptr; + uint32_t decode_buf_size = 0; + decoder.get_decoded_data(&decoded_data, &decode_buf_size); + DnsPayloadEvent dns_payload_event(decoded_data, decode_buf_size, (source_id == SRC_CLIENT), true, true); + DnsDataStash* stash = new DnsDataStash(decoded_data, decode_buf_size); + flow->set_attr(STASH_DNS_DATA, stash); + DataBus::publish(intrinsic_pub_id, IntrinsicEventIds::DNS_PAYLOAD, dns_payload_event, flow); + #ifdef REG_TEST + if (HttpTestManager::use_test_output(HttpTestManager::IN_HTTP)) + { + fprintf(HttpTestManager::get_output_file(), + "Published %" PRId32 " bytes of DOH payload from base64 decoded query.\n", decode_buf_size); + fflush(HttpTestManager::get_output_file()); + } + #endif + } + } + } } bool HttpMsgHeader::has_supported_encoding() const @@ -515,11 +588,11 @@ void HttpMsgHeader::prepare_body() session_data->detect_depth_remaining[source_id] = INT64_MAX; } - // Set the default depth if we're publishing the request body. - if (params->publish_request_body and is_request) + // Set the default depth if we're publishing the request body or DOH body. + if ( (params->publish_request_body and is_request) || (get_content_type() == CT_APPLICATION_DNS) ) { session_data->publish_octets[source_id] = 0; - session_data->publish_depth_remaining[source_id] = REQUEST_PUBLISH_DEPTH; + session_data->publish_depth_remaining[source_id] = BODY_PUBLISH_DEPTH; } // Dynamically update the publish depth. diff --git a/src/service_inspectors/http_inspect/http_stream_splitter_finish.cc b/src/service_inspectors/http_inspect/http_stream_splitter_finish.cc index 5b0dd4b2f..1413c3789 100644 --- a/src/service_inspectors/http_inspect/http_stream_splitter_finish.cc +++ b/src/service_inspectors/http_inspect/http_stream_splitter_finish.cc @@ -220,7 +220,7 @@ bool HttpStreamSplitter::finish(Flow* flow) #endif } - if ((source_id == SRC_CLIENT) and (session_data->publish_octets[source_id] < REQUEST_PUBLISH_DEPTH)) + if ((source_id == SRC_CLIENT) and (session_data->publish_octets[source_id] < BODY_PUBLISH_DEPTH)) { HttpRequestBodyEvent http_request_body_event(nullptr, 0, session_data->publish_octets[source_id], true, session_data); diff --git a/src/service_inspectors/http_inspect/http_tables.cc b/src/service_inspectors/http_inspect/http_tables.cc index f5cfdf79e..3c2ed9ec6 100755 --- a/src/service_inspectors/http_inspect/http_tables.cc +++ b/src/service_inspectors/http_inspect/http_tables.cc @@ -174,6 +174,7 @@ const StrCode HttpMsgHeadShared::content_code_list[] = const StrCode HttpMsgHeadShared::content_type_list[] = { + { CT_APPLICATION_DNS, "application/dns-message" }, { CT_APPLICATION_PDF, "application/pdf" }, { CT_APPLICATION_OCTET_STREAM, "application/octet-stream" }, { CT_APPLICATION_JAVASCRIPT, "application/javascript" }, diff --git a/src/stream/tcp/tcp_reassembler.cc b/src/stream/tcp/tcp_reassembler.cc index 51ab31c8e..66140d871 100644 --- a/src/stream/tcp/tcp_reassembler.cc +++ b/src/stream/tcp/tcp_reassembler.cc @@ -254,12 +254,11 @@ Packet* TcpReassemblerBase::initialize_pdu(Packet* p, uint32_t pkt_flags, struct pdu->dsize = 0; pdu->data = nullptr; pdu->ip_proto_next = (IpProtocol)p->flow->ip_proto; - + memcpy( pdu->layers, p->layers, p->num_layers * sizeof(Layer)); + pdu->num_layers = p->num_layers; if ( p->proto_bits & PROTO_BIT__VLAN ) { - memcpy( pdu->layers, p->layers, p->num_layers * sizeof(Layer)); - pdu->num_layers = p->num_layers; pdu->proto_bits |= PROTO_BIT__VLAN; pdu->vlan_idx = p->vlan_idx; }