From a0472349a4eac17c5480e67738f416b3fbc5cf2c Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 22 Jan 2026 09:33:03 +0100 Subject: [PATCH 1/3] detect/transforms: add gunzip transform Ticket: 7846 --- doc/userguide/rules/transforms.rst | 19 +++ rust/src/detect/transforms/decompress.rs | 202 +++++++++++++++++++++++ rust/src/detect/transforms/mod.rs | 1 + src/detect-engine-register.c | 1 + 4 files changed, 223 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..9ecf954d2f4f 100644 --- a/doc/userguide/rules/transforms.rst +++ b/doc/userguide/rules/transforms.rst @@ -394,3 +394,22 @@ 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. +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..c09488745bbb --- /dev/null +++ b/rust/src/detect/transforms/decompress.rs @@ -0,0 +1,202 @@ +/* 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 output = std::slice::from_raw_parts_mut(output, ctx.max_size as usize); + + if let Some(nb) = gunzip_transform_do(input, output) { + 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 59c3ce42a3f2..7f491fa928d5 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -775,6 +775,7 @@ void SigTableSetup(void) DetectTransformFromBase64DecodeRegister(); SCDetectTransformDomainRegister(); DetectTransformLuaxformRegister(); + DetectTransformGunzipRegister(); DetectFileHandlerRegister(); From e7af724334d9b0921198fe0db95db1f796701dc4 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 22 Jan 2026 14:45:01 +0100 Subject: [PATCH 2/3] detect/transforms: add zlib_deflate transform Ticket: 7846 --- doc/userguide/rules/transforms.rst | 19 +++++++ rust/src/detect/transforms/decompress.rs | 72 +++++++++++++++++++++++- src/detect-engine-register.c | 1 + 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/doc/userguide/rules/transforms.rst b/doc/userguide/rules/transforms.rst index 9ecf954d2f4f..fe3f7d431206 100644 --- a/doc/userguide/rules/transforms.rst +++ b/doc/userguide/rules/transforms.rst @@ -413,3 +413,22 @@ 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. +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 c09488745bbb..e4868f655acd 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::{ZlibDecoder, GzDecoder}; 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 { @@ -160,6 +161,54 @@ 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 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 output = std::slice::from_raw_parts_mut(output, ctx.max_size as usize); + + if let Some(nb) = zlib_deflate_transform_do(input, output) { + SCInspectionBufferTruncate(buffer, nb); + } else { + // decompression failure + SCInspectionBufferTruncate(buffer, 0); + } +} + #[no_mangle] pub unsafe extern "C" fn DetectTransformGunzipRegister() { let kw = SCTransformTableElmt { @@ -181,6 +230,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 7f491fa928d5..b37f72bf5164 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -776,6 +776,7 @@ void SigTableSetup(void) SCDetectTransformDomainRegister(); DetectTransformLuaxformRegister(); DetectTransformGunzipRegister(); + DetectTransformZlibDeflateRegister(); DetectFileHandlerRegister(); From 99863c4971baa35bf0d0cd61492dc9e0e252edf6 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 22 Jan 2026 17:56:49 +0100 Subject: [PATCH 3/3] dummy ci test --- rust/src/detect/transforms/decompress.rs | 28 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/rust/src/detect/transforms/decompress.rs b/rust/src/detect/transforms/decompress.rs index e4868f655acd..9be0194399c2 100644 --- a/rust/src/detect/transforms/decompress.rs +++ b/rust/src/detect/transforms/decompress.rs @@ -116,10 +116,20 @@ unsafe extern "C" fn gunzip_setup( 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, - }; + let mut offset = 0u32; + loop { + match gz.read(&mut output[offset as usize..]) { + Ok(0) => { + return Some(offset); + } + Ok(n) => { + offset += n as u32; + } + _ => { + return None; + } + } + } } unsafe extern "C" fn gunzip_transform( @@ -178,8 +188,14 @@ unsafe extern "C" fn zlib_deflate_setup( 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, + Ok(n) => { + println!("zlib ok {n} for {}", output.len()); + Some(n as u32) + } + Err(e) => { + println!("zlib err {:?}", e); + None + } }; }