/* Copyright (C) 2024-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. */ // Author: Jeff Lucovsky use crate::detect::error::RuleParseError; use crate::detect::parser::{parse_var, take_until_whitespace, ResultValue}; use crate::detect::{SIGMATCH_OPTIONAL_OPT, SIGMATCH_TRANSFORM_CAN_FAIL}; use crate::ffi::base64::{base64_decode, SCBase64Mode}; use suricata_sys::sys::{ DetectEngineCtx, DetectEngineThreadCtx, InspectionBuffer, SCDetectHelperTransformRegister, SCDetectSignatureAddTransform, SCInspectionBufferCheckAndExpand, SCInspectionBufferInPlace, SCInspectionBufferSetError, SCInspectionBufferTruncate, SCTransformTableElmt, Signature, }; use nom8::bytes::complete::tag; use nom8::character::complete::multispace0; use nom8::sequence::preceded; use nom8::{Err, IResult, Parser}; use std::ffi::CStr; use std::os::raw::{c_char, c_int, c_void}; use std::str; const TRANSFORM_FROM_BASE64_MODE_DEFAULT: SCBase64Mode = SCBase64Mode::SCBase64ModeRFC4648; const DETECT_TRANSFORM_BASE64_MAX_PARAM_COUNT: usize = 3; const DETECT_TRANSFORM_BASE64_FLAG_MODE: u8 = 0x01; const DETECT_TRANSFORM_BASE64_FLAG_NBYTES: u8 = 0x02; const DETECT_TRANSFORM_BASE64_FLAG_OFFSET: u8 = 0x04; // repr C to ensure a stable layout // good field ordering to avoid padding as rust does not have stable zeroed allocs #[repr(C)] #[derive(Debug, PartialEq)] struct DetectTransformFromBase64Data { nbytes: u32, offset: u32, mode: SCBase64Mode, // repr u8 flags: u8, } impl Default for DetectTransformFromBase64Data { fn default() -> Self { DetectTransformFromBase64Data { mode: TRANSFORM_FROM_BASE64_MODE_DEFAULT, nbytes: 0, offset: 0, flags: 0, } } } fn get_mode_value(value: &str) -> Option { let res = match value { "rfc4648" => Some(SCBase64Mode::SCBase64ModeRFC4648), "rfc2045" => Some(SCBase64Mode::SCBase64ModeRFC2045), "strict" => Some(SCBase64Mode::SCBase64ModeStrict), _ => None, }; res } fn parse_transform_base64( input: &str, ) -> IResult<&str, DetectTransformFromBase64Data, RuleParseError<&str>> { // Inner utility function for easy error creation. fn make_error(reason: String) -> nom8::Err> { Err::Error(RuleParseError::InvalidTransformBase64(reason)) } let mut transform_base64 = DetectTransformFromBase64Data::default(); // No options so return defaults if input.is_empty() { return Ok((input, transform_base64)); } let (_, values) = nom8::multi::separated_list1( tag(","), preceded(multispace0, nom8::bytes::complete::is_not(",")), ) .parse(input)?; // Too many options? if values.len() > DETECT_TRANSFORM_BASE64_MAX_PARAM_COUNT { return Err(make_error(format!("Incorrect argument string; at least 1 value must be specified but no more than {}: {:?}", DETECT_TRANSFORM_BASE64_MAX_PARAM_COUNT, input))); } for value in values { let (mut val, mut name) = take_until_whitespace(value)?; val = val.trim(); name = name.trim(); match name { "mode" => { if 0 != (transform_base64.flags & DETECT_TRANSFORM_BASE64_FLAG_MODE) { return Err(make_error("mode already set".to_string())); } if let Some(mode) = get_mode_value(val) { transform_base64.mode = mode; } else { return Err(make_error(format!("invalid mode value: {}", val))); } transform_base64.flags |= DETECT_TRANSFORM_BASE64_FLAG_MODE; } "offset" => { if 0 != (transform_base64.flags & DETECT_TRANSFORM_BASE64_FLAG_OFFSET) { return Err(make_error("offset already set".to_string())); } let (_, res) = parse_var(val)?; match res { ResultValue::Numeric(val) => { if val <= u64::from(u16::MAX) { transform_base64.offset = val as u32 } else { return Err(make_error(format!( "invalid offset value: must be between 0 and {}: {}", u16::MAX, val ))); } } ResultValue::String(_val) => { SCLogError!("offset value must be a value, not a variable name"); return Err(make_error( "offset value must be a value, not a variable name".to_string(), )); } } transform_base64.flags |= DETECT_TRANSFORM_BASE64_FLAG_OFFSET; } "bytes" => { if 0 != (transform_base64.flags & DETECT_TRANSFORM_BASE64_FLAG_NBYTES) { return Err(make_error("bytes already set".to_string())); } let (_, res) = parse_var(val)?; match res { ResultValue::Numeric(val) => { if val as u32 <= u32::from(u16::MAX) { transform_base64.nbytes = val as u32 } else { return Err(make_error(format!( "invalid bytes value: must be between {} and {}: {}", 0, u16::MAX, val ))); } } ResultValue::String(_val) => { SCLogError!("byte value must be a value, not a variable name"); return Err(make_error( "byte value must be a value, not a variable name".to_string(), )); } } transform_base64.flags |= DETECT_TRANSFORM_BASE64_FLAG_NBYTES; } _ => { return Err(make_error(format!("unknown base64 keyword: {}", name))); } }; } Ok((input, transform_base64)) } unsafe fn base64_parse(c_arg: *const c_char) -> *mut DetectTransformFromBase64Data { if c_arg.is_null() { let detect = DetectTransformFromBase64Data::default(); return Box::into_raw(Box::new(detect)); } if let Ok(arg) = CStr::from_ptr(c_arg).to_str() { match parse_transform_base64(arg) { Ok((_, detect)) => return Box::into_raw(Box::new(detect)), Err(_) => return std::ptr::null_mut(), } } return std::ptr::null_mut(); } unsafe extern "C" fn base64_free(_de: *mut DetectEngineCtx, ctx: *mut c_void) { std::mem::drop(Box::from_raw(ctx as *mut DetectTransformFromBase64Data)); } static mut G_TRANSFORM_BASE64_ID: c_int = 0; unsafe extern "C" fn base64_setup( de: *mut DetectEngineCtx, s: *mut Signature, opt_str: *const std::os::raw::c_char, ) -> c_int { let ctx = base64_parse(opt_str) as *mut c_void; if ctx.is_null() { return -1; } let r = SCDetectSignatureAddTransform(s, G_TRANSFORM_BASE64_ID, ctx); if r != 0 { base64_free(de, ctx); } return r; } unsafe extern "C" fn base64_id(data: *mut *const u8, length: *mut u32, ctx: *const c_void) { if data.is_null() || length.is_null() || ctx.is_null() { return; } // This works because the structure is flat // Once variables are really implemented, we should investigate if the structure should own // its serialization or just borrow it to a caller *data = ctx as *const u8; *length = std::mem::size_of::() as u32; } /// Outcome of a base64 decode against an input slice. /// /// The pure decode logic is separated from the FFI shim so tests can drive /// it directly with a `&[u8]` and assert against a plain Rust value, rather /// than mocking the InspectionBuffer helpers. #[derive(Debug, PartialEq, Eq)] enum Base64TransformOutcome { /// Empty input: nothing to decode. The inspection buffer is left untouched. Skip, /// Decoder produced at least one byte. The FFI shim copies these into /// the inspection buffer scratch and calls `SCInspectionBufferTruncate`. Decoded(Vec), /// A non-empty input was not decoded: offset at or past the end, nbytes /// larger than the input left after offset, or zero bytes decoded. The /// FFI shim calls `SCInspectionBufferSetError` to signal the failure. Error, } /// Apply `offset` and `bytes` to the input. `Err` holds the outcome when /// there is nothing to decode: `Skip` or `Error`. fn base64_slice<'a>( input: &'a [u8], ctx: &DetectTransformFromBase64Data, ) -> Result<&'a [u8], Base64TransformOutcome> { if input.is_empty() { return Err(Base64TransformOutcome::Skip); } let mut slice = input; if ctx.offset > 0 { if ctx.offset as usize >= slice.len() { return Err(Base64TransformOutcome::Error); } slice = &slice[ctx.offset as usize..]; } if ctx.nbytes > 0 { if ctx.nbytes as usize > slice.len() { return Err(Base64TransformOutcome::Error); } slice = &slice[..ctx.nbytes as usize]; } Ok(slice) } fn base64_transform_pure( input: &[u8], ctx: &DetectTransformFromBase64Data, ) -> Base64TransformOutcome { let slice = match base64_slice(input, ctx) { Ok(slice) => slice, Err(outcome) => return outcome, }; // base64_decode needs an output at least as long as its input. let mut output = vec![0u8; slice.len()]; let num_decoded = base64_decode(slice, ctx.mode, &mut output) as usize; if num_decoded == 0 { return Base64TransformOutcome::Error; } output.truncate(num_decoded); Base64TransformOutcome::Decoded(output) } unsafe extern "C" fn base64_transform( _det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *const c_void, ) { let input_ptr = (*buffer).inspect; let input_len = (*buffer).inspect_len; if input_ptr.is_null() { return; } let input = build_slice!(input_ptr, input_len as usize); let ctx = cast_pointer!(ctx, DetectTransformFromBase64Data); if SCInspectionBufferInPlace(buffer) { // The input is the scratch buffer, so a decode that fails partway // would leave decoded bytes over it. Decode into a separate buffer // and copy the result over only when the decode succeeds. match base64_transform_pure(input, ctx) { Base64TransformOutcome::Skip => {} Base64TransformOutcome::Decoded(decoded) => { let scratch = SCInspectionBufferCheckAndExpand(buffer, decoded.len() as u32); if scratch.is_null() { // allocation failure: the decoded bytes cannot replace the input SCInspectionBufferSetError(buffer); return; } std::ptr::copy_nonoverlapping(decoded.as_ptr(), scratch, decoded.len()); SCInspectionBufferTruncate(buffer, decoded.len() as u32); } Base64TransformOutcome::Error => { SCInspectionBufferSetError(buffer); } } return; } let slice = match base64_slice(input, ctx) { Ok(slice) => slice, Err(Base64TransformOutcome::Error) => { SCInspectionBufferSetError(buffer); return; } Err(_) => return, }; // base64_decode needs an output at least as long as its input. let scratch = SCInspectionBufferCheckAndExpand(buffer, slice.len() as u32); if scratch.is_null() { // allocation failure: the decoded bytes cannot replace the input SCInspectionBufferSetError(buffer); return; } let output = std::slice::from_raw_parts_mut(scratch, slice.len()); let num_decoded = base64_decode(slice, ctx.mode, output); if num_decoded == 0 { // inspect still points at the input, so it is unchanged SCInspectionBufferSetError(buffer); return; } SCInspectionBufferTruncate(buffer, num_decoded); } #[no_mangle] pub unsafe extern "C" fn DetectTransformFromBase64DecodeRegister() { let kw = SCTransformTableElmt { name: b"from_base64\0".as_ptr() as *const libc::c_char, desc: b"convert the base64 decode of the buffer\0".as_ptr() as *const libc::c_char, url: b"/rules/transforms.html#from-base64\0".as_ptr() as *const libc::c_char, Setup: Some(base64_setup), flags: SIGMATCH_OPTIONAL_OPT | SIGMATCH_TRANSFORM_CAN_FAIL, Transform: Some(base64_transform), Free: Some(base64_free), TransformValidate: None, TransformId: Some(base64_id), }; unsafe { G_TRANSFORM_BASE64_ID = SCDetectHelperTransformRegister(&kw); if G_TRANSFORM_BASE64_ID < 0 { SCLogWarning!("Failed registering transform base64"); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parser_invalid() { assert!(parse_transform_base64("bytes 4, offset 3933, mode unknown").is_err()); assert!(parse_transform_base64("bytes 4, offset 70000, mode strict").is_err()); assert!(parse_transform_base64("bytes 4, offset 3933, mode strict, mode rfc2045").is_err()); } #[test] fn test_parser_parse_partial_valid() { let (_, val) = parse_transform_base64("bytes 4").unwrap(); assert_eq!( val, DetectTransformFromBase64Data { nbytes: 4, offset: 0, mode: TRANSFORM_FROM_BASE64_MODE_DEFAULT, flags: DETECT_TRANSFORM_BASE64_FLAG_NBYTES, } ); let args = DetectTransformFromBase64Data { nbytes: 4, offset: 3933, mode: TRANSFORM_FROM_BASE64_MODE_DEFAULT, flags: DETECT_TRANSFORM_BASE64_FLAG_NBYTES | DETECT_TRANSFORM_BASE64_FLAG_OFFSET, }; let (_, val) = parse_transform_base64("bytes 4, offset 3933").unwrap(); assert_eq!(val, args); let (_, val) = parse_transform_base64("offset 3933, bytes 4").unwrap(); assert_eq!(val, args); let (_, val) = parse_transform_base64("mode rfc2045").unwrap(); assert_eq!( val, DetectTransformFromBase64Data { nbytes: 0, offset: 0, mode: SCBase64Mode::SCBase64ModeRFC2045, flags: DETECT_TRANSFORM_BASE64_FLAG_MODE, } ); } #[test] fn test_parser_parse_valid() { let (_, val) = parse_transform_base64("").unwrap(); assert_eq!( val, DetectTransformFromBase64Data { mode: TRANSFORM_FROM_BASE64_MODE_DEFAULT, ..Default::default() } ); let (_, val) = parse_transform_base64("bytes 4, offset 3933, mode strict").unwrap(); assert_eq!( val, DetectTransformFromBase64Data { nbytes: 4, offset: 3933, mode: SCBase64Mode::SCBase64ModeStrict, flags: DETECT_TRANSFORM_BASE64_FLAG_NBYTES | DETECT_TRANSFORM_BASE64_FLAG_OFFSET | DETECT_TRANSFORM_BASE64_FLAG_MODE, } ); let (_, val) = parse_transform_base64("bytes 4, offset 3933, mode rfc2045").unwrap(); assert_eq!( val, DetectTransformFromBase64Data { nbytes: 4, offset: 3933, mode: SCBase64Mode::SCBase64ModeRFC2045, flags: DETECT_TRANSFORM_BASE64_FLAG_NBYTES | DETECT_TRANSFORM_BASE64_FLAG_OFFSET | DETECT_TRANSFORM_BASE64_FLAG_MODE, } ); let (_, val) = parse_transform_base64("bytes 4, offset 3933, mode rfc4648").unwrap(); assert_eq!( val, DetectTransformFromBase64Data { nbytes: 4, offset: 3933, mode: SCBase64Mode::SCBase64ModeRFC4648, flags: DETECT_TRANSFORM_BASE64_FLAG_NBYTES | DETECT_TRANSFORM_BASE64_FLAG_OFFSET | DETECT_TRANSFORM_BASE64_FLAG_MODE, } ); assert!(parse_transform_base64("bytes 4, offset var, mode rfc4648").is_err()); assert!(parse_transform_base64("bytes var, offset 3933, mode rfc4648").is_err()); } /// Parse `sig`, run the pure decode against `buf`, and assert the outcome. fn assert_outcome(sig: &str, buf: &[u8], expected: Base64TransformOutcome) { let (_, ctx) = parse_transform_base64(sig).unwrap(); assert_eq!(base64_transform_pure(buf, &ctx), expected); } #[test] fn test_base64_transform() { use Base64TransformOutcome::*; /* Simple success case */ assert_outcome( "", b"VGhpcyBpcyBTdXJpY2F0YQ==", Decoded(b"This is Suricata".to_vec()), ); /* Simple success case with RFC2045 */ assert_outcome("mode rfc2045", b"Zm 9v Ym Fy", Decoded(b"foobar".to_vec())); /* Decode failure case -- should signal error */ assert_outcome("mode strict", b"This is Suricata\n", Error); /* bytes > len: nothing to decode --> error */ assert_outcome("bytes 25", b"VGhpcyBpcyBTdXJpY2F0YQ==", Error); /* offset >= len: nothing to decode --> error */ assert_outcome("offset 25", b"VGhpcyBpcyBTdXJpY2F0YQ==", Error); /* partial transform */ assert_outcome( "bytes 12", b"VGhpcyBpcyBTdXJpY2F0YQ==", Decoded(b"This is S".to_vec()), ); /* transform from non-zero offset */ assert_outcome( "offset 4", b"VGhpcyBpcyBTdXJpY2F0YQ==", Decoded(b"s is Suricata".to_vec()), ); /* partial decode */ assert_outcome( "mode rfc2045, bytes 15", b"SGVs bG8 gV29y bGQ=", Decoded(b"Hello Wor".to_vec()), ); /* rfc2045 partial decode: valid prefix decoded, invalid suffix skipped */ assert_outcome( "mode rfc2045", b"ABCD!!!!", Decoded(b"\x00\x10\x83".to_vec()), ); /* input is not base64 encoded: decoder returns 0 -> Error */ assert_outcome("mode rfc2045", b"!!!!", Error); } #[test] fn test_base64_transform_decode_error() { use Base64TransformOutcome::*; assert_outcome("mode strict", b"!!!!", Error); assert_outcome("mode rfc4648", b"!!!!", Error); } /// An empty (non-absent) buffer is neither a decode success nor a decode /// failure: the pure core returns Skip so the FFI shim leaves the /// inspection buffer untouched. #[test] fn test_base64_transform_empty_buffer_is_skip() { assert_outcome("", b"", Base64TransformOutcome::Skip); } /// offset at or past the end, and nbytes larger than the input left after /// offset, leave nothing to decode and are errors. nbytes equal to the /// input left after offset decodes all of it. #[test] fn test_base64_transform_offset_and_nbytes_out_of_range() { use Base64TransformOutcome::*; assert_outcome("offset 10", b"YWJj", Error); assert_outcome("offset 4", b"YWJj", Error); assert_outcome("bytes 10", b"YWJj", Error); assert_outcome("bytes 5", b"YWJj", Error); assert_outcome("bytes 4", b"YWJj", Decoded(b"abc".to_vec())); assert_outcome("bytes 4, offset 2", b"xxYWJj", Decoded(b"abc".to_vec())); assert_outcome("bytes 5, offset 2", b"xxYWJj", Error); } }