diff --git a/CHANGES b/CHANGES index b82d1d67f2..fa05b6b7e8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,12 @@ +8.0.0-dev.506 | 2025-06-24 17:44:06 +0200 + + * DNS: Implement NAPTR RR support (Arne Welzel, Corelight) + + My phone is sending NAPTR queries and we reported an unknown RR type 35 + in weird.log for the response, so figured I'd just add it. + + * DNS: Move extract_char_string() helper around (Arne Welzel, Corelight) + 8.0.0-dev.503 | 2025-06-24 17:14:28 +0200 * cluster/zeromq: Short-circuit DoPublishLogWrite() when not initialized (Arne Welzel, Corelight) diff --git a/NEWS b/NEWS index 485bff0a78..79ed2e0b1a 100644 --- a/NEWS +++ b/NEWS @@ -114,6 +114,9 @@ New Functionality redef Intel::manage_seen_event_groups = F; +- The DNS analyzer was extended to support NAPTR RRs (RFC 2915, RFC 3403). + A corresponding ``dns_NAPTR_reply`` event was added. + Changed Functionality --------------------- diff --git a/VERSION b/VERSION index a5bd06a820..8e9f11daf3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.0-dev.503 +8.0.0-dev.506 diff --git a/scripts/base/init-bare.zeek b/scripts/base/init-bare.zeek index b47899d2f6..11185edeb7 100644 --- a/scripts/base/init-bare.zeek +++ b/scripts/base/init-bare.zeek @@ -3048,6 +3048,20 @@ type dns_svcb_rr: record { target_name: string; ##< Target name, the hostname of the service endpoint. }; +## A NAPTR record. +## +## See also RFC 2915 - The Naming Authority Pointer (NAPTR) DNS Resource Record. +## +## .. zeek:see:: dns_NAPTR_reply +type dns_naptr_rr: record { + order: count; ##< Order in which to process NAPTR records. + preference: count; ##< Preference specifying processing order for *equal* :zeek:field:`dns_naptr_rr$order` fields. + flags: string; ##< Flags to control rewriting. E.g. "u", "a", "s" or "p". + service: string; ##< The services available down this rewrite path. + regexp: string; ##< Substitution expression to be applied to the original query. + replacement: string; ##< The next name to query, where the type is depending on the :zeek:field:`dns_naptr_rr$flags` field. +}; + # DNS answer types. # # .. zeek:see:: dns_answer diff --git a/scripts/base/protocols/dns/main.zeek b/scripts/base/protocols/dns/main.zeek index 078d50ecae..ac7e59793f 100644 --- a/scripts/base/protocols/dns/main.zeek +++ b/scripts/base/protocols/dns/main.zeek @@ -537,6 +537,27 @@ event dns_SRV_reply(c: connection, msg: dns_msg, ans: dns_answer, target: string hook DNS::do_reply(c, msg, ans, target); } +event dns_NAPTR_reply(c: connection, msg: dns_msg, ans: dns_answer, naptr: dns_naptr_rr) &priority=5 + { + # Just encode all the fields for NAPTR RR in the reply string. + local tmp = ""; + + if ( |naptr$regexp| > 0 ) + tmp += naptr$regexp; + + if ( |naptr$replacement| > 0 ) + { + if ( |tmp| > 0 ) + tmp += " "; + + tmp += naptr$replacement; + } + + local r = fmt("NAPTR %s %s %s %s %s", naptr$order, naptr$preference, naptr$flags, naptr$service, tmp); + + hook DNS::do_reply(c, msg, ans, r); + } + # TODO: figure out how to handle these #event dns_EDNS(c: connection, msg: dns_msg, ans: dns_answer) # { diff --git a/src/analyzer/protocol/dns/DNS.cc b/src/analyzer/protocol/dns/DNS.cc index 98204320f8..9ef6a1fc79 100644 --- a/src/analyzer/protocol/dns/DNS.cc +++ b/src/analyzer/protocol/dns/DNS.cc @@ -11,10 +11,37 @@ #include "zeek/Event.h" #include "zeek/NetVar.h" #include "zeek/RunState.h" +#include "zeek/Val.h" #include "zeek/ZeekString.h" #include "zeek/analyzer/protocol/dns/events.bif.h" #include "zeek/session/Manager.h" +namespace { +zeek::StringValPtr extract_char_string(zeek::analyzer::Analyzer* analyzer, const u_char*& data, int& len, int& rdlen) { + if ( rdlen <= 0 ) + return nullptr; + + uint8_t str_size = data[0]; + + --rdlen; + --len; + ++data; + + if ( str_size > rdlen ) { + analyzer->Weird("DNS_TXT_char_str_past_rdlen"); + return nullptr; + } + + auto rval = zeek::make_intrusive(str_size, reinterpret_cast(data)); + + rdlen -= str_size; + len -= str_size; + data += str_size; + + return rval; +} +} // namespace + namespace zeek::analyzer::dns { namespace detail { @@ -273,6 +300,8 @@ bool DNS_Interpreter::ParseAnswer(detail::DNS_MsgInfo* msg, const u_char*& data, break; + case detail::TYPE_NAPTR: status = ParseRR_NAPTR(msg, data, len, rdlength, msg_start); break; + case detail::TYPE_EDNS: status = ParseRR_EDNS(msg, data, len, rdlength, msg_start); break; case detail::TYPE_TKEY: status = ParseRR_TKEY(msg, data, len, rdlength, msg_start); break; @@ -602,6 +631,50 @@ bool DNS_Interpreter::ParseRR_SRV(detail::DNS_MsgInfo* msg, const u_char*& data, return true; } +bool DNS_Interpreter::ParseRR_NAPTR(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, + const u_char* msg_start) { + zeek_uint_t order = ExtractShort(data, len); + zeek_uint_t preference = ExtractShort(data, len); + rdlength -= 4; + + if ( len <= 0 || rdlength <= 0 ) { + analyzer->AnalyzerViolation("DNS_NAPTR_too_short"); + return false; + } + + // These all check rdlength and return nullptr if there's not enough data available. + auto flags = extract_char_string(analyzer, data, len, rdlength); + auto service = extract_char_string(analyzer, data, len, rdlength); + auto regexp = extract_char_string(analyzer, data, len, rdlength); + + // The replacement string is a name. Compression shouldn't be used, but doesn't seem + // we have a helper that would allow to control this. + u_char replacement[513]; + int replacement_len = sizeof(replacement) - 1; + u_char* replacement_end = ExtractName(data, len, replacement, replacement_len, msg_start, false); + + if ( ! flags || ! service || ! regexp || ! replacement_end ) { + analyzer->AnalyzerViolation("DNS_NAPTR_RR_too_short"); + return false; + } + + if ( dns_NAPTR_reply && ! msg->skip_event ) { + static auto dns_naptr_rr = id::find_type("dns_naptr_rr"); + auto r = make_intrusive(dns_naptr_rr); + + r->Assign(0, order); + r->Assign(1, preference); + r->Assign(2, std::move(flags)); + r->Assign(3, std::move(service)); + r->Assign(4, std::move(regexp)); + r->Assign(5, zeek::make_intrusive(new String(replacement, replacement_end - replacement, true))); + + analyzer->EnqueueConnEvent(dns_NAPTR_reply, analyzer->ConnVal(), msg->BuildHdrVal(), msg->BuildAnswerVal(), r); + } + + return true; +} + bool DNS_Interpreter::ParseRR_EDNS(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, const u_char* msg_start) { if ( dns_EDNS_addl && ! msg->skip_event ) @@ -1413,30 +1486,6 @@ bool DNS_Interpreter::ParseRR_WKS(detail::DNS_MsgInfo* msg, const u_char*& data, return true; } -static StringValPtr extract_char_string(analyzer::Analyzer* analyzer, const u_char*& data, int& len, int& rdlen) { - if ( rdlen <= 0 ) - return nullptr; - - uint8_t str_size = data[0]; - - --rdlen; - --len; - ++data; - - if ( str_size > rdlen ) { - analyzer->Weird("DNS_TXT_char_str_past_rdlen"); - return nullptr; - } - - auto rval = make_intrusive(str_size, reinterpret_cast(data)); - - rdlen -= str_size; - len -= str_size; - data += str_size; - - return rval; -} - bool DNS_Interpreter::ParseRR_HINFO(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength) { if ( ! dns_HINFO_reply || msg->skip_event ) { data += rdlength; diff --git a/src/analyzer/protocol/dns/DNS.h b/src/analyzer/protocol/dns/DNS.h index 57735470f2..1e449eacc3 100644 --- a/src/analyzer/protocol/dns/DNS.h +++ b/src/analyzer/protocol/dns/DNS.h @@ -364,6 +364,7 @@ protected: bool ParseRR_MX(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, const u_char* msg_start); bool ParseRR_NBS(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, const u_char* msg_start); bool ParseRR_SRV(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, const u_char* msg_start); + bool ParseRR_NAPTR(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, const u_char* msg_start); bool ParseRR_EDNS(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, const u_char* msg_start); bool ParseRR_EDNS_ECS(detail::DNS_MsgInfo* msg, const u_char*& data, int& len, int rdlength, const u_char* msg_start); diff --git a/src/analyzer/protocol/dns/events.bif b/src/analyzer/protocol/dns/events.bif index d271065642..f9bb501157 100644 --- a/src/analyzer/protocol/dns/events.bif +++ b/src/analyzer/protocol/dns/events.bif @@ -467,6 +467,23 @@ event dns_CAA_reply%(c: connection, msg: dns_msg, ans: dns_answer, flags: count, ## dns_skip_addl dns_skip_all_addl dns_skip_all_auth dns_skip_auth event dns_SRV_reply%(c: connection, msg: dns_msg, ans: dns_answer, target: string, priority: count, weight: count, p: count%); +## Generated for DNS replies of type *NAPTR*. For replies with multiple answers, +## an individual event of the corresponding type is raised for each. +## +## c: The connection, which may be UDP or TCP depending on the type of the +## transport-layer session being analyzed. +## +## msg: The parsed DNS message header. +## +## ans: The type-independent part of the parsed answer record. +## +## naptr: The parsed RDATA of NAPTR type record. +## +## .. zeek:see:: dns_AAAA_reply dns_A_reply dns_CNAME_reply dns_EDNS_addl +## dns_HINFO_reply dns_MX_reply dns_NS_reply dns_PTR_reply dns_SOA_reply +## dns_TSIG_addl dns_TXT_reply dns_SPF_reply dns_WKS_reply dns_SRV_reply dns_end +event dns_NAPTR_reply%(c: connection, msg: dns_msg, ans: dns_answer, naptr: dns_naptr_rr%); + ## Generated on DNS reply resource records when the type of record is not one ## that Zeek knows how to parse and generate another more specific event. ## diff --git a/testing/btest/Baseline/scripts.base.protocols.dns.naptr/.stdout b/testing/btest/Baseline/scripts.base.protocols.dns.naptr/.stdout new file mode 100644 index 0000000000..978e884e28 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.dns.naptr/.stdout @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +NAPTR, [id=20970, opcode=0, rcode=0, QR=T, AA=F, TC=F, RD=T, RA=T, Z=0, AD=F, CD=F, num_queries=1, num_answers=1, num_auth=0, num_addl=0], [answer_type=1, query=fp-de-carrier-vodafone.rcs.telephony.goog, qtype=35, qclass=1, TTL=2.0 mins 48.0 secs], [order=100, preference=100, flags=s, service=SIPS+D2T, regexp=, replacement=_sips._tcp.fp-de-carrier-vodafone.rcs.telephony.goog] diff --git a/testing/btest/Baseline/scripts.base.protocols.dns.naptr/dns.log.cut b/testing/btest/Baseline/scripts.base.protocols.dns.naptr/dns.log.cut new file mode 100644 index 0000000000..e3bed37e96 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.dns.naptr/dns.log.cut @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +uid query qclass_name qtype_name answers +CHhAvVGS1DHFjwGM9 fp-de-carrier-vodafone.rcs.telephony.goog C_INTERNET NAPTR NAPTR 100 100 s SIPS+D2T _sips._tcp.fp-de-carrier-vodafone.rcs.telephony.goog diff --git a/testing/btest/Baseline/scripts.base.protocols.dns.naptr/out b/testing/btest/Baseline/scripts.base.protocols.dns.naptr/out new file mode 100644 index 0000000000..978e884e28 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.dns.naptr/out @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +NAPTR, [id=20970, opcode=0, rcode=0, QR=T, AA=F, TC=F, RD=T, RA=T, Z=0, AD=F, CD=F, num_queries=1, num_answers=1, num_auth=0, num_addl=0], [answer_type=1, query=fp-de-carrier-vodafone.rcs.telephony.goog, qtype=35, qclass=1, TTL=2.0 mins 48.0 secs], [order=100, preference=100, flags=s, service=SIPS+D2T, regexp=, replacement=_sips._tcp.fp-de-carrier-vodafone.rcs.telephony.goog] diff --git a/testing/btest/Traces/dns/naptr.pcap b/testing/btest/Traces/dns/naptr.pcap new file mode 100644 index 0000000000..e785d53a23 Binary files /dev/null and b/testing/btest/Traces/dns/naptr.pcap differ diff --git a/testing/btest/scripts/base/protocols/dns/naptr.zeek b/testing/btest/scripts/base/protocols/dns/naptr.zeek new file mode 100644 index 0000000000..cd7de6d8ce --- /dev/null +++ b/testing/btest/scripts/base/protocols/dns/naptr.zeek @@ -0,0 +1,11 @@ +# @TEST-EXEC: zeek -b -r $TRACES/dns/naptr.pcap %INPUT >out +# @TEST-EXEC: btest-diff out +# @TEST-EXEC: zeek-cut -m uid query qclass_name qtype_name answers < dns.log > dns.log.cut +# @TEST-EXEC: btest-diff dns.log.cut + +@load base/protocols/dns + +event dns_NAPTR_reply(c: connection, msg: dns_msg, ans: dns_answer, naptr: dns_naptr_rr) + { + print "NAPTR", msg, ans, naptr; + }