From 13df0f7a06581b2aec569a12a4dfc3a192f8942c Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Wed, 31 Jul 2024 14:41:58 +0200 Subject: [PATCH] llmnr: implement parser This adds a parser for LLMNR protocol over both UDP and TCP. The parser reuses the existing DNS functions since LLMNR shares the same wire format as DNS, but implements its own protocol-specific validation: - LLMNR-specific flag checks (C, TC, T bits) - Opcode validation (only standard query opcode 0 is valid) - An Event is set if Z-flag is set, since it's must be zero per RFC4795 LLMNR transactions inherit DNS behavior where each packet creates a new state with one transaction. Ticket #8366 --- rust/src/dns/dns.rs | 15 ++-- rust/src/lib.rs | 1 + rust/src/llmnr/llmnr.rs | 162 ++++++++++++++++++++++++++++++++++++++++ rust/src/llmnr/mod.rs | 22 ++++++ rust/sys/src/sys.rs | 5 +- src/app-layer-parser.c | 2 + src/app-layer-protos.h | 1 + src/app-layer.c | 1 + suricata.yaml.in | 6 ++ 9 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 rust/src/llmnr/llmnr.rs create mode 100644 rust/src/llmnr/mod.rs diff --git a/rust/src/dns/dns.rs b/rust/src/dns/dns.rs index f0cb041e68..7cefb189f4 100644 --- a/rust/src/dns/dns.rs +++ b/rust/src/dns/dns.rs @@ -361,6 +361,7 @@ impl ConfigTracker { pub(crate) enum DnsVariant { Dns, MulticastDns, + Llmnr, } impl DnsVariant { @@ -371,6 +372,10 @@ impl DnsVariant { pub fn is_mdns(&self) -> bool { matches!(self, DnsVariant::MulticastDns) } + + pub fn is_llmnr(&self) -> bool { + matches!(self, DnsVariant::Llmnr) + } } //#[derive(Default)] @@ -454,7 +459,7 @@ pub(crate) fn dns_parse_request( tx.set_event(DNSEvent::ZFlagSet); } - if opcode >= 7 { + if (variant.is_llmnr() && opcode != 0) || opcode >= 7 { tx.set_event(DNSEvent::InvalidOpcode); } @@ -939,7 +944,7 @@ pub(crate) unsafe extern "C" fn parse_request( AppLayerResult::ok() } -unsafe extern "C" fn parse_response( +pub(crate) unsafe extern "C" fn parse_response( flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState, stream_slice: StreamSlice, _data: *mut std::os::raw::c_void, ) -> AppLayerResult { @@ -949,7 +954,7 @@ unsafe extern "C" fn parse_response( } /// C binding parse a DNS request. Returns 1 on success, -1 on failure. -unsafe extern "C" fn parse_request_tcp( +pub(crate) unsafe extern "C" fn parse_request_tcp( flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState, stream_slice: StreamSlice, _data: *mut std::os::raw::c_void, ) -> AppLayerResult { @@ -962,7 +967,7 @@ unsafe extern "C" fn parse_request_tcp( AppLayerResult::ok() } -unsafe extern "C" fn parse_response_tcp( +pub(crate) unsafe extern "C" fn parse_response_tcp( flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState, stream_slice: StreamSlice, _data: *mut std::os::raw::c_void, ) -> AppLayerResult { @@ -1228,7 +1233,7 @@ pub(crate) unsafe extern "C" fn probe_udp( return 0; } -unsafe extern "C" fn c_probe_tcp( +pub(crate) unsafe extern "C" fn c_probe_tcp( _flow: *const Flow, direction: u8, input: *const u8, len: u32, rdir: *mut u8, ) -> AppProto { if input.is_null() || len < std::mem::size_of::() as u32 + 2 { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 262a000ad6..45f1688d91 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -140,6 +140,7 @@ pub mod sdp; pub mod ldap; pub mod flow; pub use suricata_ffi::direction; +pub mod llmnr; #[allow(unused_imports)] pub use suricata_lua_sys; diff --git a/rust/src/llmnr/llmnr.rs b/rust/src/llmnr/llmnr.rs new file mode 100644 index 0000000000..6b086f6ca4 --- /dev/null +++ b/rust/src/llmnr/llmnr.rs @@ -0,0 +1,162 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * 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 + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// written by Giuseppe Longo + +use std; +use std::ffi::CString; + +use crate::applayer::*; +use crate::core::*; +use crate::dns::dns; +use crate::dns::dns::DNSHeader; +use crate::flow::Flow; + +use suricata_sys::sys::{ + AppProto, AppProtoEnum, SCAppLayerParserConfParserEnabled, + SCAppLayerProtoDetectConfProtoDetectionEnabled, +}; + +static mut ALPROTO_LLMNR: AppProto = ALPROTO_UNKNOWN; + +unsafe extern "C" fn probe_udp( + _flow: *const Flow, _dir: u8, input: *const u8, len: u32, _rdir: *mut u8, +) -> AppProto { + if crate::dns::dns::probe_udp(_flow, _dir, input, len, _rdir) + == AppProtoEnum::ALPROTO_DNS as u16 + { + return ALPROTO_LLMNR; + } + return 0; +} + +unsafe extern "C" fn probe_tcp( + _flow: *const Flow, _dir: u8, input: *const u8, len: u32, _rdir: *mut u8, +) -> AppProto { + if crate::dns::dns::c_probe_tcp(_flow, _dir, input, len, _rdir) + == AppProtoEnum::ALPROTO_DNS as u16 + { + return ALPROTO_LLMNR; + } + return 0; +} + +pub(crate) extern "C" fn state_new( + _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto, +) -> *mut std::os::raw::c_void { + let state = dns::DNSState::new_variant(dns::DnsVariant::Llmnr); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut _; +} + +#[no_mangle] +pub unsafe extern "C" fn SCRegisterLLMNRUdpParser() { + let default_port = std::ffi::CString::new("5355").unwrap(); + let parser = RustParser { + name: b"llmnr\0".as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_UDP, + probe_ts: Some(probe_udp), + probe_tc: Some(probe_udp), + min_depth: 0, + max_depth: std::mem::size_of::() as u16, + state_new, + state_free: dns::state_free, + tx_free: dns::state_tx_free, + parse_ts: dns::parse_request, + parse_tc: dns::parse_response, + get_tx_count: dns::state_get_tx_count, + get_tx: dns::state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: dns::tx_get_alstate_progress, + get_eventinfo: Some(dns::DNSEvent::get_event_info), + get_eventinfo_byid: Some(dns::DNSEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some( + crate::applayer::state_get_tx_iterator::, + ), + get_tx_data: dns::state_get_tx_data, + get_state_data: dns::dns_get_state_data, + apply_tx_config: None, + flags: 0, + get_frame_id_by_name: Some(dns::DnsFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(dns::DnsFrameType::ffi_name_from_id), + get_state_id_by_name: None, + get_state_name_by_id: None, + }; + + let ip_proto_str = CString::new("udp").unwrap(); + if SCAppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = applayer_register_protocol_detection(&parser, 1); + ALPROTO_LLMNR = alproto; + if SCAppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn SCRegisterLLMNRTcpParser() { + let default_port = std::ffi::CString::new("5355").unwrap(); + let parser = RustParser { + name: b"llmnr\0".as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(probe_tcp), + probe_tc: Some(probe_tcp), + min_depth: 0, + max_depth: std::mem::size_of::() as u16 + 2, + state_new, + state_free: dns::state_free, + tx_free: dns::state_tx_free, + parse_ts: dns::parse_request_tcp, + parse_tc: dns::parse_response_tcp, + get_tx_count: dns::state_get_tx_count, + get_tx: dns::state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: dns::tx_get_alstate_progress, + get_eventinfo: Some(dns::DNSEvent::get_event_info), + get_eventinfo_byid: Some(dns::DNSEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some( + crate::applayer::state_get_tx_iterator::, + ), + get_tx_data: dns::state_get_tx_data, + get_state_data: dns::dns_get_state_data, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + get_frame_id_by_name: Some(dns::DnsFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(dns::DnsFrameType::ffi_name_from_id), + get_state_id_by_name: None, + get_state_name_by_id: None, + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + if SCAppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = applayer_register_protocol_detection(&parser, 1); + ALPROTO_LLMNR = alproto; + if SCAppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + } +} diff --git a/rust/src/llmnr/mod.rs b/rust/src/llmnr/mod.rs new file mode 100644 index 0000000000..c5ff69eabd --- /dev/null +++ b/rust/src/llmnr/mod.rs @@ -0,0 +1,22 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * 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 + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +//! LLMNR protocol parser and logger module. + +// written by Giuseppe Longo + +pub mod llmnr; diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs index 65c941a459..0ef83233b5 100644 --- a/rust/sys/src/sys.rs +++ b/rust/sys/src/sys.rs @@ -67,8 +67,9 @@ pub enum AppProtoEnum { ALPROTO_BITTORRENT_DHT = 35, ALPROTO_POP3 = 36, ALPROTO_MDNS = 37, - ALPROTO_HTTP = 38, - ALPROTO_MAX_STATIC = 39, + ALPROTO_LLMNR = 38, + ALPROTO_HTTP = 39, + ALPROTO_MAX_STATIC = 40, } pub type AppProto = u16; extern "C" { diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index ad01110b05..f26efea15f 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1851,6 +1851,8 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterHTTP2Parsers(); SCRegisterTelnetParser(); RegisterIMAPParsers(); + SCRegisterLLMNRUdpParser(); + SCRegisterLLMNRTcpParser(); for (size_t i = 0; i < preregistered_callbacks_nb; i++) { PreRegisteredCallbacks[i](); diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index 2498c744d2..6eca4c99a0 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -70,6 +70,7 @@ enum AppProtoEnum { ALPROTO_BITTORRENT_DHT, ALPROTO_POP3, ALPROTO_MDNS, + ALPROTO_LLMNR, // signature-only (ie not seen in flow) // HTTP for any version (ALPROTO_HTTP1 (version 1) or ALPROTO_HTTP2) diff --git a/src/app-layer.c b/src/app-layer.c index e5c075ccb9..98fb98cade 100644 --- a/src/app-layer.c +++ b/src/app-layer.c @@ -1072,6 +1072,7 @@ static void AppLayerNamesSetup(void) AppProtoRegisterProtoString(ALPROTO_LDAP, "ldap"); AppProtoRegisterProtoString(ALPROTO_DOH2, "doh2"); AppProtoRegisterProtoString(ALPROTO_MDNS, "mdns"); + AppProtoRegisterProtoString(ALPROTO_LLMNR, "llmnr"); AppProtoRegisterProtoString(ALPROTO_TEMPLATE, "template"); AppProtoRegisterProtoString(ALPROTO_RDP, "rdp"); AppProtoRegisterProtoString(ALPROTO_HTTP2, "http2"); diff --git a/suricata.yaml.in b/suricata.yaml.in index b2c63cb5f8..ec1a4afc80 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -1296,6 +1296,12 @@ app-layer: mdns: enabled: yes + llmnr: + enabled: yes + detection-ports: + dp: 5355 + sp: 5355 + # Limit for the maximum number of asn1 frames to decode (default 256) asn1-max-frames: 256