mirror of https://github.com/OISF/suricata
detect/transform: from_base64 option parsing
Issue: 6487 Implement from_base64 option parsing in Rust. The Rust module also contains unit tests.pull/11353/head
parent
ab0cb960a1
commit
1823681709
@ -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 <jlucovsky@oisf.net>
|
||||||
|
|
||||||
|
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<DetectBase64Mode> {
|
||||||
|
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<RuleParseError<&'static str>> {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue