From 7bc883ef939ed4ec1283afb62aaea0a5bad9abce Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Fri, 19 Jun 2026 13:08:44 -0400 Subject: [PATCH 1/3] detect/transforms: disqualify variable-key transforms from prefilter A transform that derives its key material from a runtime byte_extract or byte_math variable cannot have its buffer precomputed at load time, so the content of that buffer is unusable as a fast_pattern. Add the engine plumbing to recognise such transforms and keep them out of prefilter/MPM. SignatureInitData gains a var_transform flag. RetrieveFPForSig bails out for any signature that sets it, so the signature falls back to full inspection rather than contributing an MPM pattern that can't be built ahead of time. New helpers support a transform consuming a variable: - SCSignatureSetVarTransform() raises the disqualification flag. - SCDetectByteVarResolve() resolves a byte_extract or byte_math variable by name to its runtime slot, and for a byte_extract reports its natural key width (a byte_math result has none). - SCDetectEngineThreadCtxGetByteVar() reads a byte_* variable's runtime value from the thread context. The helpers are declared in detect-engine-buffer.h so the bindgen run picks them up; the Rust bindings are regenerated to match. Issue: 7847 --- rust/sys/src/sys.rs | 15 +++++++++++++++ src/detect-byte.c | 35 ++++++++++++++++++++++++++++++++++- src/detect-engine-buffer.c | 7 ++++++- src/detect-engine-buffer.h | 24 +++++++++++++++++++++++- src/detect-engine-mpm.c | 11 ++++++++++- src/detect.h | 8 +++++++- 6 files changed, 95 insertions(+), 5 deletions(-) diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs index b13890f6b9..8eda300d62 100644 --- a/rust/sys/src/sys.rs +++ b/rust/sys/src/sys.rs @@ -689,6 +689,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 90291d03ef..5ee712ced4 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 c712680236..1e83766a32 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 @@ -676,6 +676,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 */ From 09a525659c1b081df3133266daf7c845c47cccfa Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Fri, 19 Jun 2026 13:10:23 -0400 Subject: [PATCH 2/3] detect/xor: support a variable key from a byte_extract or byte_math variable Extend the xor transform to take its key either as a literal hex string (xor:"") or as a byte_extract or byte_math variable (xor:var []) resolved at inspection time. For a variable key, setup resolves the variable to its runtime slot and marks the signature so it stays out of prefilter. At inspection the transform reads the value from the thread context and renders the low bytes of that value big-endian, regardless of the keyword's own endianness. The optional sets the key width. For a byte_extract variable read in the default (non-string) mode it defaults to the read width and may be omitted. A byte_math result, or a string-mode byte_extract, has no inherent value width, so the width is required there. The buffer-dedup id carries a leading discriminant so static and variable keys never share an inspection buffer. The key option is now optionally quoted (SIGMATCH_QUOTES_OPTIONAL) so the unquoted 'var ' form is accepted alongside the quoted hex key. Issue: 7847 --- rust/src/detect/transforms/varkey.rs | 188 ++++++++++++++++++++++++++- rust/src/detect/transforms/xor.rs | 182 +++++++++++++++++--------- 2 files changed, 310 insertions(+), 60 deletions(-) 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; From 5d61056cf7128ed39e981f2587a0fb70e81119fc Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Fri, 19 Jun 2026 13:10:32 -0400 Subject: [PATCH 3/3] doc: document xor transform variable key syntax Document the new `xor:var ` form: the key is read from a byte_extract or byte_math variable at inspection time. Note the ordering requirement (the variable's buffer must be inspected before the transformed buffer) and that such a signature is disqualified from prefilter/MPM. Issue: 7847 --- doc/userguide/rules/transforms.rst | 54 +++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/doc/userguide/rules/transforms.rst b/doc/userguide/rules/transforms.rst index d199bbd999..ca0026dd8c 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 ----------------