diff --git a/doc/userguide/rules/transforms.rst b/doc/userguide/rules/transforms.rst index ae16aafed2..7012f2db85 100644 --- a/doc/userguide/rules/transforms.rst +++ b/doc/userguide/rules/transforms.rst @@ -193,10 +193,19 @@ xor Takes the buffer, applies xor decoding. -The key can be a hexadecimal string or a variable specified inline using -``extract ``. When a variable key is used, the engine reads -```` bytes starting at ```` in the raw inspection buffer -at transform time. +The key can be given as: + +* a literal hexadecimal string (``xor:""``); +* an inline buffer location (``xor:extract ``), where the + engine reads ```` bytes starting at ```` in the raw inspection + buffer at transform time; or +* the name of a ``byte_extract`` or ``byte_math`` variable + (``xor:var []``), whose value is used as the key, read at + inspection time. + +Exactly one key form may be given per ``xor`` transform. The hexadecimal string, +``extract``, and ``var`` forms are mutually exclusive and cannot be combined in a +single transform; for example, ``xor:extract 1 0 var xkey`` is rejected. An optional ``offset`` parameter specifies the byte position in the buffer where XOR decoding starts. Bytes before this position are left as-is. @@ -207,13 +216,15 @@ Syntax:: xor:"" xor:extract + xor:var [] xor:offset ,"" xor:offset ,extract + xor:offset ,var [] Quotes around a hex key are optional; ``xor:0d0ac8ff`` and ``xor:"0d0ac8ff"`` are equivalent. -This example alerts if ``http.uri`` contains ``password=`` xored with 4-bytes key ``0d0ac8ff``: +This example alerts if ``http.uri`` contains ``password=`` xored with 4-byte key ``0d0ac8ff``: .. container:: example-rule @@ -229,6 +240,39 @@ matches ``infected`` in the decoded data: alert http any any -> any any (msg:"XOR with variable key"; \ http.request_body; xor:offset 1,extract 1 0; content:"infected"; sid:2;) +With a ``byte_extract`` or ``byte_math`` variable key, the keyword reads or +computes the key from traffic and ``xor:var`` consumes it. The variable must be +produced by a buffer that is inspected before the transformed buffer (a lower +app-layer progress), because a transform runs when its buffer is built. A +signature whose transform depends on a runtime variable cannot have its buffer +precomputed, so it is disqualified from prefilter/MPM and inspected in full. + +The optional ```` gives the key width in bytes (1-8). For a +``byte_extract`` variable in the default (non-``string``) mode it defaults to +the number of bytes the keyword reads and may be omitted. When ``byte_extract`` +uses ``string`` mode its read count is a number of characters rather than a +value width, and a ``byte_math`` result is a computed value with no inherent +width; in both cases ```` is required. The low ```` bytes of the +variable's value form the key, in big-endian order regardless of the keyword's +own endianness. + +This example extracts a one-byte key from ``http.uri`` and uses it to xor-decode +the request body: + +.. container:: example-rule + + alert http any any -> any any (msg:"xor variable key"; \ + http.uri; byte_extract:1,1,xkey; \ + http.request_body; xor:var xkey; content:"SECRET"; sid:1;) + +This example computes the key with ``byte_math`` and supplies an explicit width: + +.. container:: example-rule + + alert http any any -> any any (msg:"xor byte_math key"; \ + http.uri; byte_math:bytes 1, offset 1, oper -, rvalue 1, result xkey; \ + http.request_body; xor:var xkey 1; content:"SECRET"; sid:1;) + header_lowercase ---------------- diff --git a/rust/src/detect/transforms/varkey.rs b/rust/src/detect/transforms/varkey.rs index cab9b7c379..0118f0fe85 100644 --- a/rust/src/detect/transforms/varkey.rs +++ b/rust/src/detect/transforms/varkey.rs @@ -15,8 +15,15 @@ * 02110-1301, USA. */ -//! Shared helpers for transforms that read key bytes from a fixed -//! position within the inspection buffer at transform time. +//! Shared helpers for transforms whose key is not a literal in the rule. +//! +//! Two key sources are supported: +//! +//! * a fixed location within the inspection buffer, read at transform time +//! ([`VariableKeyLocation`] / [`variable_key_bytes`]); and +//! * a `byte_extract` or `byte_math` variable, resolved against the signature +//! at setup ([`parse_byte_var`] / [`resolve_byte_var`]) and read from the +//! thread context at transform time ([`byte_var_key_bytes`]). use nom8::{ character::complete::{digit1, multispace1}, @@ -24,6 +31,11 @@ use nom8::{ sequence::separated_pair, IResult, Parser, }; +use std::ffi::CString; +use suricata_sys::sys::{ + DetectEngineThreadCtx, SCDetectByteVarResolve, SCDetectEngineThreadCtxGetByteVar, + SCSignatureSetVarTransform, Signature, +}; /// Location in an inspection buffer from which key bytes are read at /// transform time. @@ -76,6 +88,127 @@ pub(super) fn parse_key_location(s: &str) -> Option { } } +/// Parsed `var []` spec, before the variable is resolved +/// against the signature. +#[derive(Debug, PartialEq, Eq)] +pub(super) struct ByteVarSpec { + /// The `byte_extract`/`byte_math` variable name. + pub(super) name: String, + /// Optional explicit key width in bytes; resolved against the variable's + /// natural width when omitted. + pub(super) width: Option, +} + +/// A key read from a `byte_extract`/`byte_math` variable at transform time. +/// `local_id` is the runtime slot; `nbytes` (1-8) selects how many low bytes +/// of the variable's value form the key, rendered big-endian. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ByteVarKey { + pub(super) local_id: u8, + pub(super) nbytes: u8, +} + +/// Parse ` []` for the `var` form (the `var` keyword has already +/// been stripped by the caller). Logs and returns `None` on malformed input. +pub(super) fn parse_byte_var(rest: &str) -> Option { + let mut parts = rest.split_whitespace(); + let name = match parts.next() { + Some(n) => n, + None => { + SCLogError!("transform 'var' requires a variable name"); + return None; + } + }; + // Optional explicit key width (number of bytes of the variable's value to + // use). Required for byte_math variables, which have no natural width; + // optional for byte_extract, which defaults to its read width. + let width = match parts.next() { + Some(w) => match w.parse::() { + Ok(n) => Some(n), + Err(_) => { + SCLogError!("transform 'var' width must be a number"); + return None; + } + }, + None => None, + }; + if parts.next().is_some() { + SCLogError!("transform 'var' takes a variable name and optional width"); + return None; + } + Some(ByteVarSpec { + name: name.to_string(), + width, + }) +} + +/// Resolve a parsed byte-variable spec against signature `s`: look up the +/// variable, settle the key width, validate it, and mark the signature so it +/// is kept out of prefilter (the key is only known at inspection time). +/// Returns the compiled key, or `None` on any error (already logged). +/// +/// # Safety +/// +/// `s` must be a valid signature pointer; intended to be called from a +/// transform's `Setup` callback. +pub(super) unsafe fn resolve_byte_var(s: *mut Signature, spec: &ByteVarSpec) -> Option { + let cname = CString::new(spec.name.as_str()).ok()?; + let mut local_id: u8 = 0; + let mut natural_nbytes: u8 = 0; + if !SCDetectByteVarResolve(s, cname.as_ptr(), &mut local_id, &mut natural_nbytes) { + SCLogError!( + "transform 'var': unknown byte_extract or byte_math variable '{}'", + spec.name + ); + return None; + } + // Use the explicit width if given, otherwise the variable's natural width. + // A byte_math result has no natural width (reported as 0), so an explicit + // width is required there. + let nbytes = match spec.width { + Some(w) => w, + None => { + if natural_nbytes == 0 { + SCLogError!( + "transform 'var': variable '{}' has no inherent width; specify the key width, e.g. 'var '", + spec.name + ); + return None; + } + natural_nbytes + } + }; + if nbytes == 0 || nbytes > 8 { + SCLogError!("transform 'var': key width must be 1-8 bytes"); + return None; + } + /* The key is only known at inspection time, so the buffer can't be + * precomputed: disqualify this signature from prefilter/MPM. */ + SCSignatureSetVarTransform(s); + Some(ByteVarKey { local_id, nbytes }) +} + +/// Read the byte-variable key's value from the thread context and render its +/// low `nbytes` big-endian into `out`, returning the written slice. `out` must +/// be at least `key.nbytes` (<= 8) bytes long. Returns `None` if `det` is null. +/// +/// # Safety +/// +/// `det` must be a valid thread-context pointer or null. +pub(super) unsafe fn byte_var_key_bytes<'a>( + det: *mut DetectEngineThreadCtx, key: &ByteVarKey, out: &'a mut [u8], +) -> Option<&'a [u8]> { + if det.is_null() { + return None; + } + // Render the low `nbytes` bytes of the value big-endian, regardless of the + // keyword's own endianness. + let value = SCDetectEngineThreadCtxGetByteVar(det, key.local_id); + let n = key.nbytes as usize; + out[..n].copy_from_slice(&value.to_be_bytes()[8 - n..]); + Some(&out[..n]) +} + #[cfg(test)] mod tests { use super::*; @@ -85,6 +218,57 @@ mod tests { assert_eq!(strip_keyword_prefix("var 1 0", "var"), Some("1 0")); } + #[test] + fn test_parse_byte_var_name_only() { + // width is resolved later from the variable + assert_eq!( + parse_byte_var("xkey"), + Some(ByteVarSpec { + name: "xkey".to_string(), + width: None + }) + ); + // extra whitespace is tolerated + assert_eq!( + parse_byte_var(" xkey "), + Some(ByteVarSpec { + name: "xkey".to_string(), + width: None + }) + ); + } + + #[test] + fn test_parse_byte_var_explicit_width() { + assert_eq!( + parse_byte_var("xkey 4"), + Some(ByteVarSpec { + name: "xkey".to_string(), + width: Some(4) + }) + ); + } + + #[test] + fn test_parse_byte_var_missing_name() { + assert!(parse_byte_var("").is_none()); + assert!(parse_byte_var(" ").is_none()); + } + + #[test] + fn test_parse_byte_var_bad_width() { + // width must be numeric + assert!(parse_byte_var("xkey wide").is_none()); + // width must fit in u8 + assert!(parse_byte_var("xkey 256").is_none()); + } + + #[test] + fn test_parse_byte_var_trailing_garbage() { + // more than a name and a width + assert!(parse_byte_var("xkey 1 extra").is_none()); + } + #[test] fn test_strip_keyword_prefix_offset() { assert_eq!( diff --git a/rust/src/detect/transforms/xor.rs b/rust/src/detect/transforms/xor.rs index 6c4a70fe56..d546b995f1 100644 --- a/rust/src/detect/transforms/xor.rs +++ b/rust/src/detect/transforms/xor.rs @@ -22,7 +22,8 @@ use suricata_sys::sys::{ }; use super::varkey::{ - parse_key_location, strip_keyword_prefix, variable_key_bytes, VariableKeyLocation, + byte_var_key_bytes, parse_byte_var, parse_key_location, resolve_byte_var, strip_keyword_prefix, + variable_key_bytes, ByteVarKey, ByteVarSpec, VariableKeyLocation, }; use std::ffi::CStr; use std::os::raw::{c_char, c_int, c_void}; @@ -36,6 +37,8 @@ enum XorKeySource { Static(Vec), /// Key read directly from the inspection buffer at a fixed location. Variable(VariableKeyLocation), + /// Key read from a byte_extract/byte_math variable at inspection time. + ByteVar(ByteVarKey), } #[derive(Debug)] @@ -47,17 +50,21 @@ struct DetectTransformXorData { /// Precomputed identity bytes returned by xor_id. Layout: /// Static: [0x00, key_bytes..., xor_offset_le4] /// Variable: [0x01, key_offset_lo, key_offset_hi, nbytes, xor_offset_le4] - /// The leading discriminant byte ensures static and variable identities - /// can never collide. Including xor_offset ensures rules with the same - /// key but different decode-start positions get independent buffers. + /// ByteVar: [0x02, local_id, nbytes, xor_offset_le4] + /// The leading discriminant byte ensures the three key kinds can never + /// collide. Including xor_offset ensures rules with the same key but + /// different decode-start positions get independent buffers. id_buf: Vec, } -/// Parsed key specifier — either decoded key bytes or an inline buffer location. +/// Parsed key specifier — decoded key bytes, an inline buffer location, or the +/// name of a byte_extract/byte_math variable. #[derive(Debug, PartialEq)] enum XorKeySpec { Hex(Vec), Var(VariableKeyLocation), + /// `var []`, resolved against the signature at setup time. + ByteVar(ByteVarSpec), } /// Intermediate parse result. @@ -76,7 +83,8 @@ fn try_parse_hex_key(s: &str) -> Option> { .filter(|k| !k.is_empty() && k.len() <= usize::from(u8::MAX)) } -/// Parse a key specifier — either `extract ` or a hex string. +/// Parse a key specifier — `extract `, `var []`, +/// or a hex string. fn parse_key_part(s: &str) -> Option { if let Some(rest) = strip_keyword_prefix(s, "extract") { if let Some(loc) = parse_key_location(rest) { @@ -85,6 +93,9 @@ fn parse_key_part(s: &str) -> Option { SCLogError!("XOR transform: 'extract' requires format 'extract '"); return None; } + if let Some(rest) = strip_keyword_prefix(s, "var") { + return parse_byte_var(rest).map(XorKeySpec::ByteVar); + } let s = strip_quotes(s); if s.is_empty() { SCLogError!("XOR transform: missing hex key"); @@ -102,6 +113,7 @@ fn parse_key_part(s: &str) -> Option { /// Parse the xor option string. Accepts: /// - `` /// - `extract ` +/// - `var []` /// - `offset ,` /// - `offset ,extract ` fn xor_parse_options(input: &str) -> Option { @@ -148,32 +160,29 @@ fn strip_quotes(s: &str) -> &str { .unwrap_or(s) } -fn xor_build_ctx(input: &str) -> Option { - let parsed = xor_parse_options(input)?; - let xor_offset = parsed.xor_offset.unwrap_or(0); - let key_source = match parsed.key_spec { - XorKeySpec::Hex(key) => XorKeySource::Static(key), - XorKeySpec::Var(loc) => XorKeySource::Variable(loc), - }; - let id_buf = match &key_source { +/// Build the transform context (including the buffer-dedup identity) from a +/// resolved key source and decode offset. +fn build_xor_data(key_source: XorKeySource, xor_offset: u32) -> DetectTransformXorData { + // Each arm builds the discriminant + key-specific prefix; the xor_offset + // suffix is invariant across kinds, so append it once after the match. + let mut id_buf = match &key_source { XorKeySource::Static(key) => { let mut buf = vec![0x00]; buf.extend_from_slice(key); - buf.extend_from_slice(&xor_offset.to_le_bytes()); buf } XorKeySource::Variable(loc) => { let [lo, hi] = loc.offset.to_le_bytes(); - let mut buf = vec![0x01, lo, hi, loc.nbytes]; - buf.extend_from_slice(&xor_offset.to_le_bytes()); - buf + vec![0x01, lo, hi, loc.nbytes] } + XorKeySource::ByteVar(key) => vec![0x02, key.local_id, key.nbytes], }; - Some(DetectTransformXorData { + id_buf.extend_from_slice(&xor_offset.to_le_bytes()); + DetectTransformXorData { key_source, xor_offset, id_buf, - }) + } } unsafe extern "C" fn xor_setup( @@ -183,10 +192,22 @@ unsafe extern "C" fn xor_setup( Ok(s) => s, Err(_) => return -1, }; - let ctx = match xor_build_ctx(input) { - Some(d) => Box::into_raw(Box::new(d)) as *mut c_void, + let parsed = match xor_parse_options(input) { + Some(p) => p, None => return -1, }; + let xor_offset = parsed.xor_offset.unwrap_or(0); + let key_source = match parsed.key_spec { + XorKeySpec::Hex(key) => XorKeySource::Static(key), + XorKeySpec::Var(loc) => XorKeySource::Variable(loc), + XorKeySpec::ByteVar(spec) => match resolve_byte_var(s, &spec) { + Some(key) => XorKeySource::ByteVar(key), + None => return -1, + }, + }; + + let ctx = Box::into_raw(Box::new(build_xor_data(key_source, xor_offset))) as *mut c_void; + let r = SCDetectSignatureAddTransform(s, G_TRANSFORM_XOR_ID, ctx); if r != 0 { xor_free(de, ctx); @@ -209,7 +230,7 @@ fn xor_transform_do(input: &[u8], output: &mut [u8], key: &[u8], xor_offset: usi } unsafe extern "C" fn xor_transform( - _det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *const c_void, + det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *const c_void, ) { let input = (*buffer).inspect; let input_len = (*buffer).inspect_len; @@ -220,6 +241,7 @@ unsafe extern "C" fn xor_transform( let output = SCInspectionBufferCheckAndExpand(buffer, input_len); if output.is_null() { + // allocation failure return; } let output = std::slice::from_raw_parts_mut(output, input_len as usize); @@ -246,6 +268,10 @@ unsafe extern "C" fn xor_transform( } None => return, }, + XorKeySource::ByteVar(key) => match byte_var_key_bytes(det, key, &mut var_key) { + Some(k) => k, + None => return, + }, }; xor_transform_do(input, output, key, xor_offset); @@ -347,6 +373,36 @@ mod tests { assert_eq!(r.xor_offset, Some(4)); } + #[test] + fn test_parse_byte_var() { + // The `var` form is recognized and produces a ByteVarSpec; detailed + // spec parsing is covered in the varkey module's tests. + let r = xor_parse_options("var xkey").unwrap(); + assert_eq!( + r.key_spec, + XorKeySpec::ByteVar(ByteVarSpec { + name: "xkey".to_string(), + width: None + }) + ); + assert_eq!(r.xor_offset, None); + // explicit width, with a decode offset preceding the variable key + let r = xor_parse_options("offset 2,var xkey 4").unwrap(); + assert_eq!( + r.key_spec, + XorKeySpec::ByteVar(ByteVarSpec { + name: "xkey".to_string(), + width: Some(4) + }) + ); + assert_eq!(r.xor_offset, Some(2)); + // malformed var specs are rejected (details tested in varkey) + assert!(xor_parse_options("var").is_none()); + assert!(xor_parse_options("var xkey wide").is_none()); + // no separating whitespace is not a var spec; falls through to hex + assert!(xor_parse_options("varxkey").is_none()); + } + #[test] fn test_parse_empty() { assert!(xor_parse_options("").is_none()); @@ -418,7 +474,7 @@ mod tests { assert_eq!(recovered, input); } - // Build an id_buf for a variable key the same way xor_build_ctx does. + // Build an id_buf for a variable (in-buffer) key the same way build_xor_data does. fn make_id_buf_var(key_offset: u16, nbytes: u8, xor_offset: u32) -> Vec { let [lo, hi] = key_offset.to_le_bytes(); let mut buf = vec![0x01, lo, hi, nbytes]; @@ -426,7 +482,7 @@ mod tests { buf } - // Build an id_buf for a static key the same way xor_build_ctx does. + // Build an id_buf for a static key the same way build_xor_data does. fn make_id_buf_static(key: &[u8], xor_offset: u32) -> Vec { let mut buf = vec![0x00]; buf.extend_from_slice(key); @@ -437,16 +493,15 @@ mod tests { #[test] fn test_xor_id_variable_key() { // key_offset=0, nbytes=1, xor_offset=0 → [0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00] - let id_buf = make_id_buf_var(0, 1, 0); - let ctx = Box::new(DetectTransformXorData { - key_source: XorKeySource::Variable(VariableKeyLocation { + let data = build_xor_data( + XorKeySource::Variable(VariableKeyLocation { offset: 0, nbytes: 1, }), - xor_offset: 0, - id_buf, - }); - let ctx_ptr: *const c_void = &*ctx as *const _ as *const c_void; + 0, + ); + assert_eq!(data.id_buf, make_id_buf_var(0, 1, 0)); + let ctx_ptr: *const c_void = &data as *const _ as *const c_void; let mut data_ptr: *const u8 = std::ptr::null(); let mut length: u32 = 0; unsafe { @@ -463,38 +518,53 @@ mod tests { #[test] fn test_xor_id_variable_key_large_offset() { // key_offset=300 (0x012c LE = [0x2c, 0x01]), nbytes=4, xor_offset=0 - let id_buf = make_id_buf_var(300, 4, 0); - let ctx = Box::new(DetectTransformXorData { - key_source: XorKeySource::Variable(VariableKeyLocation { + let data = build_xor_data( + XorKeySource::Variable(VariableKeyLocation { offset: 300, nbytes: 4, }), - xor_offset: 0, - id_buf, - }); - let ctx_ptr: *const c_void = &*ctx as *const _ as *const c_void; - let mut data_ptr: *const u8 = std::ptr::null(); - let mut length: u32 = 0; - unsafe { - xor_id(&mut data_ptr, &mut length, ctx_ptr as *mut c_void); - assert!(!data_ptr.is_null()); - assert_eq!(length, 8); - assert_eq!( - std::slice::from_raw_parts(data_ptr, 8), - &[0x01u8, 0x2c, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00] - ); - } + 0, + ); + assert_eq!( + data.id_buf, + &[0x01u8, 0x2c, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00] + ); } #[test] fn test_xor_id_variable_key_nonzero_xor_offset() { - // Verify that two rules with the same key location but different xor_offset + // Two rules with the same key location but different xor_offset must // produce distinct id_bufs and therefore get independent buffers. let id_a = make_id_buf_var(0, 1, 0); let id_b = make_id_buf_var(0, 1, 5); assert_ne!(id_a, id_b); } + #[test] + fn test_xor_id_byte_var() { + // byte_extract/byte_math key: discriminant 0x02, local_id, nbytes, xor_offset. + let data = build_xor_data( + XorKeySource::ByteVar(ByteVarKey { + local_id: 3, + nbytes: 4, + }), + 0, + ); + assert_eq!(data.id_buf, vec![0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00]); + // distinct from a static key whose bytes happen to match + let stat = build_xor_data(XorKeySource::Static(vec![0x03, 0x04]), 0); + assert_ne!(data.id_buf, stat.id_buf); + // distinct from an in-buffer variable key with matching numbers + let var = build_xor_data( + XorKeySource::Variable(VariableKeyLocation { + offset: 3, + nbytes: 4, + }), + 0, + ); + assert_ne!(data.id_buf, var.id_buf); + } + #[test] fn test_var_key_transform() { // Buffer: [key_byte, encoded[0], encoded[1], ...] @@ -521,14 +591,10 @@ mod tests { fn test_xor_id() { // Static key [1,2,3,4,5] with xor_offset=0: id = [0x00, key_bytes..., 0,0,0,0] let key = vec![1u8, 2, 3, 4, 5]; - let id_buf = make_id_buf_static(&key, 0); - let ctx = Box::new(DetectTransformXorData { - key_source: XorKeySource::Static(key), - xor_offset: 0, - id_buf, - }); + let data = build_xor_data(XorKeySource::Static(key.clone()), 0); + assert_eq!(data.id_buf, make_id_buf_static(&key, 0)); - let ctx_ptr: *const c_void = &*ctx as *const _ as *const c_void; + let ctx_ptr: *const c_void = &data as *const _ as *const c_void; let mut data_ptr: *const u8 = std::ptr::null(); let mut length: u32 = 0; diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs index 67097e5edb..cef746b21b 100644 --- a/rust/sys/src/sys.rs +++ b/rust/sys/src/sys.rs @@ -695,6 +695,21 @@ extern "C" { det_ctx: *mut DetectEngineThreadCtx, id: ::std::os::raw::c_int, ) -> *mut ::std::os::raw::c_void; } +extern "C" { + #[doc = " \\brief Flag the signature as carrying a transform whose key comes from a\n runtime variable; this disqualifies the signature from prefilter/MPM."] + pub fn SCSignatureSetVarTransform(s: *mut Signature); +} +extern "C" { + #[doc = " \\brief Resolve a byte variable (byte_extract or byte_math) by name for use\n as transform input.\n\n Returns the runtime slot (local_id) for the named variable. For a\n byte_extract variable, nbytes is the natural key width (the number of bytes\n the keyword reads); a byte_math result is a computed value with no inherent\n width, so nbytes is set to 0 and the caller must supply an explicit width.\n\n \\retval true variable found; local_id set, nbytes is the natural width or 0\n \\retval false no such byte variable"] + pub fn SCDetectByteVarResolve( + s: *const Signature, name: *const ::std::os::raw::c_char, local_id: *mut u8, + nbytes: *mut u8, + ) -> bool; +} +extern "C" { + #[doc = " \\brief Read a byte_* variable's runtime value from the thread context."] + pub fn SCDetectEngineThreadCtxGetByteVar(det_ctx: *const DetectEngineThreadCtx, id: u8) -> u64; +} #[repr(C)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct InspectionBuffer { diff --git a/src/detect-byte.c b/src/detect-byte.c index 3f8735b119..b435602229 100644 --- a/src/detect-byte.c +++ b/src/detect-byte.c @@ -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 @@ -26,6 +26,7 @@ #include "detect-byte.h" #include "detect-byte-extract.h" #include "detect-bytemath.h" +#include "detect-engine-buffer.h" /** * \brief Used to retrieve args from BM. @@ -54,3 +55,35 @@ bool DetectByteRetrieveSMVar( } return false; } + +bool SCDetectByteVarResolve( + const Signature *s, const char *name, uint8_t *local_id, uint8_t *nbytes) +{ + /* byte_extract carries a natural key width (the number of bytes it reads), + * so resolve it directly to get both the runtime slot and that width. */ + SigMatch *bed_sm = DetectByteExtractRetrieveSMVar(name, s->init_data->list, s); + if (bed_sm != NULL) { + const SCDetectByteExtractData *bed = (const SCDetectByteExtractData *)bed_sm->ctx; + *local_id = bed->local_id; + /* In string mode nbytes is the count of input characters parsed, not the + * byte width of the resulting value, so it is not a usable key width; + * report 0 and let the caller require an explicit width. */ + *nbytes = (bed->flags & DETECT_BYTE_EXTRACT_FLAG_STRING) ? 0 : bed->nbytes; + return true; + } + + /* a byte_math result is a computed value with no inherent width, so report + * 0 and let the caller require an explicit width. */ + SigMatch *bmd_sm = DetectByteMathRetrieveSMVar(name, s->init_data->list, s); + if (bmd_sm != NULL) { + *local_id = ((const DetectByteMathData *)bmd_sm->ctx)->local_id; + *nbytes = 0; + return true; + } + return false; +} + +uint64_t SCDetectEngineThreadCtxGetByteVar(const DetectEngineThreadCtx *det_ctx, uint8_t id) +{ + return det_ctx->byte_values[id]; +} diff --git a/src/detect-engine-buffer.c b/src/detect-engine-buffer.c index 9041315af1..828b83f83a 100644 --- a/src/detect-engine-buffer.c +++ b/src/detect-engine-buffer.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Open Information Security Foundation +/* Copyright (C) 2025-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 @@ -197,3 +197,8 @@ int SCDetectSignatureAddTransform(Signature *s, int transform, void *options) SCReturnInt(0); } + +void SCSignatureSetVarTransform(Signature *s) +{ + s->init_data->var_transform = true; +} diff --git a/src/detect-engine-buffer.h b/src/detect-engine-buffer.h index d98b1298d0..56d27dcdf7 100644 --- a/src/detect-engine-buffer.h +++ b/src/detect-engine-buffer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Open Information Security Foundation +/* Copyright (C) 2025-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 @@ -43,4 +43,26 @@ int SCDetectRegisterThreadCtxGlobalFuncs( const char *name, void *(*InitFunc)(void *), void *data, void (*FreeFunc)(void *)); void *SCDetectThreadCtxGetGlobalKeywordThreadCtx(DetectEngineThreadCtx *det_ctx, int id); +/** \brief Flag the signature as carrying a transform whose key comes from a + * runtime variable; this disqualifies the signature from prefilter/MPM. */ +void SCSignatureSetVarTransform(Signature *s); + +/** + * \brief Resolve a byte variable (byte_extract or byte_math) by name for use + * as transform input. + * + * Returns the runtime slot (local_id) for the named variable. For a + * byte_extract variable, nbytes is the natural key width (the number of bytes + * the keyword reads); a byte_math result is a computed value with no inherent + * width, so nbytes is set to 0 and the caller must supply an explicit width. + * + * \retval true variable found; local_id set, nbytes is the natural width or 0 + * \retval false no such byte variable + */ +bool SCDetectByteVarResolve( + const Signature *s, const char *name, uint8_t *local_id, uint8_t *nbytes); + +/** \brief Read a byte_* variable's runtime value from the thread context. */ +uint64_t SCDetectEngineThreadCtxGetByteVar(const DetectEngineThreadCtx *det_ctx, uint8_t id); + #endif /* SURICATA_DETECT_ENGINE_BUFFER_H */ diff --git a/src/detect-engine-mpm.c b/src/detect-engine-mpm.c index 694fc26112..4348eb3c78 100644 --- a/src/detect-engine-mpm.c +++ b/src/detect-engine-mpm.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2021 Open Information Security Foundation +/* Copyright (C) 2007-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 @@ -1186,6 +1186,15 @@ void RetrieveFPForSig(const DetectEngineCtx *de_ctx, Signature *s) if (s->init_data->mpm_sm != NULL) return; + /* A transform that derives its key from a runtime byte_extract or byte_math + * variable cannot have its buffer precomputed at load time, so the content + * of such a buffer is unusable as a fast_pattern. Disqualify the whole + * signature from prefilter; it falls back to full inspection. */ + if (s->init_data->var_transform) { + SCLogDebug("sid %u uses a variable-key transform; skipping fast_pattern", s->id); + return; + } + const int nlists = s->init_data->max_content_list_id + 1; DEBUG_VALIDATE_BUG_ON(nlists > UINT16_MAX); int pos_sm_list[nlists]; diff --git a/src/detect.h b/src/detect.h index 605b54693a..1e4c839124 100644 --- a/src/detect.h +++ b/src/detect.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2025 Open Information Security Foundation +/* Copyright (C) 2007-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 @@ -686,6 +686,12 @@ typedef struct SignatureInitData_ { /* Signature is a "firewall" rule. */ bool firewall_rule; + + /* True if a transform on one of this signature's buffers reads its key + * material from a runtime byte_extract or byte_math variable. Such a buffer + * cannot be precomputed, so the signature is disqualified from + * prefilter/MPM. */ + bool var_transform; } SignatureInitData; /** \brief Signature container */