diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index d4c3b271b6..9a0f8a0150 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -39,3 +39,6 @@ snmp-parser = "0.6" tls-parser = "0.9" x509-parser = "0.6.5" libc = "0.2.67" + +[dev-dependencies] +test-case = "1.0" diff --git a/rust/src/asn1/mod.rs b/rust/src/asn1/mod.rs new file mode 100644 index 0000000000..c88bcdfc86 --- /dev/null +++ b/rust/src/asn1/mod.rs @@ -0,0 +1,428 @@ +/* Copyright (C) 2020 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. + */ + +use der_parser::ber::{parse_ber_recursive, BerObject, BerObjectContent, BerTag}; +use der_parser::error::BerError; +use std::convert::TryFrom; + +mod parse_rules; +use parse_rules::DetectAsn1Data; + +/// Container for parsed Asn1 objects +#[derive(Debug)] +pub struct Asn1(Vec>); + +/// Errors possible during decoding of Asn1 +#[derive(Debug)] +#[repr(u32)] +pub enum Asn1DecodeError { + Success = 0, + InvalidKeywordParameter, + MaxFrames, + InvalidStructure, + BerTypeError, + BerValueError, + InvalidTag, + InvalidLength, + InvalidClass, + ConstructExpected, + ConstructUnexpected, + IntegerTooLarge, + BerMaxDepth, + ObjectTooShort, + DerConstraintFailed, + UnknownTag, + Unsupported, +} + +/// Enumeration of Asn1 checks +#[derive(Debug, PartialEq)] +enum Asn1Check { + OversizeLength, + BitstringOverflow, + DoubleOverflow, +} + +/// Errors possible during Asn1 checks +#[derive(Debug)] +#[repr(u32)] +pub enum Asn1CheckError { + Success = 0, + MaxDepth, +} + +impl Asn1 { + /// Checks each BerObject contained in self with the provided detection + /// data, returns the first successful match if one occurs + fn check(&self, ad: &DetectAsn1Data) -> Result, Asn1CheckError> { + for obj in &self.0 { + let res = Asn1::check_object_recursive(obj, ad, ad.max_frames as usize)?; + if res.is_some() { + return Ok(res); + } + } + + Ok(None) + } + + fn check_object_recursive( + obj: &BerObject, + ad: &DetectAsn1Data, + max_depth: usize, + ) -> Result, Asn1CheckError> { + // Check stack depth + if max_depth == 0 { + return Err(Asn1CheckError::MaxDepth); + } + + // Check current object + let res = Asn1::check_object(obj, ad); + if res.is_some() { + return Ok(res); + } + + // Check sub-nodes + for node in obj.ref_iter() { + let res = Asn1::check_object_recursive(node, ad, max_depth - 1)?; + if res.is_some() { + return Ok(res); + } + } + + Ok(None) + } + + /// Checks a BerObject and subnodes against the Asn1 checks + fn check_object(obj: &BerObject, ad: &DetectAsn1Data) -> Option { + // oversize_length will check if a node has a length greater than + // the user supplied length + if let Some(oversize_length) = ad.oversize_length { + if obj.header.len > oversize_length as u64 + || obj.content.as_slice().unwrap_or(&[]).len() > oversize_length as usize + { + return Some(Asn1Check::OversizeLength); + } + } + + // bitstring_overflow check a malformed option where the number of bits + // to ignore is greater than the length decoded (in bits) + if ad.bitstring_overflow + && (obj.header.is_universal() + && obj.header.tag == BerTag::BitString + && obj.header.is_primitive()) + { + if let BerObjectContent::BitString(bits, _v) = &obj.content { + if obj.header.len > 0 + && *bits as u64 > (obj.header.len.checked_mul(8).unwrap_or(std::u64::MAX)) + { + return Some(Asn1Check::BitstringOverflow); + } + } + } + + // double_overflow checks a known issue that affects the MSASN1 library + // when decoding double/real types. If the encoding is ASCII, + // and the buffer is greater than 256, the array is overflown + if ad.double_overflow + && (obj.header.is_universal() + && obj.header.tag == BerTag::RealType + && obj.header.is_primitive()) + { + if let Ok(data) = obj.content.as_slice() { + if obj.header.len > 0 + && !data.is_empty() + && data[0] & 0xC0 == 0 + && (obj.header.len > 256 || data.len() > 256) + { + return Some(Asn1Check::DoubleOverflow); + } + } + } + + None + } + + fn from_slice(input: &'static [u8], ad: &DetectAsn1Data) -> Result { + let mut results = Vec::new(); + let mut rest = input; + + // while there's data to process + while !rest.is_empty() { + let max_depth = ad.max_frames as usize; + + if results.len() >= max_depth { + return Err(Asn1DecodeError::MaxFrames); + } + + let res = parse_ber_recursive(rest, max_depth); + + match res { + Ok((new_rest, obj)) => { + results.push(obj); + + rest = new_rest; + } + // If there's an error, bail + Err(_) => { + // silent error as this could fail + // on non-asn1 or fragmented packets + break; + } + } + } + + Ok(Asn1(results)) + } +} + +/// Decodes Asn1 objects from an input + length while applying the offset +/// defined in the asn1 keyword options +fn asn1_decode( + input: *const u8, + input_len: u32, + ad: &DetectAsn1Data, +) -> Result { + // Get offset + let offset = if let Some(absolute_offset) = ad.absolute_offset { + absolute_offset as isize + } else if let Some(relative_offset) = ad.relative_offset { + relative_offset as isize + } else { + 0 + }; + + // Make sure we won't read past the end of the buffer + if offset >= input_len as isize { + return Err(Asn1DecodeError::InvalidKeywordParameter); + } + + // Apply offset to input pointer + let input = unsafe { input.offset(offset) }; + + // Adjust the length + let input_len = (input_len as isize) + .checked_sub(offset) + .ok_or(Asn1DecodeError::InvalidKeywordParameter)?; + let input_len = + usize::try_from(input_len).map_err(|_| Asn1DecodeError::InvalidKeywordParameter)?; + + // Get the slice from memory + let slice = build_slice!(input, input_len); + + Asn1::from_slice(slice, ad) +} + +/// Attempt to parse a Asn1 object from input, and return a pointer +/// to the parsed object if successful, null on failure +/// +/// # Safety +/// +/// input must be a valid buffer of at least input_len bytes +/// pointer must be freed using `rs_asn1_free` +#[no_mangle] +pub(crate) unsafe extern "C" fn rs_asn1_decode( + input: *const u8, + input_len: u32, + ad_ptr: *const DetectAsn1Data, +) -> *mut Asn1 { + if input.is_null() || input_len == 0 || ad_ptr.is_null() { + return std::ptr::null_mut(); + } + + let ad = &*ad_ptr; + + let res = asn1_decode(input, input_len, ad); + + match res { + Ok(asn1) => Box::into_raw(Box::new(asn1)), + Err(_e) => std::ptr::null_mut(), + } +} + +/// Free a Asn1 object allocated by Rust +/// +/// # Safety +/// +/// ptr must be a valid object obtained using `rs_asn1_decode` +#[no_mangle] +pub unsafe extern "C" fn rs_asn1_free(ptr: *mut Asn1) { + if ptr.is_null() { + return; + } + drop(Box::from_raw(ptr)); +} + +/// This function implements the detection of the following options: +/// - oversize_length +/// - bitstring_overflow +/// - double_overflow +/// +/// # Safety +/// +/// ptr must be a valid object obtained using `rs_asn1_decode` +/// ad_ptr must be a valid object obtained using `rs_detect_asn1_parse` +/// +/// Returns 1 if any of the options match, 0 if not +#[no_mangle] +pub(crate) unsafe extern "C" fn rs_asn1_checks( + ptr: *const Asn1, + ad_ptr: *const DetectAsn1Data, +) -> u8 { + if ptr.is_null() || ad_ptr.is_null() { + return 0; + } + + let asn1 = &*ptr; + let ad = &*ad_ptr; + + if let Ok(Some(_)) = asn1.check(ad) { + return 1; + } + + 0 +} + +impl From> for Asn1DecodeError { + fn from(e: nom::Err) -> Asn1DecodeError { + match e { + nom::Err::Incomplete(_) => Asn1DecodeError::InvalidLength, + nom::Err::Error(e) | nom::Err::Failure(e) => match e { + BerError::BerTypeError => Asn1DecodeError::BerTypeError, + BerError::BerValueError => Asn1DecodeError::BerValueError, + BerError::InvalidTag => Asn1DecodeError::InvalidTag, + BerError::InvalidClass => Asn1DecodeError::InvalidClass, + BerError::InvalidLength => Asn1DecodeError::InvalidLength, + BerError::ConstructExpected => Asn1DecodeError::ConstructExpected, + BerError::ConstructUnexpected => Asn1DecodeError::ConstructUnexpected, + BerError::IntegerTooLarge => Asn1DecodeError::IntegerTooLarge, + BerError::BerMaxDepth => Asn1DecodeError::BerMaxDepth, + BerError::ObjectTooShort => Asn1DecodeError::ObjectTooShort, + BerError::DerConstraintFailed => Asn1DecodeError::DerConstraintFailed, + BerError::UnknownTag => Asn1DecodeError::UnknownTag, + BerError::Unsupported => Asn1DecodeError::Unsupported, + _ => Asn1DecodeError::InvalidStructure, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + // Example from the specification X.690-0207 Appendix A.3 + static ASN1_A3: &[u8] = b"\x60\x81\x85\x61\x10\x1A\x04John\x1A\x01 \ + P\x1A\x05Smith\xA0\x0A\x1A\x08Director \ + \x42\x01\x33\xA1\x0A\x43\x0819710917 \ + \xA2\x12\x61\x10\x1A\x04Mary\x1A\x01T\x1A\x05 \ + Smith\xA3\x42\x31\x1F\x61\x11\x1A\x05Ralph\x1A\x01 \ + T\x1A\x05Smith\xA0\x0A\x43\x0819571111 \ + \x31\x1F\x61\x11\x1A\x05Susan\x1A\x01B\x1A\x05 \ + Jones\xA0\x0A\x43\x0819590717"; + + /// Ensure that the checks work when they should + #[test_case("oversize_length 132 absolute_offset 0", ASN1_A3, DetectAsn1Data { + oversize_length: Some(132), + absolute_offset: Some(0), + ..Default::default() + }, Some(Asn1Check::OversizeLength); "Test oversize_length rule (match)" )] + #[test_case("oversize_length 133 absolute_offset 0", ASN1_A3, DetectAsn1Data { + oversize_length: Some(133), + absolute_offset: Some(0), + ..Default::default() + }, None; "Test oversize_length rule (non-match)" )] + #[test_case("bitstring_overflow, absolute_offset 0", + /* tagnum bitstring, primitive, and as universal tag, + length = 1 octet, but the next octet specify to ignore the last 256 bits */ + b"\x03\x01\xFF", + DetectAsn1Data { + bitstring_overflow: true, + absolute_offset: Some(0), + ..Default::default() + }, Some(Asn1Check::BitstringOverflow); "Test bitstring_overflow rule (match)" )] + #[test_case("bitstring_overflow, absolute_offset 0", + /* tagnum bitstring, primitive, and as universal tag, + length = 1 octet, but the next octet specify to ignore the last 7 bits */ + b"\x03\x01\x07", + DetectAsn1Data { + bitstring_overflow: true, + absolute_offset: Some(0), + ..Default::default() + }, None; "Test bitstring_overflow rule (non-match)" )] + #[test_case("double_overflow, absolute_offset 0", + { + static TEST_BUF: [u8; 261] = { + let mut b = [0x05; 261]; + /* universal class, primitive type, tag_num = 9 (Data type Real) */ + b[0] = 0x09; + /* length, definite form, 2 octets */ + b[1] = 0x82; + /* length is the sum of the following octets (257): */ + b[2] = 0x01; + b[3] = 0x01; + + b + }; + + &TEST_BUF + }, + DetectAsn1Data { + double_overflow: true, + absolute_offset: Some(0), + ..Default::default() + }, Some(Asn1Check::DoubleOverflow); "Test double_overflow rule (match)" )] + #[test_case("double_overflow, absolute_offset 0", + { + static TEST_BUF: [u8; 261] = { + let mut b = [0x05; 261]; + /* universal class, primitive type, tag_num = 9 (Data type Real) */ + b[0] = 0x09; + /* length, definite form, 2 octets */ + b[1] = 0x82; + /* length is the sum of the following octets (256): */ + b[2] = 0x01; + b[3] = 0x00; + + b + }; + + &TEST_BUF + }, + DetectAsn1Data { + double_overflow: true, + absolute_offset: Some(0), + ..Default::default() + }, None; "Test double_overflow rule (non-match)" )] + fn test_checks( + rule: &str, + asn1_buf: &'static [u8], + expected_data: DetectAsn1Data, + expected_check: Option, + ) { + // Parse rule + let (_rest, ad) = parse_rules::asn1_parse_rule(rule).unwrap(); + assert_eq!(expected_data, ad); + + // Decode + let asn1 = Asn1::from_slice(asn1_buf, &ad).unwrap(); + + // Run checks + let result = asn1.check(&ad).unwrap(); + assert_eq!(expected_check, result); + } +} diff --git a/rust/src/asn1/parse_rules.rs b/rust/src/asn1/parse_rules.rs new file mode 100644 index 0000000000..c1cfae66c0 --- /dev/null +++ b/rust/src/asn1/parse_rules.rs @@ -0,0 +1,275 @@ +/* Copyright (C) 2020 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. + */ + +use crate::log::*; +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::character::complete::{digit1, multispace0, multispace1}; +use nom::combinator::{map_res, opt}; +use nom::sequence::{separated_pair, tuple}; +use nom::IResult; +use std::ffi::CStr; +use std::os::raw::c_char; + +const ASN1_DEFAULT_MAX_FRAMES: u16 = 30; + +/// Parse the asn1 keyword and return a pointer to a `DetectAsn1Data` +/// containing the parsed options, returns null on failure +/// +/// # Safety +/// +/// pointer must be free'd using `rs_detect_asn1_free` +#[no_mangle] +pub(crate) unsafe extern "C" fn rs_detect_asn1_parse(input: *const c_char) -> *mut DetectAsn1Data { + if input.is_null() { + return std::ptr::null_mut(); + } + + let arg = match CStr::from_ptr(input).to_str() { + Ok(arg) => arg, + _ => { + return std::ptr::null_mut(); + } + }; + + match asn1_parse_rule(&arg) { + Ok((_rest, data)) => { + let mut data = data; + + // Get configuration value + if let Some(max_frames) = crate::conf::conf_get("asn1-max-frames") { + if let Ok(v) = max_frames.parse::() { + data.max_frames = v; + } else { + SCLogDebug!("Could not parse asn1-max-frames: {}", max_frames); + }; + } + + Box::into_raw(Box::new(data)) + } + Err(_) => std::ptr::null_mut(), + } +} + +/// Free a `DetectAsn1Data` object allocated by Rust +/// +/// # Safety +/// +/// ptr must be a valid object obtained using `rs_detect_asn1_parse` +#[no_mangle] +pub(crate) unsafe extern "C" fn rs_detect_asn1_free(ptr: *mut DetectAsn1Data) { + if ptr.is_null() { + return; + } + drop(Box::from_raw(ptr)); +} + +/// Struct to hold parsed asn1 keyword options +#[derive(Debug, PartialEq)] +pub(crate) struct DetectAsn1Data { + pub bitstring_overflow: bool, + pub double_overflow: bool, + pub oversize_length: Option, + pub absolute_offset: Option, + pub relative_offset: Option, + pub max_frames: u16, +} + +impl Default for DetectAsn1Data { + fn default() -> DetectAsn1Data { + DetectAsn1Data { + bitstring_overflow: false, + double_overflow: false, + oversize_length: None, + absolute_offset: None, + relative_offset: None, + max_frames: ASN1_DEFAULT_MAX_FRAMES, + } + } +} + +fn parse_u32_number(input: &str) -> IResult<&str, u32> { + map_res(digit1, |digits: &str| digits.parse::())(input) +} +fn parse_i32_number(input: &str) -> IResult<&str, i32> { + let (rest, negate) = opt(tag("-"))(input)?; + let (rest, d) = map_res(digit1, |s: &str| s.parse::())(rest)?; + let n = if negate.is_some() { -1 } else { 1 }; + Ok((rest, d * n)) +} + +/// Parse asn1 keyword options +pub(super) fn asn1_parse_rule(input: &str) -> IResult<&str, DetectAsn1Data> { + // If nothing to parse, return + if input.is_empty() { + return Err(nom::Err::Error(nom::error::make_error( + input, + nom::error::ErrorKind::Eof, + ))); + } + + // Rule parsing functions + fn bitstring_overflow(i: &str) -> IResult<&str, &str> { + tag("bitstring_overflow")(i) + } + + fn double_overflow(i: &str) -> IResult<&str, &str> { + tag("double_overflow")(i) + } + + fn oversize_length(i: &str) -> IResult<&str, (&str, u32)> { + separated_pair(tag("oversize_length"), multispace1, parse_u32_number)(i) + } + + fn absolute_offset(i: &str) -> IResult<&str, (&str, u32)> { + separated_pair(tag("absolute_offset"), multispace1, parse_u32_number)(i) + } + + fn relative_offset(i: &str) -> IResult<&str, (&str, i32)> { + separated_pair(tag("relative_offset"), multispace1, parse_i32_number)(i) + } + + let mut data = DetectAsn1Data::default(); + + let mut rest = input; + + // Parse the input and set data + while !rest.is_empty() { + let ( + new_rest, + ( + _, + bitstring_overflow, + double_overflow, + oversize_length, + absolute_offset, + relative_offset, + _, + ), + ) = tuple(( + opt(multispace0), + opt(bitstring_overflow), + opt(double_overflow), + opt(oversize_length), + opt(absolute_offset), + opt(relative_offset), + opt(alt((multispace1, tag(",")))), + ))(rest)?; + + if bitstring_overflow.is_some() { + data.bitstring_overflow = true; + } else if double_overflow.is_some() { + data.double_overflow = true; + } else if let Some((_, v)) = oversize_length { + data.oversize_length = Some(v); + } else if let Some((_, v)) = absolute_offset { + data.absolute_offset = Some(v); + } else if let Some((_, v)) = relative_offset { + data.relative_offset = Some(v); + } else { + return Err(nom::Err::Error(nom::error::make_error( + rest, + nom::error::ErrorKind::Verify, + ))); + } + + rest = new_rest; + } + + Ok((rest, data)) +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + // Test oversize_length + #[test_case("oversize_length 1024", + DetectAsn1Data { oversize_length: Some(1024), ..Default::default()}; + "check that we parse oversize_length correctly")] + #[test_case("oversize_length", + DetectAsn1Data::default() => panics "Error((\"oversize_length\", Verify))"; + "check that we fail if the needed arg oversize_length is not given")] + // Test absolute_offset + #[test_case("absolute_offset 1024", + DetectAsn1Data { absolute_offset: Some(1024), ..Default::default()}; + "check that we parse absolute_offset correctly")] + #[test_case("absolute_offset", + DetectAsn1Data::default() => panics "Error((\"absolute_offset\", Verify))"; + "check that we fail if the needed arg absolute_offset is not given")] + // Test relative_offset + #[test_case("relative_offset 1024", + DetectAsn1Data { relative_offset: Some(1024), ..Default::default()}; + "check that we parse relative_offset correctly")] + #[test_case("relative_offset", + DetectAsn1Data::default() => panics "Error((\"relative_offset\", Verify))"; + "check that we fail if the needed arg relative_offset is not given")] + // Test bitstring_overflow + #[test_case("bitstring_overflow", + DetectAsn1Data { bitstring_overflow: true, ..Default::default()}; + "check that we parse bitstring_overflow correctly")] + // Test double_overflow + #[test_case("double_overflow", + DetectAsn1Data { double_overflow: true, ..Default::default()}; + "check that we parse double_overflow correctly")] + // Test combination of params + #[test_case("oversize_length 1024, relative_offset 10", + DetectAsn1Data { oversize_length: Some(1024), relative_offset: Some(10), + ..Default::default()}; + "check for combinations of keywords (comma seperated)")] + #[test_case("oversize_length 1024 absolute_offset 10", + DetectAsn1Data { oversize_length: Some(1024), absolute_offset: Some(10), + ..Default::default()}; + "check for combinations of keywords (space seperated)")] + #[test_case("oversize_length 1024 absolute_offset 10, bitstring_overflow", + DetectAsn1Data { bitstring_overflow: true, oversize_length: Some(1024), + absolute_offset: Some(10), ..Default::default()}; + "check for combinations of keywords (space/comma seperated)")] + #[test_case( + "double_overflow, oversize_length 1024 absolute_offset 10,\n bitstring_overflow", + DetectAsn1Data { double_overflow: true, bitstring_overflow: true, + oversize_length: Some(1024), absolute_offset: Some(10), + ..Default::default()}; + "1. check for combinations of keywords (space/comma/newline seperated)")] + #[test_case( + "\n\t double_overflow, oversize_length 1024 relative_offset 10,\n bitstring_overflow", + DetectAsn1Data { double_overflow: true, bitstring_overflow: true, + oversize_length: Some(1024), relative_offset: Some(10), + ..Default::default()}; + "2. check for combinations of keywords (space/comma/newline seperated)")] + // Test empty + #[test_case("", + DetectAsn1Data::default() => panics "Error((\"\", Eof))"; + "test that we break with a empty string")] + // Test invalid rules + #[test_case("oversize_length 1024, some_other_param 360", + DetectAsn1Data::default() => panics "Error((\" some_other_param 360\", Verify))"; + "test that we break on invalid options")] + #[test_case("oversize_length 1024,,", + DetectAsn1Data::default() => panics "Error((\",\", Verify))"; + "test that we break on invalid format (missing option)")] + #[test_case("bitstring_overflowabsolute_offset", + DetectAsn1Data::default() => panics "Error((\"absolute_offset\", Verify))"; + "test that we break on invalid format (missing seperator)")] + fn test_asn1_parse_rule(input: &str, expected: DetectAsn1Data) { + let (rest, res) = asn1_parse_rule(input).unwrap(); + + assert_eq!(0, rest.len()); + assert_eq!(expected, res); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 637c3d1a86..7e742b5d39 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -79,4 +79,5 @@ pub mod rfb; pub mod applayertemplate; pub mod rdp; pub mod x509; +pub mod asn1; pub mod ssh;