diff --git a/doc/userguide/rules/dcerpc-keywords.rst b/doc/userguide/rules/dcerpc-keywords.rst index a84d099847..697f356efa 100644 --- a/doc/userguide/rules/dcerpc-keywords.rst +++ b/doc/userguide/rules/dcerpc-keywords.rst @@ -59,6 +59,53 @@ Examples:: dcerpc.opnum can since suricata 9 use an :ref:`unsigned 16-bits integer `. +dcerpc.ptype +------------ + +Match on the PDU type of a DCERPC header. On a to_server match the request PDU +type is used, on a to_client match the response PDU type is used. + +The format of the keyword:: + + dcerpc.ptype:; + dcerpc.ptype:[>,<,!,=]; + +The PDU type values are: + +===== ===================== +Value PDU type +===== ===================== +0 request +1 ping +2 response +3 fault +4 working +5 nocall +6 reject +7 ack +8 cl_cancel +9 fack +10 cancel_ack +11 bind +12 bind_ack +13 bind_nak +14 alter_context +15 alter_context_resp +16 auth3 +17 shutdown +18 co_cancel +19 orphaned +20 rts +===== ===================== + +Examples:: + + dcerpc.ptype:11; + dcerpc.ptype:!0; + dcerpc.ptype:>10; + +dcerpc.ptype uses an :ref:`unsigned 8-bits integer `. + dcerpc.stub_data ---------------- diff --git a/etc/schema.json b/etc/schema.json index 4cff77cefb..53caf436e9 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -469,7 +469,12 @@ } }, "request": { - "type": "string" + "type": "string", + "suricata": { + "keywords": [ + "dcerpc.ptype" + ] + } }, "res": { "type": "object", @@ -484,7 +489,12 @@ } }, "response": { - "type": "string" + "type": "string", + "suricata": { + "keywords": [ + "dcerpc.ptype" + ] + } }, "rpc_version": { "type": "string" diff --git a/rust/src/dcerpc/dcerpc.rs b/rust/src/dcerpc/dcerpc.rs index f5a7f2b816..7c3ad0efef 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_seen: bool, + pub resp_seen: bool, pub activityuuid: Vec, pub seqnum: u32, pub tx_data: AppLayerTxData, @@ -221,9 +223,9 @@ impl DCERPCTransaction { return Self { stub_data_buffer_ts: Vec::new(), stub_data_buffer_tc: Vec::new(), - req_cmd: DCERPC_TYPE_REQUEST, - resp_cmd: DCERPC_TYPE_RESPONSE, activityuuid: Vec::new(), + req_cmd: DCERPC_TYPE_UNKNOWN, + resp_cmd: DCERPC_TYPE_UNKNOWN, tx_data: AppLayerTxData::new(), ..Default::default() }; @@ -278,13 +280,13 @@ pub struct Uuid { pub node: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct DCERPCHdr { pub rpc_vers: u8, pub rpc_vers_minor: u8, pub hdrtype: u8, pub pfc_flags: u8, - pub packed_drep: Vec, + pub packed_drep: [u8; 4], pub frag_length: u16, pub auth_length: u16, pub call_id: u32, @@ -445,8 +447,8 @@ impl DCERPCState { if tx.req_done || tx.req_lost { continue; } - let resp_cmd = get_resp_type_for_req(cmd); - if resp_cmd != tx.resp_cmd { + /* do not match if the transaction's command does not match expected */ + if tx.req_cmd != cmd && get_req_type_for_resp(tx.resp_cmd) != cmd { continue; } } @@ -454,8 +456,8 @@ impl DCERPCState { if tx.resp_done || tx.resp_lost { continue; } - let req_cmd = get_req_type_for_resp(cmd); - if req_cmd != tx.req_cmd { + /* do not match if the transaction's command does not match expected */ + if tx.resp_cmd != cmd && get_resp_type_for_req(tx.req_cmd) != cmd { continue; } } @@ -585,6 +587,7 @@ impl DCERPCState { } let mut tx = self.create_tx(hdr); tx.req_cmd = hdr.hdrtype; + tx.req_seen = true; 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_seen = true; 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_seen = true; tx.ctxid = request.ctxid; tx.opnum = request.opnum; tx.first_request_seen = request.first_request_seen; @@ -796,6 +801,24 @@ impl DCERPCState { } } + fn save_unhandled_ptypes(&mut self, hdr: &DCERPCHdr, direction: Direction) { + /* Create tx even on unhandled PDU types to be able to + * at least issue a match on header only fields */ + let mut tx = self.create_tx(hdr); + if direction == Direction::ToServer { + tx.req_cmd = hdr.hdrtype; + tx.req_seen = true; + } else { + tx.resp_cmd = hdr.hdrtype; + tx.resp_seen = true; + } + tx.req_done = true; + tx.resp_done = true; + self.transactions.push_back(tx); + // recognized DCERPC PDU types that we do not process + SCLogDebug!("Unhandled packet type: {:?}", hdr.hdrtype); + } + pub fn handle_input_data( &mut self, stream_slice: StreamSlice, direction: Direction, ) -> AppLayerResult { @@ -888,6 +911,15 @@ impl DCERPCState { // input only consists of the header and that was consumed, so, return early cur_i = &cur_i[fraglen as usize..]; consumed += fraglen as u32; + // save header-only PDU types too + if matches!( + hdr.hdrtype, + DCERPC_TYPE_SHUTDOWN | DCERPC_TYPE_CO_CANCEL | DCERPC_TYPE_ORPHANED + ) { + self.save_unhandled_ptypes(&hdr, direction); + /* DCERPC_TYPE_CO_CANCEL and ORPHANED can optionally be more than header + * too so they're also handled later on the in the unrecognized branch */ + } continue; } cmp::Ordering::Greater => {} @@ -949,10 +981,12 @@ impl DCERPCState { self.get_tx_by_call_id(current_call_id, Direction::ToClient, hdrtype) { tx.resp_cmd = hdrtype; + tx.resp_seen = true; tx } else { let mut tx = self.create_tx(&hdr); tx.resp_cmd = hdrtype; + tx.resp_seen = true; self.transactions.push_back(tx); self.transactions.back_mut().unwrap() }; @@ -980,10 +1014,12 @@ impl DCERPCState { match transaction { Some(tx) => { tx.resp_cmd = hdrtype; + tx.resp_seen = true; } None => { let mut tx = self.create_tx(&hdr); tx.resp_cmd = hdrtype; + tx.resp_seen = true; self.transactions.push_back(tx); } }; @@ -997,9 +1033,24 @@ impl DCERPCState { return AppLayerResult::err(); } } + DCERPC_TYPE_PING + | DCERPC_TYPE_FAULT + | DCERPC_TYPE_WORKING + | DCERPC_TYPE_NOCALL + | DCERPC_TYPE_REJECT + | DCERPC_TYPE_ACK + | DCERPC_TYPE_CL_CANCEL + | DCERPC_TYPE_FACK + | DCERPC_TYPE_CANCEL_ACK + | DCERPC_TYPE_BINDNAK + | DCERPC_TYPE_AUTH3 + | DCERPC_TYPE_CO_CANCEL + | DCERPC_TYPE_ORPHANED + | DCERPC_TYPE_RTS => { + self.save_unhandled_ptypes(&hdr, direction); + } _ => { SCLogDebug!("Unrecognized packet type: {:?}", hdrtype); - // skip unrecognized packet types such as AUTH3 } } consumed += fraglen as u32; diff --git a/rust/src/dcerpc/dcerpc_udp.rs b/rust/src/dcerpc/dcerpc_udp.rs index 538ee3d3c6..898b7df19b 100644 --- a/rust/src/dcerpc/dcerpc_udp.rs +++ b/rust/src/dcerpc/dcerpc_udp.rs @@ -19,7 +19,10 @@ use crate::applayer::{self, *}; use crate::core; use crate::dcerpc::dcerpc::{ cfg_max_stub_size, get_alstate_progress, DCERPCTransaction, ALPROTO_DCERPC, DCERPC_MAX_TX, - DCERPC_TYPE_REQUEST, DCERPC_TYPE_RESPONSE, PARSER_NAME, PFCL1_FRAG, PFCL1_LASTFRAG, + DCERPC_TYPE_ACK, DCERPC_TYPE_CANCEL_ACK, DCERPC_TYPE_CL_CANCEL, DCERPC_TYPE_FACK, + DCERPC_TYPE_FAULT, DCERPC_TYPE_NOCALL, DCERPC_TYPE_PING, DCERPC_TYPE_REJECT, + DCERPC_TYPE_REQUEST, DCERPC_TYPE_RESPONSE, DCERPC_TYPE_WORKING, PARSER_NAME, PFCL1_FRAG, + PFCL1_LASTFRAG, }; use crate::dcerpc::parser; use crate::direction::{Direction, DIR_BOTH}; @@ -163,8 +166,8 @@ impl DCERPCUDPState { } pub fn handle_fragment_data(&mut self, hdr: &DCERPCHdrUdp, input: &[u8]) -> bool { - if hdr.pkt_type != DCERPC_TYPE_REQUEST && hdr.pkt_type != DCERPC_TYPE_RESPONSE { - SCLogDebug!("Unrecognized packet type"); + if hdr.pkt_type > DCERPC_TYPE_CANCEL_ACK { + SCLogDebug!("Unrecognized packet type: {}", hdr.pkt_type); return false; } @@ -190,6 +193,8 @@ impl DCERPCUDPState { let max_size = cfg_max_stub_size() as usize; match hdr.pkt_type { DCERPC_TYPE_REQUEST => { + tx.req_cmd = hdr.pkt_type; + tx.req_seen = true; 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 +208,8 @@ impl DCERPCUDPState { return true; } DCERPC_TYPE_RESPONSE => { + tx.resp_cmd = hdr.pkt_type; + tx.resp_seen = true; 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); @@ -215,8 +222,32 @@ impl DCERPCUDPState { } return true; } + /* Unhandled connectionless PDU types are not otherwise + * processed; store the PDU type so dcerpc.ptype can match */ + DCERPC_TYPE_PING | DCERPC_TYPE_ACK | DCERPC_TYPE_CL_CANCEL => { + // client-to-server PDU types + tx.req_cmd = hdr.pkt_type; + tx.req_seen = true; + tx.req_done = true; + tx.resp_done = true; + return true; + } + // server-to-client PDU types (fack is bidirectional in CL and + // is recorded here as a response) + DCERPC_TYPE_FAULT + | DCERPC_TYPE_WORKING + | DCERPC_TYPE_NOCALL + | DCERPC_TYPE_REJECT + | DCERPC_TYPE_FACK + | DCERPC_TYPE_CANCEL_ACK => { + tx.resp_cmd = hdr.pkt_type; + tx.resp_seen = true; + tx.req_done = true; + tx.resp_done = true; + return true; + } _ => { - // unreachable + // unreachable: pkt_type is guarded to 0..=10 above } } } diff --git a/rust/src/dcerpc/detect.rs b/rust/src/dcerpc/detect.rs index 03241f7a87..9d097cb68d 100644 --- a/rust/src/dcerpc/detect.rs +++ b/rust/src/dcerpc/detect.rs @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Open Information Security Foundation +/* Copyright (C) 2020-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 @@ -22,7 +22,9 @@ use super::dcerpc::{ 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_opnum, smb_tx_match_dce_ptype, +}; use crate::smb::smb::ALPROTO_SMB; use std::ffi::CStr; use std::os::raw::{c_char, c_int, c_void}; @@ -439,9 +441,85 @@ unsafe extern "C" fn dcerpc_tx_get_stub_data( return buffer; } +fn parse_dcerpc_ptype(arg: &str) -> Option> { + if let Ok((_, ctx)) = detect_parse_uint::(arg) { + return Some(ctx); + } + None +} + +unsafe extern "C" fn dcerpc_ptype_parse(carg: *const c_char) -> *mut c_void { + if let Ok(arg) = CStr::from_ptr(carg).to_str() { + if let Some(detect) = parse_dcerpc_ptype(arg) { + return Box::into_raw(Box::new(detect)) as *mut _; + } + } + return std::ptr::null_mut(); +} + +unsafe extern "C" fn dcerpc_ptype_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_ptype_parse(raw); + if ctx.is_null() { + return -1; + } + if SCSigMatchAppendSMToList( + de, + s, + G_DCERPC_PTYPE_KW_ID, + ctx as *mut SigMatchCtx, + G_DCERPC_GENERIC_BUFFER_ID, + ) + .is_null() + { + dcerpc_ptype_free(std::ptr::null_mut(), ctx); + return -1; + } + return 0; +} + +unsafe fn dcerpc_tx_match_dce_ptype(tx: *mut c_void, ctx: *const SigMatchCtx, flags: u8) -> c_int { + let tx = cast_pointer!(tx, DCERPCTransaction); + let du8 = cast_pointer!(ctx, DetectUintData); + + if flags & STREAM_TOSERVER != 0 { + if tx.req_seen && detect_match_uint(du8, tx.req_cmd) { + return 1; + } + } else if tx.resp_seen && detect_match_uint(du8, tx.resp_cmd) { + return 1; + } + return 0; +} + +unsafe extern "C" fn dcerpc_ptype_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_dce_ptype(tx, ctx, flags); + } + if smb_tx_match_dce_ptype(tx, ctx, flags) != 1 { + return 0; + } + + return 1; +} + +unsafe extern "C" fn dcerpc_ptype_free(_de: *mut DetectEngineCtx, ptr: *mut c_void) { + if !ptr.is_null() { + std::mem::drop(Box::from_raw(ptr as *mut DetectUintData)); + } +} + static mut G_DCERPC_OPNUM_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_PTYPE_KW_ID: u16 = 0; static mut G_DCERPC_STUB_BUFFER_ID: c_int = 0; #[no_mangle] @@ -490,6 +568,17 @@ pub unsafe extern "C" fn SCDetectDcerpcRegister() { b"dce_iface\0".as_ptr() as *const libc::c_char, ); + let kw = SCSigTableAppLiteElmt { + name: b"dcerpc.ptype\0".as_ptr() as *const libc::c_char, + desc: b"match on the PDU type in a DCERPC header\0".as_ptr() as *const libc::c_char, + url: b"/rules/dcerpc-keywords.html#dcerpc-ptype\0".as_ptr() as *const libc::c_char, + AppLayerTxMatch: Some(dcerpc_ptype_match), + Setup: Some(dcerpc_ptype_setup), + Free: Some(dcerpc_ptype_free), + flags: 0, + }; + G_DCERPC_PTYPE_KW_ID = SCDetectHelperKeywordRegister(&kw); + let kw_stub = SigTableElmtStickyBuffer { name: String::from("dcerpc.stub_data"), desc: String::from("match on the stub data in a DCERPC packet"), diff --git a/rust/src/dcerpc/parser.rs b/rust/src/dcerpc/parser.rs index a80126b1c7..3fe2adea48 100644 --- a/rust/src/dcerpc/parser.rs +++ b/rust/src/dcerpc/parser.rs @@ -233,7 +233,12 @@ pub(super) fn parse_dcerpc_header(i: &[u8]) -> IResult<&[u8], DCERPCHdr> { rpc_vers_minor, hdrtype, pfc_flags, - packed_drep: packed_drep.to_vec(), + packed_drep: [ + packed_drep[0], + packed_drep[1], + packed_drep[2], + packed_drep[3], + ], frag_length, auth_length, call_id, diff --git a/rust/src/smb/detect.rs b/rust/src/smb/detect.rs index dcb16bc4d5..0411a5626c 100644 --- a/rust/src/smb/detect.rs +++ b/rust/src/smb/detect.rs @@ -19,7 +19,7 @@ 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::detect::uint::detect_match_uint; +use crate::detect::uint::{detect_match_uint, DetectUintData}; use crate::detect::{helper_keyword_register_sticky_buffer, SigTableElmtStickyBuffer}; use crate::direction::Direction; use crate::smb::smb::*; @@ -125,6 +125,25 @@ pub(crate) unsafe extern "C" fn smb_tx_match_dce_opnum( return 0; } +pub(crate) unsafe extern "C" fn smb_tx_match_dce_ptype( + tx: *mut c_void, ctx: *const SigMatchCtx, flags: u8, +) -> u8 { + let tx = cast_pointer!(tx, SMBTransaction); + let du8 = cast_pointer!(ctx, DetectUintData); + + if let Some(SMBTransactionTypeData::DCERPC(ref x)) = tx.type_data { + if flags & STREAM_TOSERVER != 0 { + if x.req_set && detect_match_uint(du8, x.req_cmd) { + return 1; + } + } else if x.res_set && detect_match_uint(du8, x.res_cmd) { + 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)