You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
suricata/rust/src/detect/transforms/base64.rs

556 lines
20 KiB
Rust

/* 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 <jlucovsky@oisf.net>
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<SCBase64Mode> {
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<RuleParseError<&'static str>> {
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::<DetectTransformFromBase64Data>() 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<u8>),
/// 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);
}
}