From 04a7075b9cb5e7dc1b102a83ce0860db1aa55d67 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 22 Jan 2026 09:33:03 +0100 Subject: [PATCH 1/2] detect/transforms: add gunzip transform Ticket: 7846 --- doc/userguide/rules/transforms.rst | 21 +++ rust/src/detect/transforms/decompress.rs | 213 +++++++++++++++++++++++ rust/src/detect/transforms/mod.rs | 1 + src/detect-engine-register.c | 1 + 4 files changed, 236 insertions(+) create mode 100644 rust/src/detect/transforms/decompress.rs diff --git a/doc/userguide/rules/transforms.rst b/doc/userguide/rules/transforms.rst index eb5117ba7dee..62e1b9214219 100644 --- a/doc/userguide/rules/transforms.rst +++ b/doc/userguide/rules/transforms.rst @@ -394,3 +394,24 @@ the buffer. local sub = string.sub(input, offset + 1, offset + bytes) return string.upper(sub), bytes end + +gunzip +------ + +Takes the buffer, applies gunzip decompression. + +This transform takes an optional argument which is a comma-separated list of key-values. +The only key being interperted is ``max-size``, which is the max output size. +Default for max-size is 1024. +If the decompressed data were to be larger than max-size, +the transform will decompress data up to max-size. +Value 0 is forbidden for max-size (there is no unlimited value). + +This example alerts if ``http.uri`` contains base64-encoded gzipped value +Example:: + + alert http any any -> any any (msg:"from_base64 + gunzip"; + http.uri; content:"/gzb64?value="; fast_pattern; + from_base64: offset 13 ; + gunzip; content:"This is compressed then base64-encoded"; startswith; endswith; + sid:2; rev:1;) diff --git a/rust/src/detect/transforms/decompress.rs b/rust/src/detect/transforms/decompress.rs new file mode 100644 index 000000000000..dc9afed084f0 --- /dev/null +++ b/rust/src/detect/transforms/decompress.rs @@ -0,0 +1,213 @@ +/* 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. + */ + +use crate::detect::uint::detect_parse_uint_with_unit; +use crate::detect::SIGMATCH_OPTIONAL_OPT; +use flate2::bufread::GzDecoder; +use suricata_sys::sys::{ + DetectEngineCtx, DetectEngineThreadCtx, InspectionBuffer, SCDetectHelperTransformRegister, + SCDetectSignatureAddTransform, SCInspectionBufferCheckAndExpand, SCInspectionBufferTruncate, + SCTransformTableElmt, Signature, +}; + +use std::ffi::CStr; +use std::io::Read; +use std::os::raw::{c_int, c_void}; + +static mut G_TRANSFORM_GUNZIP_ID: c_int = 0; + +#[derive(Debug, PartialEq)] +struct DetectTransformDecompressData { + max_size: u32, +} + +const DEFAULT_MAX_SIZE: u32 = 1024; +// 16 MiB +const ABSOLUTE_MAX_SIZE: u32 = 16*1024*1024; + +fn decompress_parse_do(s: &str) -> Option { + let mut max_size_parsed = None; + for p in s.split(',') { + let kv: Vec<&str> = p.split('=').collect(); + if kv.len() != 2 { + SCLogError!("Bad key value for decompress transform {}", p); + return None; + } + match kv[0] { + "max-size" => { + if max_size_parsed.is_some() { + SCLogError!("Multiple max-size values for decompress transform"); + return None; + } + if let Ok((_, val)) = detect_parse_uint_with_unit::(kv[1]) { + if val == 0 { + SCLogError!("max-size 0 for decompress transform would always produce an empty buffer"); + return None; + } else if val > ABSOLUTE_MAX_SIZE { + SCLogError!("max-size is too big > {}", ABSOLUTE_MAX_SIZE); + return None; + } + max_size_parsed = Some(val); + } else { + SCLogError!("Invalid max-size value for decompress transform {}", kv[1]); + return None; + } + } + _ => { + SCLogError!("Unknown key for decompress transform {}", kv[0]); + return None; + } + } + } + let max_size = if let Some(val) = max_size_parsed { + val + } else { + DEFAULT_MAX_SIZE + }; + return Some(DetectTransformDecompressData { max_size }); +} + +unsafe fn decompress_parse(raw: *const std::os::raw::c_char) -> *mut c_void { + if raw.is_null() { + let ctx = DetectTransformDecompressData { + max_size: DEFAULT_MAX_SIZE, + }; + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + let raw: &CStr = CStr::from_ptr(raw); //unsafe + if let Ok(s) = raw.to_str() { + if let Some(ctx) = decompress_parse_do(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +unsafe extern "C" fn gunzip_setup( + de: *mut DetectEngineCtx, s: *mut Signature, opt_str: *const std::os::raw::c_char, +) -> c_int { + let ctx = decompress_parse(opt_str); + if ctx.is_null() { + return -1; + } + let r = SCDetectSignatureAddTransform(s, G_TRANSFORM_GUNZIP_ID, ctx); + if r != 0 { + decompress_free(de, ctx); + } + return r; +} + +fn gunzip_transform_do(input: &[u8], output: &mut [u8]) -> Option { + let mut gz = GzDecoder::new(input); + return match gz.read(output) { + Ok(n) => Some(n as u32), + _ => None, + }; +} + +unsafe extern "C" fn gunzip_transform( + _det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *mut c_void, +) { + let input = (*buffer).inspect; + let input_len = (*buffer).inspect_len; + if input.is_null() || input_len == 0 { + return; + } + let input = build_slice!(input, input_len as usize); + let ctx = cast_pointer!(ctx, DetectTransformDecompressData); + + let output = SCInspectionBufferCheckAndExpand(buffer, ctx.max_size); + if output.is_null() { + // allocation failure + return; + } + let buf = std::slice::from_raw_parts_mut(output, ctx.max_size as usize); + let mut tmp = Vec::new(); + let input = if std::ptr::eq(output, input.as_ptr()) { + // need a temporary buffer as we cannot do the transfom in place + // rather copy the input which should be smaller than the decompressed output + // (Transform is tried in place when there are multiple chaines transforms) + tmp.extend_from_slice(input); + &tmp + } else { + input + }; + + // this succeeds if decompressed data > max_size, but we get nb = max_size + if let Some(nb) = gunzip_transform_do(input, buf) { + SCInspectionBufferTruncate(buffer, nb); + } else { + // decompression failure + SCInspectionBufferTruncate(buffer, 0); + } +} + +unsafe extern "C" fn decompress_free(_de: *mut DetectEngineCtx, ctx: *mut c_void) { + std::mem::drop(Box::from_raw(ctx as *mut DetectTransformDecompressData)); +} + +unsafe extern "C" fn decompress_id(data: *mut *const u8, length: *mut u32, ctx: *mut c_void) { + if data.is_null() || length.is_null() || ctx.is_null() { + return; + } + + *data = ctx as *const u8; + *length = std::mem::size_of::() as u32; // 4 +} + +#[no_mangle] +pub unsafe extern "C" fn DetectTransformGunzipRegister() { + let kw = SCTransformTableElmt { + name: b"gunzip\0".as_ptr() as *const libc::c_char, + desc: b"modify buffer via gunzip decompression\0".as_ptr() as *const libc::c_char, + url: b"/rules/transforms.html#gunzip\0".as_ptr() as *const libc::c_char, + Setup: Some(gunzip_setup), + flags: SIGMATCH_OPTIONAL_OPT, + Transform: Some(gunzip_transform), + Free: Some(decompress_free), + TransformValidate: None, + TransformId: Some(decompress_id), + }; + unsafe { + G_TRANSFORM_GUNZIP_ID = SCDetectHelperTransformRegister(&kw); + if G_TRANSFORM_GUNZIP_ID < 0 { + SCLogWarning!("Failed registering transform gunzip"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decompress_parse() { + assert!(decompress_parse_do("keywithoutvalue").is_none()); + assert!(decompress_parse_do("unknown=1").is_none()); + assert!(decompress_parse_do("max-size=0").is_none()); + assert!(decompress_parse_do("max-size=1,max-size=1").is_none()); + assert!(decompress_parse_do("max-size=toto").is_none()); + assert_eq!( + decompress_parse_do("max-size=1MiB"), + Some(DetectTransformDecompressData { + max_size: 1024 * 1024 + }) + ); + } +} diff --git a/rust/src/detect/transforms/mod.rs b/rust/src/detect/transforms/mod.rs index 939fbfa1954d..677b1ba2ef56 100644 --- a/rust/src/detect/transforms/mod.rs +++ b/rust/src/detect/transforms/mod.rs @@ -20,6 +20,7 @@ pub mod base64; pub mod casechange; pub mod compress_whitespace; +pub mod decompress; pub mod domain; pub mod dotprefix; pub mod hash; diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 37d7b7c0f58b..7b6665980429 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -749,6 +749,7 @@ void SigTableSetup(void) DetectTransformFromBase64DecodeRegister(); SCDetectTransformDomainRegister(); DetectTransformLuaxformRegister(); + DetectTransformGunzipRegister(); DetectFileHandlerRegister(); From 427a79d64fcb95162125408e286887a281e480d9 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 22 Jan 2026 14:45:01 +0100 Subject: [PATCH 2/2] detect/transforms: add zlib_deflate transform Ticket: 7846 --- doc/userguide/rules/transforms.rst | 21 +++++++ rust/src/detect/transforms/decompress.rs | 72 +++++++++++++++++++++--- src/detect-engine-register.c | 1 + 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/doc/userguide/rules/transforms.rst b/doc/userguide/rules/transforms.rst index 62e1b9214219..4bb7915170a3 100644 --- a/doc/userguide/rules/transforms.rst +++ b/doc/userguide/rules/transforms.rst @@ -415,3 +415,24 @@ Example:: from_base64: offset 13 ; gunzip; content:"This is compressed then base64-encoded"; startswith; endswith; sid:2; rev:1;) + +zlib_deflate +------------ + +Takes the buffer, applies zlib decompression. + +This transform takes an optional argument which is a comma-separated list of key-values. +The only key being interperted is ``max-size``, which is the max output size. +Default for max-size is 1024. +If the decompressed data were to be larger than max-size, +the transform will decompress data up to max-size. +Value 0 is forbidden for max-size (there is no unlimited value). + +This example alerts if ``http.uri`` contains base64-encoded zlib-compressed value +Example:: + + alert http any any -> any any (msg:"from_base64 + gunzip"; + http.uri; content:"/zb64?value="; fast_pattern; + from_base64: offset 12 ; + zlib_deflate; content:"This is compressed then base64-encoded"; startswith; endswith; + sid:2; rev:1;) diff --git a/rust/src/detect/transforms/decompress.rs b/rust/src/detect/transforms/decompress.rs index dc9afed084f0..110476be36e3 100644 --- a/rust/src/detect/transforms/decompress.rs +++ b/rust/src/detect/transforms/decompress.rs @@ -17,7 +17,7 @@ use crate::detect::uint::detect_parse_uint_with_unit; use crate::detect::SIGMATCH_OPTIONAL_OPT; -use flate2::bufread::GzDecoder; +use flate2::bufread::{GzDecoder, ZlibDecoder}; use suricata_sys::sys::{ DetectEngineCtx, DetectEngineThreadCtx, InspectionBuffer, SCDetectHelperTransformRegister, SCDetectSignatureAddTransform, SCInspectionBufferCheckAndExpand, SCInspectionBufferTruncate, @@ -29,6 +29,7 @@ use std::io::Read; use std::os::raw::{c_int, c_void}; static mut G_TRANSFORM_GUNZIP_ID: c_int = 0; +static mut G_TRANSFORM_ZLIB_DEFLATE_ID: c_int = 0; #[derive(Debug, PartialEq)] struct DetectTransformDecompressData { @@ -37,7 +38,7 @@ struct DetectTransformDecompressData { const DEFAULT_MAX_SIZE: u32 = 1024; // 16 MiB -const ABSOLUTE_MAX_SIZE: u32 = 16*1024*1024; +const ABSOLUTE_MAX_SIZE: u32 = 16 * 1024 * 1024; fn decompress_parse_do(s: &str) -> Option { let mut max_size_parsed = None; @@ -121,8 +122,10 @@ fn gunzip_transform_do(input: &[u8], output: &mut [u8]) -> Option { }; } -unsafe extern "C" fn gunzip_transform( - _det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *mut c_void, +pub type DecompressFn = fn(input: &[u8], output: &mut [u8]) -> Option; + +unsafe fn decompress_transform( + buffer: *mut InspectionBuffer, ctx: &DetectTransformDecompressData, decompress_fn: DecompressFn, ) { let input = (*buffer).inspect; let input_len = (*buffer).inspect_len; @@ -130,8 +133,6 @@ unsafe extern "C" fn gunzip_transform( return; } let input = build_slice!(input, input_len as usize); - let ctx = cast_pointer!(ctx, DetectTransformDecompressData); - let output = SCInspectionBufferCheckAndExpand(buffer, ctx.max_size); if output.is_null() { // allocation failure @@ -150,7 +151,7 @@ unsafe extern "C" fn gunzip_transform( }; // this succeeds if decompressed data > max_size, but we get nb = max_size - if let Some(nb) = gunzip_transform_do(input, buf) { + if let Some(nb) = decompress_fn(input, buf) { SCInspectionBufferTruncate(buffer, nb); } else { // decompression failure @@ -158,6 +159,13 @@ unsafe extern "C" fn gunzip_transform( } } +unsafe extern "C" fn gunzip_transform( + _det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *mut c_void, +) { + let ctx = cast_pointer!(ctx, DetectTransformDecompressData); + decompress_transform(buffer, ctx, gunzip_transform_do); +} + unsafe extern "C" fn decompress_free(_de: *mut DetectEngineCtx, ctx: *mut c_void) { std::mem::drop(Box::from_raw(ctx as *mut DetectTransformDecompressData)); } @@ -171,6 +179,35 @@ unsafe extern "C" fn decompress_id(data: *mut *const u8, length: *mut u32, ctx: *length = std::mem::size_of::() as u32; // 4 } +unsafe extern "C" fn zlib_deflate_setup( + de: *mut DetectEngineCtx, s: *mut Signature, opt_str: *const std::os::raw::c_char, +) -> c_int { + let ctx = decompress_parse(opt_str); + if ctx.is_null() { + return -1; + } + let r = SCDetectSignatureAddTransform(s, G_TRANSFORM_ZLIB_DEFLATE_ID, ctx); + if r != 0 { + decompress_free(de, ctx); + } + return r; +} + +fn zlib_deflate_transform_do(input: &[u8], output: &mut [u8]) -> Option { + let mut gz = ZlibDecoder::new(input); + return match gz.read(output) { + Ok(n) => Some(n as u32), + _ => None, + }; +} + +unsafe extern "C" fn zlib_deflate_transform( + _det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *mut c_void, +) { + let ctx = cast_pointer!(ctx, DetectTransformDecompressData); + decompress_transform(buffer, ctx, zlib_deflate_transform_do); +} + #[no_mangle] pub unsafe extern "C" fn DetectTransformGunzipRegister() { let kw = SCTransformTableElmt { @@ -192,6 +229,27 @@ pub unsafe extern "C" fn DetectTransformGunzipRegister() { } } +#[no_mangle] +pub unsafe extern "C" fn DetectTransformZlibDeflateRegister() { + let kw = SCTransformTableElmt { + name: b"zlib_deflate\0".as_ptr() as *const libc::c_char, + desc: b"modify buffer via zlib decompression\0".as_ptr() as *const libc::c_char, + url: b"/rules/transforms.html#zlib_deflate\0".as_ptr() as *const libc::c_char, + Setup: Some(zlib_deflate_setup), + flags: SIGMATCH_OPTIONAL_OPT, + Transform: Some(zlib_deflate_transform), + Free: Some(decompress_free), + TransformValidate: None, + TransformId: Some(decompress_id), + }; + unsafe { + G_TRANSFORM_ZLIB_DEFLATE_ID = SCDetectHelperTransformRegister(&kw); + if G_TRANSFORM_ZLIB_DEFLATE_ID < 0 { + SCLogWarning!("Failed registering transform zlib_deflate"); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 7b6665980429..477247642b04 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -750,6 +750,7 @@ void SigTableSetup(void) SCDetectTransformDomainRegister(); DetectTransformLuaxformRegister(); DetectTransformGunzipRegister(); + DetectTransformZlibDeflateRegister(); DetectFileHandlerRegister();