diff --git a/doc/userguide/rules/transforms.rst b/doc/userguide/rules/transforms.rst index eb5117ba7dee..4bb7915170a3 100644 --- a/doc/userguide/rules/transforms.rst +++ b/doc/userguide/rules/transforms.rst @@ -394,3 +394,45 @@ 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;) + +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 new file mode 100644 index 000000000000..110476be36e3 --- /dev/null +++ b/rust/src/detect/transforms/decompress.rs @@ -0,0 +1,271 @@ +/* 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, ZlibDecoder}; +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; +static mut G_TRANSFORM_ZLIB_DEFLATE_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, + }; +} + +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; + if input.is_null() || input_len == 0 { + return; + } + let input = build_slice!(input, input_len as usize); + 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) = decompress_fn(input, buf) { + SCInspectionBufferTruncate(buffer, nb); + } else { + // decompression failure + SCInspectionBufferTruncate(buffer, 0); + } +} + +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)); +} + +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 +} + +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 { + 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"); + } + } +} + +#[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::*; + + #[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 81da9fa1ee0f..da576623e408 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -754,6 +754,8 @@ void SigTableSetup(void) DetectTransformFromBase64DecodeRegister(); SCDetectTransformDomainRegister(); DetectTransformLuaxformRegister(); + DetectTransformGunzipRegister(); + DetectTransformZlibDeflateRegister(); DetectFileHandlerRegister(); diff --git a/src/stream-tcp.c b/src/stream-tcp.c index 509008b820df..c535b89e1f87 100644 --- a/src/stream-tcp.c +++ b/src/stream-tcp.c @@ -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 @@ -5724,6 +5724,7 @@ int StreamTcpPacket (ThreadVars *tv, Packet *p, StreamTcpThread *stt, if (StreamTcpPacketStateNone(tv, p, stt, ssn) == -1) { goto error; } + ssn = (TcpSession *)p->flow->protoctx; if (ssn != NULL) SCLogDebug("ssn->alproto %"PRIu16"", p->flow->alproto); @@ -5797,10 +5798,6 @@ int StreamTcpPacket (ThreadVars *tv, Packet *p, StreamTcpThread *stt, skip: StreamTcpPacketCheckPostRst(ssn, p); - - if (ssn->state >= TCP_ESTABLISHED) { - p->flags |= PKT_STREAM_EST; - } } if (ssn != NULL) { @@ -5808,6 +5805,12 @@ int StreamTcpPacket (ThreadVars *tv, Packet *p, StreamTcpThread *stt, if (p->flags & PKT_STREAM_MODIFIED) { ReCalculateChecksum(p); } + + /* if ssn was set in this run, reflect TCP state on the packet */ + if (ssn->state >= TCP_ESTABLISHED) { + p->flags |= PKT_STREAM_EST; + } + /* check for conditions that may make us not want to log this packet */ /* streams that hit depth */