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/rust/src/krb/krb5.rs b/rust/src/krb/krb5.rs index 5e51570f3da6..8e53724304ff 100644 --- a/rust/src/krb/krb5.rs +++ b/rust/src/krb/krb5.rs @@ -522,8 +522,8 @@ unsafe extern "C" fn krb5_parse_request_tcp( if state.parse(cur_i, flow, Direction::ToServer) < 0 { return AppLayerResult::err(); } - state.record_ts = 0; cur_i = &cur_i[state.record_ts..]; + state.record_ts = 0; } else { // more fragments required state.defrag_buf_ts.extend_from_slice(cur_i); @@ -580,8 +580,8 @@ unsafe extern "C" fn krb5_parse_response_tcp( if state.parse(cur_i, flow, Direction::ToClient) < 0 { return AppLayerResult::err(); } - state.record_tc = 0; cur_i = &cur_i[state.record_tc..]; + state.record_tc = 0; } else { // more fragments required state.defrag_buf_tc.extend_from_slice(cur_i); diff --git a/src/datasets-context-json.c b/src/datasets-context-json.c index 3d1ac1fc07ab..36675ff74c5c 100644 --- a/src/datasets-context-json.c +++ b/src/datasets-context-json.c @@ -895,6 +895,12 @@ int DatajsonAddSerialized(Dataset *set, const char *value, const char *json) int ret = -1; switch (set->type) { case DATASET_TYPE_STRING: { + if (strlen(value) > UINT16_MAX) { + // size check before stack allocation + // should never happen as unix socket callers limits it to 4k + SCFree(jvalue.value); + return -1; + } uint32_t decoded_size = SCBase64DecodeBufferSize((uint32_t)strlen(value)); uint8_t decoded[decoded_size]; uint32_t num_decoded = SCBase64Decode( diff --git a/src/detect-engine-mpm.c b/src/detect-engine-mpm.c index c1af73c2fdd5..2c388e62c779 100644 --- a/src/detect-engine-mpm.c +++ b/src/detect-engine-mpm.c @@ -1149,6 +1149,7 @@ void RetrieveFPForSig(const DetectEngineCtx *de_ctx, Signature *s) return; const int nlists = s->init_data->max_content_list_id + 1; + DEBUG_VALIDATE_BUG_ON(nlists > UINT16_MAX); int pos_sm_list[nlists]; int neg_sm_list[nlists]; memset(pos_sm_list, 0, nlists * sizeof(int)); @@ -1733,11 +1734,12 @@ MpmStore *MpmStorePrepareBuffer(DetectEngineCtx *de_ctx, SigGroupHead *sgh, uint32_t cnt = 0; int direction = 0; uint32_t max_sid = DetectEngineGetMaxSigId(de_ctx) / 8 + 1; - uint8_t sids_array[max_sid]; - memset(sids_array, 0x00, max_sid); int sgh_mpm_context = 0; int sm_list = DETECT_SM_LIST_PMATCH; - + uint8_t *sids_array = SCCalloc(1, max_sid); + if (sids_array == NULL) { + return NULL; + } switch (buf) { case MPMB_TCP_PKT_TS: case MPMB_TCP_PKT_TC: @@ -1826,8 +1828,10 @@ MpmStore *MpmStorePrepareBuffer(DetectEngineCtx *de_ctx, SigGroupHead *sgh, } } - if (cnt == 0) + if (cnt == 0) { + SCFree(sids_array); return NULL; + } MpmStore lookup = { sids_array, max_sid, direction, buf, sm_list, 0, 0, NULL }; @@ -1839,6 +1843,7 @@ MpmStore *MpmStorePrepareBuffer(DetectEngineCtx *de_ctx, SigGroupHead *sgh, uint8_t *sids = SCCalloc(1, max_sid); if (sids == NULL) { SCFree(copy); + SCFree(sids_array); return NULL; } @@ -1852,8 +1857,10 @@ MpmStore *MpmStorePrepareBuffer(DetectEngineCtx *de_ctx, SigGroupHead *sgh, MpmStoreSetup(de_ctx, copy); MpmStoreAdd(de_ctx, copy); + SCFree(sids_array); return copy; } else { + SCFree(sids_array); return result; } } diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 37d7b7c0f58b..477247642b04 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -749,6 +749,8 @@ void SigTableSetup(void) DetectTransformFromBase64DecodeRegister(); SCDetectTransformDomainRegister(); DetectTransformLuaxformRegister(); + DetectTransformGunzipRegister(); + DetectTransformZlibDeflateRegister(); DetectFileHandlerRegister();