]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
detect/transform: from_base64 option parsing
authorJeff Lucovsky <jlucovsky@oisf.net>
Thu, 22 Feb 2024 14:47:18 +0000 (09:47 -0500)
committerVictor Julien <victor@inliniac.net>
Sat, 22 Jun 2024 13:54:36 +0000 (15:54 +0200)
Issue: 6487

Implement from_base64 option parsing in Rust. The Rust module also
contains unit tests.

rust/src/detect/error.rs
rust/src/detect/mod.rs
rust/src/detect/transform_base64.rs [new file with mode: 0644]

index 211c5afceb33859d2df19d52e7765acbe3151495..2ce949fd5c4f7cbd429ecc01a036a7c9629522a1 100644 (file)
@@ -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<I> {
     InvalidByteMath(String),
     InvalidIPRep(String),
+    InvalidTransformBase64(String),
 
     Nom(I, ErrorKind),
 }
index bb2441798edb8508c965d05b06d4600b63374336..766883623a5eca35531fe323c0d58892c7bd8c29 100644 (file)
@@ -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 (file)
index 0000000..e90ed22
--- /dev/null
@@ -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,
+        );
+    }
+}