diff --git a/doc/userguide/rules/dcerpc-keywords.rst b/doc/userguide/rules/dcerpc-keywords.rst index a84d0998478f..318a2e6636e7 100644 --- a/doc/userguide/rules/dcerpc-keywords.rst +++ b/doc/userguide/rules/dcerpc-keywords.rst @@ -68,6 +68,21 @@ Example:: dcerpc.stub_data; content:"123456"; +dcerpc.is_fragmented +-------------------- + +Match on whether a DCERPC PDU is fragmented. It takes a boolean value. + +Syntax:: + + dcerpc.is_fragmented: true|false; + +Example: + +.. container:: example-rule + + alert dcerpc any any -> any any (:example-rule-options:`dcerpc.is_fragmented: true;` sid: 1;) + Additional information ---------------------- diff --git a/rust/src/dcerpc/dcerpc.rs b/rust/src/dcerpc/dcerpc.rs index 7ea109b69f7d..64a0f207ee00 100644 --- a/rust/src/dcerpc/dcerpc.rs +++ b/rust/src/dcerpc/dcerpc.rs @@ -205,6 +205,8 @@ pub struct DCERPCTransaction { pub resp_lost: bool, pub req_cmd: u8, pub resp_cmd: u8, + pub req_flags: u16, + pub resp_flags: u16, pub activityuuid: Vec, pub seqnum: u32, pub tx_data: AppLayerTxData, @@ -585,6 +587,7 @@ impl DCERPCState { } let mut tx = self.create_tx(hdr); tx.req_cmd = hdr.hdrtype; + tx.req_flags = hdr.pfc_flags as u16; tx.req_done = true; if let Some(flow) = self.flow { sc_app_layer_parser_trigger_raw_stream_inspection( @@ -762,6 +765,7 @@ impl DCERPCState { match transaction { Some(ref mut tx) => { tx.req_cmd = hdr_type; + tx.req_flags = hdr.pfc_flags as u16; tx.ctxid = request.ctxid; tx.opnum = request.opnum; tx.first_request_seen = request.first_request_seen; @@ -769,6 +773,7 @@ impl DCERPCState { None => { let mut tx = self.create_tx(hdr); tx.req_cmd = hdr_type; + tx.req_flags = hdr.pfc_flags as u16; tx.ctxid = request.ctxid; tx.opnum = request.opnum; tx.first_request_seen = request.first_request_seen; @@ -949,10 +954,12 @@ impl DCERPCState { self.get_tx_by_call_id(current_call_id, Direction::ToClient, hdrtype) { tx.resp_cmd = hdrtype; + tx.resp_flags = hdr.pfc_flags as u16; tx } else { let mut tx = self.create_tx(&hdr); tx.resp_cmd = hdrtype; + tx.resp_flags = hdr.pfc_flags as u16; self.transactions.push_back(tx); self.transactions.back_mut().unwrap() }; @@ -980,10 +987,12 @@ impl DCERPCState { match transaction { Some(tx) => { tx.resp_cmd = hdrtype; + tx.resp_flags = hdr.pfc_flags as u16; } None => { let mut tx = self.create_tx(&hdr); tx.resp_cmd = hdrtype; + tx.resp_flags = hdr.pfc_flags as u16; self.transactions.push_back(tx); } }; diff --git a/rust/src/dcerpc/dcerpc_udp.rs b/rust/src/dcerpc/dcerpc_udp.rs index 538ee3d3c612..d2a1560e3b99 100644 --- a/rust/src/dcerpc/dcerpc_udp.rs +++ b/rust/src/dcerpc/dcerpc_udp.rs @@ -190,6 +190,7 @@ impl DCERPCUDPState { let max_size = cfg_max_stub_size() as usize; match hdr.pkt_type { DCERPC_TYPE_REQUEST => { + tx.req_flags = (hdr.flags1 as u16) | ((hdr.flags2 as u16) << 8); tx.frag_cnt_ts = tx.frag_cnt_ts.saturating_add(1); if input.len() + tx.stub_data_buffer_ts.len() < max_size { tx.stub_data_buffer_ts.extend_from_slice(input); @@ -203,6 +204,7 @@ impl DCERPCUDPState { return true; } DCERPC_TYPE_RESPONSE => { + tx.resp_flags = (hdr.flags1 as u16) | ((hdr.flags2 as u16) << 8); tx.frag_cnt_tc = tx.frag_cnt_tc.saturating_add(1); if input.len() + tx.stub_data_buffer_tc.len() < max_size { tx.stub_data_buffer_tc.extend_from_slice(input); diff --git a/rust/src/dcerpc/detect.rs b/rust/src/dcerpc/detect.rs index 03241f7a875f..a0f02d02f442 100644 --- a/rust/src/dcerpc/detect.rs +++ b/rust/src/dcerpc/detect.rs @@ -17,12 +17,15 @@ use super::dcerpc::{ DCERPCState, DCERPCTransaction, ALPROTO_DCERPC, DCERPC_TYPE_REQUEST, DCERPC_TYPE_RESPONSE, - DCERPC_UUID_ENTRY_FLAG_FF, + DCERPC_UUID_ENTRY_FLAG_FF, PFCL1_FRAG, PFC_FIRST_FRAG, PFC_LAST_FRAG, }; use crate::core::{STREAM_TOCLIENT, STREAM_TOSERVER}; use crate::detect::uint::{detect_match_uint, detect_parse_uint, DetectUintData}; use crate::detect::{helper_keyword_register_sticky_buffer, SigTableElmtStickyBuffer}; -use crate::smb::detect::{smb_tx_get_stub_data, smb_tx_match_dce_iface, smb_tx_match_dce_opnum}; +use crate::smb::detect::{ + smb_tx_get_stub_data, smb_tx_match_dce_iface, smb_tx_match_dce_is_fragmented, + smb_tx_match_dce_opnum, +}; use crate::smb::smb::ALPROTO_SMB; use std::ffi::CStr; use std::os::raw::{c_char, c_int, c_void}; @@ -393,6 +396,103 @@ unsafe extern "C" fn dcerpc_opnum_free(_de: *mut DetectEngineCtx, ptr: *mut c_vo } } +#[derive(Debug)] +pub struct DCERPCIsFragmentedData { + pub is_fragmented: bool, +} + +fn parse_is_fragmented(arg: &str) -> Option { + match arg.trim().to_ascii_lowercase().as_str() { + "true" => Some(true), + "false" => Some(false), + _ => None, + } +} + +unsafe fn dcerpc_is_fragmented_parse(carg: *const c_char) -> *mut c_void { + let arg = match CStr::from_ptr(carg).to_str() { + Ok(arg) => arg, + Err(_) => { + return std::ptr::null_mut(); + } + }; + match parse_is_fragmented(arg) { + Some(is_fragmented) => { + Box::into_raw(Box::new(DCERPCIsFragmentedData { is_fragmented })) as *mut c_void + } + None => std::ptr::null_mut(), + } +} + +unsafe fn dcerpc_tx_match_is_fragmented(flags: u8, tx: *mut c_void, ctx: *const SigMatchCtx) -> u8 { + let tx = cast_pointer!(tx, DCERPCTransaction); + let ctx = cast_pointer!(ctx, DCERPCIsFragmentedData); + + let (frag_cnt, tx_flags) = if flags & STREAM_TOSERVER != 0 { + (tx.frag_cnt_ts, tx.req_flags) + } else { + (tx.frag_cnt_tc, tx.resp_flags) + }; + // hack to tell if it's UDP + let fragmented = if !tx.activityuuid.is_empty() { + // For UDP, the flags1 fragment bit (0x04) explicitly marks a fragment + tx_flags & PFCL1_FRAG as u16 != 0 + } else { + // For TCP, a request/response PDU is a single complete message only + // when it sets both PFC_FIRST_FRAG and PFC_LAST_FRAG; any PDU missing + // either flag (first, middle or last fragment) is fragmented. The + // frag_cnt guard keeps a direction that has seen no PDU from matching. + let both = (PFC_FIRST_FRAG | PFC_LAST_FRAG) as u16; + frag_cnt > 0 && (tx_flags & both) != both + }; + if fragmented == ctx.is_fragmented { + return 1; + } + return 0; +} + +unsafe extern "C" fn dcerpc_is_fragmented_match( + _de: *mut DetectEngineThreadCtx, f: *mut crate::flow::Flow, flags: u8, _state: *mut c_void, + tx: *mut c_void, _sig: *const Signature, ctx: *const SigMatchCtx, +) -> c_int { + if SCFlowGetAppProtocol(f) == ALPROTO_DCERPC { + return dcerpc_tx_match_is_fragmented(flags, tx, ctx) as c_int; + } + + return smb_tx_match_dce_is_fragmented(flags, tx, ctx) as c_int; +} + +unsafe extern "C" fn dcerpc_is_fragmented_setup( + de: *mut DetectEngineCtx, s: *mut Signature, raw: *const libc::c_char, +) -> c_int { + if SCDetectSignatureSetAppProto(s, ALPROTO_DCERPC) != 0 { + return -1; + } + let ctx = dcerpc_is_fragmented_parse(raw); + if ctx.is_null() { + return -1; + } + if SCSigMatchAppendSMToList( + de, + s, + G_DCERPC_IS_FRAGMENTED_KW_ID, + ctx as *mut SigMatchCtx, + G_DCERPC_GENERIC_BUFFER_ID, + ) + .is_null() + { + dcerpc_is_fragmented_free(std::ptr::null_mut(), ctx); + return -1; + } + return 0; +} + +unsafe extern "C" fn dcerpc_is_fragmented_free(_de: *mut DetectEngineCtx, ptr: *mut c_void) { + if !ptr.is_null() { + std::mem::drop(Box::from_raw(ptr as *mut DCERPCIsFragmentedData)); + } +} + unsafe extern "C" fn dcerpc_stub_data_setup( de_ctx: *mut DetectEngineCtx, s: *mut Signature, _str: *const c_char, ) -> c_int { @@ -440,6 +540,7 @@ unsafe extern "C" fn dcerpc_tx_get_stub_data( } static mut G_DCERPC_OPNUM_KW_ID: u16 = 0; +static mut G_DCERPC_IS_FRAGMENTED_KW_ID: u16 = 0; static mut G_DCERPC_GENERIC_BUFFER_ID: c_int = 0; static mut G_DCERPC_IFACE_KW_ID: u16 = 0; static mut G_DCERPC_STUB_BUFFER_ID: c_int = 0; @@ -474,6 +575,17 @@ pub unsafe extern "C" fn SCDetectDcerpcRegister() { b"dce_opnum\0".as_ptr() as *const libc::c_char, ); + let kw = SCSigTableAppLiteElmt { + name: b"dcerpc.is_fragmented\0".as_ptr() as *const libc::c_char, + desc: b"match if the DCERPC PDU is fragmented\0".as_ptr() as *const libc::c_char, + url: b"/rules/dcerpc-keywords.html#dcerpc-is-fragmented\0".as_ptr() as *const libc::c_char, + AppLayerTxMatch: Some(dcerpc_is_fragmented_match), + Setup: Some(dcerpc_is_fragmented_setup), + Free: Some(dcerpc_is_fragmented_free), + flags: 0, + }; + G_DCERPC_IS_FRAGMENTED_KW_ID = SCDetectHelperKeywordRegister(&kw); + let kw = SCSigTableAppLiteElmt { name: b"dcerpc.iface\0".as_ptr() as *const libc::c_char, desc: b"match on the value of the interface UUID in a DCERPC header\0".as_ptr() diff --git a/rust/src/smb/dcerpc.rs b/rust/src/smb/dcerpc.rs index 148e2f621596..919fb71c1cad 100644 --- a/rust/src/smb/dcerpc.rs +++ b/rust/src/smb/dcerpc.rs @@ -94,6 +94,8 @@ pub struct SMBTransactionDCERPC { pub res_cmd: u8, pub res_set: bool, pub call_id: u32, + pub req_is_fragmented: bool, + pub resp_is_fragmented: bool, pub frag_cnt_ts: u16, pub frag_cnt_tc: u16, pub stub_data_ts: Vec, @@ -263,6 +265,7 @@ pub fn smb_write_dcerpc_record( SCLogDebug!("first frag size {}", recr.data.len()); tdn.opnum = recr.opnum; tdn.context_id = recr.context_id; + tdn.req_is_fragmented = !(dcer.first_frag && dcer.last_frag); tdn.frag_cnt_ts = tdn.frag_cnt_ts.saturating_add(1); let max_size = cfg_max_stub_size() as usize; if tdn.stub_data_ts.len() + recr.data.len() < max_size { @@ -452,6 +455,7 @@ fn dcerpc_response_handle(tx: &mut SMBTransaction, vercmd: SMBVerCmdStat, dcer: if let Some(SMBTransactionTypeData::DCERPC(ref mut tdn)) = tx.type_data { SCLogDebug!("CMD 11 found at tx {}", tx.id); tdn.set_result(DCERPC_TYPE_RESPONSE); + tdn.resp_is_fragmented = !(dcer.first_frag && dcer.last_frag); let max_size = cfg_max_stub_size() as usize; tdn.frag_cnt_tc = tdn.frag_cnt_tc.saturating_add(1); if tdn.stub_data_tc.len() + respr.data.len() < max_size { diff --git a/rust/src/smb/detect.rs b/rust/src/smb/detect.rs index dcb16bc4d5a4..e611fea28a44 100644 --- a/rust/src/smb/detect.rs +++ b/rust/src/smb/detect.rs @@ -18,7 +18,9 @@ use super::smb::ALPROTO_SMB; use crate::core::{STREAM_TOCLIENT, STREAM_TOSERVER}; use crate::dcerpc::dcerpc::DCERPC_TYPE_REQUEST; -use crate::dcerpc::detect::{DCEIfaceData, DCEOpnumData, DETECT_DCE_OPNUM_RANGE_UNINITIALIZED}; +use crate::dcerpc::detect::{ + DCEIfaceData, DCEOpnumData, DCERPCIsFragmentedData, DETECT_DCE_OPNUM_RANGE_UNINITIALIZED, +}; use crate::detect::uint::detect_match_uint; use crate::detect::{helper_keyword_register_sticky_buffer, SigTableElmtStickyBuffer}; use crate::direction::Direction; @@ -125,6 +127,26 @@ pub(crate) unsafe extern "C" fn smb_tx_match_dce_opnum( return 0; } +pub(crate) unsafe extern "C" fn smb_tx_match_dce_is_fragmented( + flags: u8, tx: *mut c_void, ctx: *const SigMatchCtx, +) -> u8 { + let tx = cast_pointer!(tx, SMBTransaction); + let ctx = cast_pointer!(ctx, DCERPCIsFragmentedData); + + if let Some(SMBTransactionTypeData::DCERPC(ref x)) = tx.type_data { + let fragmented = if flags & STREAM_TOSERVER != 0 { + x.req_is_fragmented + } else { + x.resp_is_fragmented + }; + if fragmented == ctx.is_fragmented { + return 1; + } + } + + return 0; +} + /* mimic logic that is/was in the C code: * - match on REQUEST (so not on BIND/BINDACK (probably for mixing with * dce_opnum and dce_stub_data)