detect/transforms: add gunzip transform

Ticket: 7846
pull/14827/head
Philippe Antoine 6 months ago committed by Victor Julien
parent 751ef1fe9b
commit dbea660729

@ -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;)

@ -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<DetectTransformDecompressData> {
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::<u32>(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<u32> {
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::<DetectTransformDecompressData>() 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
})
);
}
}

@ -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;

@ -749,6 +749,7 @@ void SigTableSetup(void)
DetectTransformFromBase64DecodeRegister();
SCDetectTransformDomainRegister();
DetectTransformLuaxformRegister();
DetectTransformGunzipRegister();
DetectFileHandlerRegister();

Loading…
Cancel
Save