From e023f0da2f9d4f37137b64e53aa6a8a2f7945769 Mon Sep 17 00:00:00 2001 From: "Adrian Mamolea (admamole)" Date: Fri, 4 Apr 2025 14:06:39 +0000 Subject: [PATCH] Pull request #4675: extractor: extend dns logging Merge in SNORT/snort3 from ~ADMAMOLE/snort3:extractor_dns2 to master Squashed commit of the following: commit 92b7e2c0ab8f1b0fba620f80a2882dea301cbc8c Author: Adrian Mamolea Date: Mon Mar 24 17:03:25 2025 -0400 extractor: extend dns logging --- doc/user/extractor.txt | 8 +- .../extractor/extractor_dns.cc | 20 +- .../extractor/extractor_service.cc | 5 +- src/pub_sub/dns_events.cc | 37 ++- src/pub_sub/dns_events.h | 13 +- src/service_inspectors/dns/CMakeLists.txt | 1 + src/service_inspectors/dns/dns.cc | 46 ++-- src/service_inspectors/dns/dns.h | 19 +- src/service_inspectors/dns/dns_rr_decoder.cc | 258 ++++++++++++++++++ 9 files changed, 370 insertions(+), 37 deletions(-) create mode 100644 src/service_inspectors/dns/dns_rr_decoder.cc diff --git a/doc/user/extractor.txt b/doc/user/extractor.txt index 1dd5566b2..bb866c7e1 100644 --- a/doc/user/extractor.txt +++ b/doc/user/extractor.txt @@ -124,8 +124,14 @@ Fields supported for DNS: * `RD` - A boolean, true when the client asks the server to pursue the query recursively * `RA` - A boolean, denotes the availability of recursive query support at the server * `Z` - A 3 bit integer set to 0 unless DNSSEC is used (see RFC 2535) -* `answers` - The list of answers to the query, only A and AAAA types are currently supported +* `answers` - The list of answers to the query +* `TTLs` - The list of caching intervals for the corresponding answers * `rejected` - A boolean, true when the server responds with an error code and no query +* `auth` - The list of authoritative responses +* `addl` - The list of additional responses + +In the answers, auth, and addl lists the decoding of the following RR types is supported: +A, AAAA, CNAME, DS, MX, NS, NSEC, PTR, RRSIG, SOA, TXT Fields supported for connection: diff --git a/src/network_inspectors/extractor/extractor_dns.cc b/src/network_inspectors/extractor/extractor_dns.cc index 7176e566f..49bad0625 100644 --- a/src/network_inspectors/extractor/extractor_dns.cc +++ b/src/network_inspectors/extractor/extractor_dns.cc @@ -124,11 +124,26 @@ static const char* get_answers(const DataEvent* event, const Flow*) return ((const DnsResponseEvent*)event)->get_answers().c_str(); } +static const char* get_TTLs(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_TTLs().c_str(); +} + static const char* get_rejected(const DataEvent* event, const Flow*) { return ((const DnsResponseEvent*)event)->get_rejected() ? "T" : "F"; } +static const char* get_auth(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_auth().c_str(); +} + +static const char* get_addl(const DataEvent* event, const Flow*) +{ + return ((const DnsResponseEvent*)event)->get_addl().c_str(); +} + static const map sub_buf_getters = { {"proto", get_proto}, @@ -141,7 +156,10 @@ static const map sub_buf_getters = {"RD", get_RD}, {"RA", get_RA}, {"answers", get_answers}, - {"rejected", get_rejected} + {"TTLs", get_TTLs}, + {"rejected", get_rejected}, + {"auth", get_auth}, + {"addl", get_addl} }; THREAD_LOCAL const snort::Connector::ID* DnsResponseExtractor::log_id = nullptr; diff --git a/src/network_inspectors/extractor/extractor_service.cc b/src/network_inspectors/extractor/extractor_service.cc index 29e388986..5401977c6 100644 --- a/src/network_inspectors/extractor/extractor_service.cc +++ b/src/network_inspectors/extractor/extractor_service.cc @@ -401,7 +401,10 @@ const ServiceBlueprint DnsExtractorService::blueprint = "RA", "Z", "answers", - "rejected" + "TTLs", + "rejected", + "auth", + "addl" }, }; diff --git a/src/pub_sub/dns_events.cc b/src/pub_sub/dns_events.cc index c6c523399..85990b59c 100644 --- a/src/pub_sub/dns_events.cc +++ b/src/pub_sub/dns_events.cc @@ -312,7 +312,22 @@ uint8_t DnsResponseEvent::get_Z() const const std::string& DnsResponseEvent::get_answers() const { - return session.answers; + if (!answers_set) + { + session.get_answers(packet, answers, ttls); + answers_set = true; + } + return answers; +} + +const std::string& DnsResponseEvent::get_TTLs() const +{ + if (!answers_set) + { + session.get_answers(packet, answers, ttls); + answers_set = true; + } + return ttls; } bool DnsResponseEvent::get_rejected() const @@ -321,3 +336,23 @@ bool DnsResponseEvent::get_rejected() const session.hdr.flags & DNS_HDR_FLAG_REPLY_CODE_MASK && !session.hdr.questions; } + +const std::string& DnsResponseEvent::get_auth() const +{ + if (!auth_set) + { + session.get_auth(packet, auth); + auth_set = true; + } + return auth; +} + +const std::string& DnsResponseEvent::get_addl() const +{ + if (!addl_set) + { + session.get_addl(packet, addl); + addl_set = true; + } + return addl; +} diff --git a/src/pub_sub/dns_events.h b/src/pub_sub/dns_events.h index dced26d2a..4c88e2aa8 100644 --- a/src/pub_sub/dns_events.h +++ b/src/pub_sub/dns_events.h @@ -86,7 +86,7 @@ private: class SO_PUBLIC DnsResponseEvent : public snort::DataEvent { public: - DnsResponseEvent(const DNSData& ssn) : session(ssn) { } + DnsResponseEvent(const DNSData& ssn, Packet* p) : session(ssn), packet(p) { } uint16_t get_trans_id() const; const std::string& get_query() const; @@ -102,10 +102,21 @@ public: bool get_RA() const; uint8_t get_Z() const; const std::string& get_answers() const; + const std::string& get_TTLs() const; bool get_rejected() const; + const std::string& get_auth() const; + const std::string& get_addl() const; private: const DNSData& session; + const Packet* packet = nullptr; + mutable std::string answers; + mutable std::string ttls; + mutable std::string auth; + mutable std::string addl; + mutable bool answers_set = false; + mutable bool auth_set = false; + mutable bool addl_set = false; }; } diff --git a/src/service_inspectors/dns/CMakeLists.txt b/src/service_inspectors/dns/CMakeLists.txt index 9a5ca65ed..cc32d6af8 100644 --- a/src/service_inspectors/dns/CMakeLists.txt +++ b/src/service_inspectors/dns/CMakeLists.txt @@ -6,6 +6,7 @@ set( FILE_LIST dns_config.h dns_module.cc dns_module.h + dns_rr_decoder.cc dns_splitter.cc dns_splitter.h ) diff --git a/src/service_inspectors/dns/dns.cc b/src/service_inspectors/dns/dns.cc index 3bc1df319..a4fa54ce6 100644 --- a/src/service_inspectors/dns/dns.cc +++ b/src/service_inspectors/dns/dns.cc @@ -40,7 +40,6 @@ using namespace snort; #define MAX_UDP_PAYLOAD 0x1FFF -#define DNS_RR_PTR 0xC0 THREAD_LOCAL ProfileStats dnsPerfStats; THREAD_LOCAL DnsStats dnsstats; @@ -127,13 +126,6 @@ bool DNSData::valid_dns(const DNSHdr& dns_header) const return true; } -void DNSData::add_answer(const char* answer) -{ - if (!answers.empty()) - answers.append(" "); - answers.append(answer); -} - DNSData* get_dns_session_data(Packet* p, bool from_server, DNSData& udpSessionData) { DnsFlowData* fd; @@ -512,7 +504,8 @@ static uint16_t ParseDNSQuestion( } static uint16_t ParseDNSAnswer( - const unsigned char* data, uint16_t bytes_unused, DNSData* dnsSessionData) + const unsigned char* data, uint16_t bytes_unused, DNSData* dnsSessionData, + const Packet* p, std::vector& tabs) { if ( !bytes_unused ) return 0; @@ -540,6 +533,7 @@ static uint16_t ParseDNSAnswer( switch (dnsSessionData->curr_rec_state) { case DNS_RESP_STATE_RR_TYPE: + tabs.emplace_back(data - p->data); dnsSessionData->curr_rr.type = (uint8_t)*data << 8; data++; @@ -756,8 +750,7 @@ static uint16_t SkipDNSRData( static uint16_t ParseDNSRData( const unsigned char* data, uint16_t bytes_unused, - DNSData* dnsSessionData, - void (DNSData::*save_rdata)(const char*) = nullptr) + DNSData* dnsSessionData) { if (bytes_unused == 0) { @@ -789,23 +782,13 @@ static uint16_t ParseDNSRData( break; case DNS_RR_TYPE_A: case DNS_RR_TYPE_AAAA: + if (dnsSessionData->publish_response()) { - auto resp_ip = DnsResponseIp(data, dnsSessionData->curr_rr.type); - if (dnsSessionData->publish_response()) - { - dnsSessionData->dns_events.add_fqdn(dnsSessionData->cur_fqdn_event, dnsSessionData->curr_rr.ttl); - dnsSessionData->dns_events.add_ip(resp_ip); - } - - if (save_rdata) - { - SfIpString ipbuf; - resp_ip.get_ip().ntop(ipbuf); - (dnsSessionData->*save_rdata)(ipbuf); - } + dnsSessionData->dns_events.add_fqdn(dnsSessionData->cur_fqdn_event, dnsSessionData->curr_rr.ttl); + dnsSessionData->dns_events.add_ip(DnsResponseIp(data, dnsSessionData->curr_rr.type)); } - bytes_unused = SkipDNSRData(data, bytes_unused, dnsSessionData); + bytes_unused = SkipDNSRData(data, bytes_unused, dnsSessionData); break; case DNS_RR_TYPE_CNAME: if (dnsSessionData->publish_response()) @@ -819,6 +802,9 @@ static uint16_t ParseDNSRData( case DNS_RR_TYPE_PTR: case DNS_RR_TYPE_HINFO: case DNS_RR_TYPE_MX: + case DNS_RR_TYPE_RRSIG: + case DNS_RR_TYPE_NSEC: + case DNS_RR_TYPE_DS: bytes_unused = SkipDNSRData(data, bytes_unused, dnsSessionData); break; default: @@ -916,7 +902,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne case DNS_RESP_STATE_ANS_RR: /* ANSWERS section */ for (i=dnsSessionData->curr_rec; ihdr.answers; i++) { - bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData); + bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData, p, dnsSessionData->answer_tabs); if (bytes_unused == 0) { @@ -933,7 +919,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne case DNS_RESP_STATE_RR_RDATA_MID: /* Data now points to the beginning of the RDATA */ data = p->data + (p->dsize - bytes_unused); - bytes_unused = ParseDNSRData(data, bytes_unused, dnsSessionData, &DNSData::add_answer); + bytes_unused = ParseDNSRData(data, bytes_unused, dnsSessionData); if (dnsSessionData->curr_rec_state != DNS_RESP_STATE_RR_COMPLETE) { needNextPacket = true; @@ -961,7 +947,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne case DNS_RESP_STATE_AUTH_RR: /* AUTHORITIES section */ for (i=dnsSessionData->curr_rec; ihdr.authorities; i++) { - bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData); + bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData, p, dnsSessionData->auth_tabs); if (bytes_unused == 0) { @@ -1006,7 +992,7 @@ static void ParseDNSResponseMessage(Packet* p, DNSData* dnsSessionData, bool& ne case DNS_RESP_STATE_ADD_RR: /* ADDITIONALS section */ for (i=dnsSessionData->curr_rec; ihdr.additionals; i++) { - bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData); + bytes_unused = ParseDNSAnswer(data, bytes_unused, dnsSessionData, p, dnsSessionData->addl_tabs); if (bytes_unused == 0) { @@ -1268,7 +1254,7 @@ static void snort_dns(Packet* p, const DnsConfig* dns_config) if (!needNextPacket and dnsSessionData->has_events()) DataBus::publish(Dns::get_pub_id(), DnsEventIds::DNS_RESPONSE_DATA, dnsSessionData->dns_events); - DnsResponseEvent dns_response_event(*dnsSessionData); + DnsResponseEvent dns_response_event(*dnsSessionData, p); DataBus::publish(Dns::get_pub_id(), DnsEventIds::DNS_RESPONSE, dns_response_event, p->flow); } diff --git a/src/service_inspectors/dns/dns.h b/src/service_inspectors/dns/dns.h index 6bf2fba60..07de79843 100644 --- a/src/service_inspectors/dns/dns.h +++ b/src/service_inspectors/dns/dns.h @@ -112,6 +112,11 @@ struct DNSNameState #define DNS_RR_TYPE_MX 0x000f #define DNS_RR_TYPE_TXT 0x0010 #define DNS_RR_TYPE_AAAA 0x001c +#define DNS_RR_TYPE_RRSIG 0x002e +#define DNS_RR_TYPE_NSEC 0x002f +#define DNS_RR_TYPE_DS 0x002b + +#define DNS_RR_PTR 0xC0 #define DNS_FLAG_NOT_DNS 0x01 @@ -208,12 +213,22 @@ struct DNSData snort::DnsResponseDataEvents dns_events; DnsResponseFqdn cur_fqdn_event; std::string resp_query; - std::string answers; + std::vector answer_tabs; + std::vector auth_tabs; + std::vector addl_tabs; bool publish_response() const; bool has_events() const; bool valid_dns(const DNSHdr&) const; - void add_answer(const char* answer); + + void decode_rdata(const snort::Packet* p, const uint8_t* rdata, uint16_t rdlength, + uint16_t type, std::string& rdata_str) const; + void get_rr_data(const snort::Packet *p, const std::vector& tabs, + std::string& rrs, std::string* ttls = nullptr) const; + void get_answers(const snort::Packet *p, std::string& answers, std::string& ttls) const + { get_rr_data(p, answer_tabs, answers, &ttls); } + void get_auth(const snort::Packet *p, std::string& auth) const { get_rr_data(p, auth_tabs, auth); } + void get_addl(const snort::Packet *p, std::string& addl) const { get_rr_data(p, addl_tabs, addl); } }; DNSData* get_dns_session_data(snort::Packet* p, bool from_server, DNSData& udpSessionData); diff --git a/src/service_inspectors/dns/dns_rr_decoder.cc b/src/service_inspectors/dns/dns_rr_decoder.cc new file mode 100644 index 000000000..daaa2bc04 --- /dev/null +++ b/src/service_inspectors/dns/dns_rr_decoder.cc @@ -0,0 +1,258 @@ +//-------------------------------------------------------------------------- +// 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.cc author Cisco + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "dns.h" + +#include +#include "sfip/sf_ip.h" + +using namespace snort; + +static void decode_ip(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str, uint16_t type) +{ + int family = (type == DNS_RR_TYPE_A) ? AF_INET : AF_INET6; + if (family == AF_INET && rdlength != 4) + return; + if (family == AF_INET6 && rdlength != 16) + return; + + SfIpString ipbuf; + SfIp rr_ip(rdata, family); + rr_ip.ntop(ipbuf); + + rdata_str = ipbuf; +} + +static const std::string part_sep = " "; // separates between parts of an item in a list + +static void decode_txt(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str) +{ + static const std::string txt_prefix = "TXT" + part_sep; + + while (rdlength) + { + if (!rdata_str.empty()) + rdata_str.append(part_sep); + rdata_str.append(txt_prefix); + + uint8_t txt_len = *rdata; + rdata_str.append(std::to_string(txt_len)); + rdata_str.append(part_sep); + rdata++; + rdlength--; + + uint8_t actual_len = rdlength > txt_len ? txt_len : rdlength; + rdata_str.append((const char*)rdata, actual_len); + rdata += actual_len; + rdlength -= actual_len; + } +} + +static void decode_domain_name(const uint8_t* rdata, uint16_t rdlength, + std::string& rdata_str, const Packet* p = nullptr) +{ + static const int MAX_LINKS = 10; + int link_count = 0; + + bool first_label = true; + while (rdlength) + { + uint8_t label_len = *rdata; + rdata++; + rdlength--; + + if (label_len == 0) + break; + + if ((label_len & DNS_RR_PTR) == DNS_RR_PTR) + { + if (p == nullptr) + break; // compression not supported + + if (rdlength < 1) + break; // incomplete offset + + uint16_t offset = ((label_len & ~DNS_RR_PTR) << 8) | *rdata; + if (offset >= p->dsize) + break; // invalid offset + if (link_count++ > MAX_LINKS) + break; // too many links + + rdata = p->data + offset; + rdlength = p->dsize - offset; + continue; + } + + if (label_len & DNS_RR_PTR) + break; // invalid label length + + uint8_t actual_len = rdlength > label_len ? label_len : rdlength; + if (!first_label) + rdata_str.append("."); + first_label = false; + rdata_str.append((const char*)rdata, actual_len); + rdata += actual_len; + rdlength -= actual_len; + } +} + +static void decode_mx(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str, const Packet* p) +{ + static const unsigned EXCHANGE_OFFSET = 2; + if (rdlength <= EXCHANGE_OFFSET) + return; // incomplete MX record + + rdata += EXCHANGE_OFFSET; + rdlength -= EXCHANGE_OFFSET; + decode_domain_name(rdata, rdlength, rdata_str, p); +} + +static void decode_rrsig(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str) +{ + static const std::string rrsig_prefix = "RRSIG" + part_sep; + static const unsigned SIGNER_NAME_OFFSET = 18; + + if (rdlength <= SIGNER_NAME_OFFSET) + return; // incomplete RRSIG record + + auto type_covered = (uint16_t)rdata[0] << 8 | rdata[1]; + rdata_str.append(rrsig_prefix); + rdata_str.append(std::to_string(type_covered)); + rdata_str.append(part_sep); + + rdata += SIGNER_NAME_OFFSET; + rdlength -= SIGNER_NAME_OFFSET; + decode_domain_name(rdata, rdlength, rdata_str); +} + +static void decode_nsec(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str, + const std::string& resp_query) +{ + static const std::string nsec_prefix = "NSEC" + part_sep; + + rdata_str.append(nsec_prefix); + rdata_str.append(resp_query); + rdata_str.append(part_sep); + decode_domain_name(rdata, rdlength, rdata_str); +} + +static void decode_ds(const uint8_t* rdata, uint16_t rdlength, std::string& rdata_str) +{ + static const std::string ds_prefix = "DS" + part_sep; + static const unsigned DIGEST_OFFSET = 4; + + if (rdlength <= DIGEST_OFFSET) + return; // incomplete DS record + + rdata_str.append(ds_prefix); + rdata_str.append(std::to_string(rdata[2])); // algorithm + rdata_str.append(part_sep); + rdata_str.append(std::to_string(rdata[3])); // digest type +} + +void DNSData::decode_rdata(const Packet* p, const uint8_t* rdata, uint16_t rdlength, uint16_t type, + std::string& rdata_str) const +{ + assert(rdata < p->data + p->dsize); + + if (rdata + rdlength > p->data + p->dsize) + rdlength = p->dsize - (rdata - p->data); + + switch (type) + { + case DNS_RR_TYPE_A: + case DNS_RR_TYPE_AAAA: + decode_ip(rdata, rdlength, rdata_str, type); + break; + + case DNS_RR_TYPE_TXT: + decode_txt(rdata, rdlength, rdata_str); + break; + + case DNS_RR_TYPE_CNAME: + case DNS_RR_TYPE_MD: + case DNS_RR_TYPE_MF: + case DNS_RR_TYPE_MB: + case DNS_RR_TYPE_MG: + case DNS_RR_TYPE_MR: + case DNS_RR_TYPE_NS: + case DNS_RR_TYPE_PTR: + case DNS_RR_TYPE_SOA: + decode_domain_name(rdata, rdlength, rdata_str, p); + break; + + case DNS_RR_TYPE_MX: + decode_mx(rdata, rdlength, rdata_str, p); + break; + + case DNS_RR_TYPE_RRSIG: + decode_rrsig(rdata, rdlength, rdata_str); + break; + + case DNS_RR_TYPE_NSEC: + decode_nsec(rdata, rdlength, rdata_str, resp_query); + break; + + case DNS_RR_TYPE_DS: + decode_ds(rdata, rdlength, rdata_str); + break; + + default: + break; + } +} + +void DNSData::get_rr_data(const Packet *p, const std::vector& tabs, + std::string& rrs, std::string* ttls) const +{ + static const std::string item_sep = " "; // list item separator + static const unsigned RDATA_OFFSET = 10; + for (auto tab : tabs) + { + if (tab + RDATA_OFFSET >= p->dsize) + continue; + + auto rdata = p->data + tab; + + uint16_t type = (rdata[0] << 8) | rdata[1]; + uint32_t ttl = (rdata[4] << 24) | (rdata[5] << 16) | (rdata[6] << 8) | rdata[7]; + + uint16_t rdlength = (rdata[8] << 8) | rdata[9]; + + std::string rdata_str; + decode_rdata(p, rdata + RDATA_OFFSET, rdlength, type, rdata_str); + + if (rdata_str.empty()) + continue; + + if (!rrs.empty()) + { + rrs.append(item_sep); + if (ttls) + ttls->append(item_sep); + } + rrs.append(rdata_str); + if (ttls) + ttls->append(std::to_string(ttl)); + } +} -- 2.47.2