From: Jeff Lucovsky Date: Thu, 22 Feb 2024 14:47:18 +0000 (-0500) Subject: detect/transform: from_base64 option parsing X-Git-Tag: suricata-8.0.0-beta1~1114 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1823681709dbd22dde5581649e3627c63f4a2257;p=thirdparty%2Fsuricata.git detect/transform: from_base64 option parsing Issue: 6487 Implement from_base64 option parsing in Rust. The Rust module also contains unit tests. --- diff --git a/rust/src/detect/error.rs b/rust/src/detect/error.rs index 211c5afceb..2ce949fd5c 100644 --- a/rust/src/detect/error.rs +++ b/rust/src/detect/error.rs @@ -20,10 +20,13 @@ use nom7::error::{ErrorKind, ParseError}; /// Custom rule parse errors. /// /// Implemented based on the Nom example for implementing custom errors. +/// The string is an error message provided by the parsing logic, e.g., +/// Incorrect usage because of "x", "y" and "z" #[derive(Debug, PartialEq, Eq)] pub enum RuleParseError { InvalidByteMath(String), InvalidIPRep(String), + InvalidTransformBase64(String), Nom(I, ErrorKind), } diff --git a/rust/src/detect/mod.rs b/rust/src/detect/mod.rs index bb2441798e..766883623a 100644 --- a/rust/src/detect/mod.rs +++ b/rust/src/detect/mod.rs @@ -22,6 +22,7 @@ pub mod error; pub mod iprep; pub mod parser; pub mod stream_size; +pub mod transform_base64; pub mod uint; pub mod uri; pub mod requires; diff --git a/rust/src/detect/transform_base64.rs b/rust/src/detect/transform_base64.rs new file mode 100644 index 0000000000..e90ed221fd --- /dev/null +++ b/rust/src/detect/transform_base64.rs @@ -0,0 +1,412 @@ +/* Copyright (C) 2024 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 std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +use nom7::bytes::complete::tag; +use nom7::character::complete::multispace0; +use nom7::sequence::preceded; +use nom7::{Err, IResult}; +use std::str; + +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DetectBase64Mode { + Base64ModeRelax = 0, + Base64ModeRFC2045, + Base64ModeStrict, + Base64ModeRFC4648, +} + +pub const TRANSFORM_FROM_BASE64_MODE_DEFAULT: DetectBase64Mode = DetectBase64Mode::Base64ModeRFC4648; + +const DETECT_TRANSFORM_BASE64_MAX_PARAM_COUNT: usize = 3; +pub const DETECT_TRANSFORM_BASE64_FLAG_MODE: u8 = 0x01; +pub const DETECT_TRANSFORM_BASE64_FLAG_NBYTES: u8 = 0x02; +pub const DETECT_TRANSFORM_BASE64_FLAG_OFFSET: u8 = 0x04; +pub const DETECT_TRANSFORM_BASE64_FLAG_OFFSET_VAR: u8 = 0x08; +pub const DETECT_TRANSFORM_BASE64_FLAG_NBYTES_VAR: u8 = 0x10; + +#[repr(C)] +#[derive(Debug)] +pub struct SCDetectTransformFromBase64Data { + flags: u8, + nbytes: u32, + nbytes_str: *const c_char, + offset: u32, + offset_str: *const c_char, + mode: DetectBase64Mode, +} + +impl Drop for SCDetectTransformFromBase64Data { + fn drop(&mut self) { + unsafe { + if !self.offset_str.is_null() { + let _ = CString::from_raw(self.offset_str as *mut c_char); + } + if !self.nbytes_str.is_null() { + let _ = CString::from_raw(self.nbytes_str as *mut c_char); + } + } + } +} +impl Default for SCDetectTransformFromBase64Data { + fn default() -> Self { + SCDetectTransformFromBase64Data { + flags: 0, + nbytes: 0, + nbytes_str: std::ptr::null_mut(), + offset: 0, + offset_str: std::ptr::null_mut(), + mode: TRANSFORM_FROM_BASE64_MODE_DEFAULT, + } + } +} + +impl SCDetectTransformFromBase64Data { + pub fn new() -> Self { + Self { + ..Default::default() + } + } +} + +fn get_mode_value(value: &str) -> Option { + let res = match value { + "rfc4648" => Some(DetectBase64Mode::Base64ModeRFC4648), + "rfc2045" => Some(DetectBase64Mode::Base64ModeRFC2045), + "strict" => Some(DetectBase64Mode::Base64ModeStrict), + _ => None, + }; + + res +} + +fn parse_transform_base64( + input: &str, +) -> IResult<&str, SCDetectTransformFromBase64Data, RuleParseError<&str>> { + // Inner utility function for easy error creation. + fn make_error(reason: String) -> nom7::Err> { + Err::Error(RuleParseError::InvalidTransformBase64(reason)) + } + let mut transform_base64 = SCDetectTransformFromBase64Data::new(); + + // No options so return defaults + if input.is_empty() { + return Ok((input, transform_base64)); + } + let (_, values) = nom7::multi::separated_list1( + tag(","), + preceded(multispace0, nom7::bytes::complete::is_not(",")), + )(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 <= u16::MAX.into() { + 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) => match CString::new(val) { + Ok(newval) => { + transform_base64.offset_str = newval.into_raw(); + transform_base64.flags |= DETECT_TRANSFORM_BASE64_FLAG_OFFSET_VAR; + } + _ => { + return Err(make_error( + "parse string not safely convertible to C".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 <= u16::MAX.into() { + 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) => match CString::new(val) { + Ok(newval) => { + transform_base64.nbytes_str = newval.into_raw(); + transform_base64.flags |= DETECT_TRANSFORM_BASE64_FLAG_NBYTES_VAR; + } + _ => { + return Err(make_error( + "parse string not safely convertible to C".to_string(), + )) + } + }, + } + transform_base64.flags |= DETECT_TRANSFORM_BASE64_FLAG_NBYTES; + } + _ => { + return Err(make_error(format!("unknown base64 keyword: {}", name))); + } + }; + } + + Ok((input, transform_base64)) +} + +/// Intermediary function between the C code and the parsing functions. +#[no_mangle] +pub unsafe extern "C" fn SCTransformBase64Parse( + c_arg: *const c_char, +) -> *mut SCDetectTransformFromBase64Data { + if c_arg.is_null() { + return std::ptr::null_mut(); + } + + let arg = CStr::from_ptr(c_arg) + .to_str() + .unwrap_or(""); + + match parse_transform_base64(arg) { + Ok((_, detect)) => return Box::into_raw(Box::new(detect)), + Err(_) => return std::ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn SCTransformBase64Free(ptr: *mut SCDetectTransformFromBase64Data) { + if !ptr.is_null() { + let _ = Box::from_raw(ptr); + } +} + +#[cfg(test)] +mod tests { + use super::*; + // structure equality only used by test cases + impl PartialEq for SCDetectTransformFromBase64Data { + fn eq(&self, other: &Self) -> bool { + let mut res: bool = true; + + if !self.nbytes_str.is_null() && !other.nbytes_str.is_null() { + let s_val = unsafe { CStr::from_ptr(self.nbytes_str) }; + let o_val = unsafe { CStr::from_ptr(other.nbytes_str) }; + res = s_val == o_val; + } else if !self.nbytes_str.is_null() || !other.nbytes_str.is_null() { + return false; + } + + if !self.offset_str.is_null() && !other.offset_str.is_null() { + let s_val = unsafe { CStr::from_ptr(self.offset_str) }; + let o_val = unsafe { CStr::from_ptr(other.offset_str) }; + res = s_val == o_val; + } else if !self.offset_str.is_null() || !other.offset_str.is_null() { + return false; + } + + res && self.nbytes == other.nbytes + && self.flags == other.flags + && self.offset == other.offset + && self.mode == other.mode + } + } + + fn valid_test( + args: &str, + nbytes: u32, + nbytes_str: &str, + offset: u32, + offset_str: &str, + mode: DetectBase64Mode, + flags: u8, + ) { + let tbd = SCDetectTransformFromBase64Data { + flags, + nbytes, + nbytes_str: if !nbytes_str.is_empty() { + CString::new(nbytes_str).unwrap().into_raw() + } else { + std::ptr::null_mut() + }, + offset, + offset_str: if !offset_str.is_empty() { + CString::new(offset_str).unwrap().into_raw() + } else { + std::ptr::null_mut() + }, + mode, + }; + + let (_, val) = parse_transform_base64(args).unwrap(); + assert_eq!(val, tbd); + } + + #[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 70000, mode strict, mode rfc2045").is_err() + ); + } + + #[test] + fn test_parser_parse_partial_valid() { + let mut tbd = SCDetectTransformFromBase64Data { + nbytes: 4, + offset: 0, + mode: TRANSFORM_FROM_BASE64_MODE_DEFAULT, + flags: 0, + ..Default::default() + }; + + tbd.mode = TRANSFORM_FROM_BASE64_MODE_DEFAULT; + tbd.flags = DETECT_TRANSFORM_BASE64_FLAG_NBYTES; + let (_, val) = parse_transform_base64("bytes 4").unwrap(); + assert_eq!(val, tbd); + + tbd.offset = 3933; + tbd.flags = DETECT_TRANSFORM_BASE64_FLAG_NBYTES | DETECT_TRANSFORM_BASE64_FLAG_OFFSET; + let (_, val) = parse_transform_base64("bytes 4, offset 3933").unwrap(); + assert_eq!(val, tbd); + + tbd.flags = DETECT_TRANSFORM_BASE64_FLAG_NBYTES | DETECT_TRANSFORM_BASE64_FLAG_OFFSET; + let (_, val) = parse_transform_base64("offset 3933, bytes 4").unwrap(); + assert_eq!(val, tbd); + + tbd.flags = DETECT_TRANSFORM_BASE64_FLAG_MODE; + tbd.mode = DetectBase64Mode::Base64ModeRFC2045; + tbd.offset = 0; + tbd.nbytes = 0; + let (_, val) = parse_transform_base64("mode rfc2045").unwrap(); + assert_eq!(val, tbd); + } + + #[test] + fn test_parser_parse_valid() { + valid_test("", 0, "", 0, "", TRANSFORM_FROM_BASE64_MODE_DEFAULT, 0); + + valid_test( + "bytes 4, offset 3933, mode strict", + 4, + "", + 3933, + "", + DetectBase64Mode::Base64ModeStrict, + DETECT_TRANSFORM_BASE64_FLAG_NBYTES + | DETECT_TRANSFORM_BASE64_FLAG_OFFSET + | DETECT_TRANSFORM_BASE64_FLAG_MODE, + ); + + valid_test( + "bytes 4, offset 3933, mode rfc2045", + 4, + "", + 3933, + "", + DetectBase64Mode::Base64ModeRFC2045, + DETECT_TRANSFORM_BASE64_FLAG_NBYTES + | DETECT_TRANSFORM_BASE64_FLAG_OFFSET + | DETECT_TRANSFORM_BASE64_FLAG_MODE, + ); + + valid_test( + "bytes 4, offset 3933, mode rfc4648", + 4, + "", + 3933, + "", + DetectBase64Mode::Base64ModeRFC4648, + DETECT_TRANSFORM_BASE64_FLAG_NBYTES + | DETECT_TRANSFORM_BASE64_FLAG_OFFSET + | DETECT_TRANSFORM_BASE64_FLAG_MODE, + ); + + valid_test( + "bytes 4, offset var, mode rfc4648", + 4, + "", + 0, + "var", + DetectBase64Mode::Base64ModeRFC4648, + DETECT_TRANSFORM_BASE64_FLAG_NBYTES + | DETECT_TRANSFORM_BASE64_FLAG_OFFSET_VAR + | DETECT_TRANSFORM_BASE64_FLAG_OFFSET + | DETECT_TRANSFORM_BASE64_FLAG_MODE, + ); + + valid_test( + "bytes var, offset 3933, mode rfc4648", + 0, + "var", + 3933, + "", + DetectBase64Mode::Base64ModeRFC4648, + DETECT_TRANSFORM_BASE64_FLAG_NBYTES + | DETECT_TRANSFORM_BASE64_FLAG_NBYTES_VAR + | DETECT_TRANSFORM_BASE64_FLAG_OFFSET + | DETECT_TRANSFORM_BASE64_FLAG_MODE, + ); + } +}