From f4da7d0bfe35d016559df140c01b807b4015806a Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 1 Dec 2023 10:51:39 +0100 Subject: [PATCH 01/12] detect: integer keywords now support hexadecimal So that we can write enip.revision: 0x203 Ticket: 6645 --- rust/src/detect/uint.rs | 59 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/rust/src/detect/uint.rs b/rust/src/detect/uint.rs index 3d6a5baab0ca..77b24ca79c4b 100644 --- a/rust/src/detect/uint.rs +++ b/rust/src/detect/uint.rs @@ -17,7 +17,7 @@ use nom7::branch::alt; use nom7::bytes::complete::{is_a, tag, tag_no_case, take_while}; -use nom7::character::complete::digit1; +use nom7::character::complete::{digit1, hex_digit1}; use nom7::combinator::{all_consuming, map_opt, opt, value, verify}; use nom7::error::{make_error, ErrorKind}; use nom7::Err; @@ -73,8 +73,25 @@ pub fn detect_parse_uint_unit(i: &str) -> IResult<&str, u64> { return Ok((i, unit)); } +pub fn detect_parse_uint_value_hex(i: &str) -> IResult<&str, T> { + let (i, _) = tag("0x")(i)?; + let (i, arg1s) = hex_digit1(i)?; + match T::from_str_radix(arg1s, 16) { + Ok(arg1) => Ok((i, arg1)), + _ => Err(Err::Error(make_error(i, ErrorKind::Verify))), + } +} + +pub fn detect_parse_uint_value(i: &str) -> IResult<&str, T> { + let (i, arg1) = alt(( + detect_parse_uint_value_hex, + map_opt(digit1, |s: &str| s.parse::().ok()), + ))(i)?; + Ok((i, arg1)) +} + pub fn detect_parse_uint_with_unit(i: &str) -> IResult<&str, T> { - let (i, arg1) = map_opt(digit1, |s: &str| s.parse::().ok())(i)?; + let (i, arg1) = detect_parse_uint_value::(i)?; let (i, unit) = opt(detect_parse_uint_unit)(i)?; if arg1 >= T::one() { if let Some(u) = unit { @@ -107,11 +124,11 @@ pub fn detect_parse_uint_start_equal( pub fn detect_parse_uint_start_interval( i: &str, ) -> IResult<&str, DetectUintData> { - let (i, arg1) = map_opt(digit1, |s: &str| s.parse::().ok())(i)?; + let (i, arg1) = detect_parse_uint_value(i)?; let (i, _) = opt(is_a(" "))(i)?; let (i, _) = alt((tag("-"), tag("<>")))(i)?; let (i, _) = opt(is_a(" "))(i)?; - let (i, arg2) = verify(map_opt(digit1, |s: &str| s.parse::().ok()), |x| { + let (i, arg2) = verify(detect_parse_uint_value, |x| { x > &arg1 && *x - arg1 > T::one() })(i)?; Ok(( @@ -127,13 +144,13 @@ pub fn detect_parse_uint_start_interval( fn detect_parse_uint_start_interval_inclusive( i: &str, ) -> IResult<&str, DetectUintData> { - let (i, arg1) = verify(map_opt(digit1, |s: &str| s.parse::().ok()), |x| { + let (i, arg1) = verify(detect_parse_uint_value::, |x| { *x > T::min_value() })(i)?; let (i, _) = opt(is_a(" "))(i)?; let (i, _) = alt((tag("-"), tag("<>")))(i)?; let (i, _) = opt(is_a(" "))(i)?; - let (i, arg2) = verify(map_opt(digit1, |s: &str| s.parse::().ok()), |x| { + let (i, arg2) = verify(detect_parse_uint_value::, |x| { *x > arg1 && *x < T::max_value() })(i)?; Ok(( @@ -162,7 +179,7 @@ pub fn detect_parse_uint_mode(i: &str) -> IResult<&str, DetectUintMode> { fn detect_parse_uint_start_symbol(i: &str) -> IResult<&str, DetectUintData> { let (i, mode) = detect_parse_uint_mode(i)?; let (i, _) = opt(is_a(" "))(i)?; - let (i, arg1) = map_opt(digit1, |s: &str| s.parse::().ok())(i)?; + let (i, arg1) = detect_parse_uint_value(i)?; match mode { DetectUintMode::DetectUintModeNe => {} @@ -407,6 +424,34 @@ pub unsafe extern "C" fn rs_detect_u16_free(ctx: &mut DetectUintData) { mod tests { use super::*; + #[test] + fn test_parse_uint_hex() { + match detect_parse_uint::("0x100") { + Ok((_, val)) => { + assert_eq!(val.arg1, 0x100); + } + Err(_) => { + assert!(false); + } + } + match detect_parse_uint::("0xFF") { + Ok((_, val)) => { + assert_eq!(val.arg1, 255); + } + Err(_) => { + assert!(false); + } + } + match detect_parse_uint::("0xff") { + Ok((_, val)) => { + assert_eq!(val.arg1, 255); + } + Err(_) => { + assert!(false); + } + } + } + #[test] fn test_parse_uint_unit() { match detect_parse_uint::(" 2kb") { From 92b3a33d4cd498b4c0c97778e61b707fe3471d34 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 30 Nov 2023 15:18:20 +0100 Subject: [PATCH 02/12] detect: integer keywords now accept negated ranges Ticket: 6646 --- rust/src/detect/uint.rs | 42 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/rust/src/detect/uint.rs b/rust/src/detect/uint.rs index 77b24ca79c4b..cd22693ccc74 100644 --- a/rust/src/detect/uint.rs +++ b/rust/src/detect/uint.rs @@ -17,7 +17,7 @@ use nom7::branch::alt; use nom7::bytes::complete::{is_a, tag, tag_no_case, take_while}; -use nom7::character::complete::{digit1, hex_digit1}; +use nom7::character::complete::{char, digit1, hex_digit1}; use nom7::combinator::{all_consuming, map_opt, opt, value, verify}; use nom7::error::{make_error, ErrorKind}; use nom7::Err; @@ -35,6 +35,7 @@ pub enum DetectUintMode { DetectUintModeGte, DetectUintModeRange, DetectUintModeNe, + DetectUintModeNegRg, } #[derive(Debug)] @@ -124,6 +125,7 @@ pub fn detect_parse_uint_start_equal( pub fn detect_parse_uint_start_interval( i: &str, ) -> IResult<&str, DetectUintData> { + let (i, neg) = opt(char('!'))(i)?; let (i, arg1) = detect_parse_uint_value(i)?; let (i, _) = opt(is_a(" "))(i)?; let (i, _) = alt((tag("-"), tag("<>")))(i)?; @@ -131,12 +133,17 @@ pub fn detect_parse_uint_start_interval( let (i, arg2) = verify(detect_parse_uint_value, |x| { x > &arg1 && *x - arg1 > T::one() })(i)?; + let mode = if neg.is_some() { + DetectUintMode::DetectUintModeNegRg + } else { + DetectUintMode::DetectUintModeRange + }; Ok(( i, DetectUintData { arg1, arg2, - mode: DetectUintMode::DetectUintModeRange, + mode, }, )) } @@ -144,6 +151,7 @@ pub fn detect_parse_uint_start_interval( fn detect_parse_uint_start_interval_inclusive( i: &str, ) -> IResult<&str, DetectUintData> { + let (i, neg) = opt(char('!'))(i)?; let (i, arg1) = verify(detect_parse_uint_value::, |x| { *x > T::min_value() })(i)?; @@ -153,12 +161,17 @@ fn detect_parse_uint_start_interval_inclusive( let (i, arg2) = verify(detect_parse_uint_value::, |x| { *x > arg1 && *x < T::max_value() })(i)?; + let mode = if neg.is_some() { + DetectUintMode::DetectUintModeNegRg + } else { + DetectUintMode::DetectUintModeRange + }; Ok(( i, DetectUintData { arg1: arg1 - T::one(), arg2: arg2 + T::one(), - mode: DetectUintMode::DetectUintModeRange, + mode, }, )) } @@ -255,6 +268,11 @@ pub fn detect_match_uint(x: &DetectUintData, val: T) -> boo return true; } } + DetectUintMode::DetectUintModeNegRg => { + if val <= x.arg1 || val >= x.arg2 { + return true; + } + } } return false; } @@ -452,6 +470,24 @@ mod tests { } } + #[test] + fn test_parse_uint_negated_range() { + match detect_parse_uint::("!1-6") { + Ok((_, val)) => { + assert_eq!(val.arg1, 1); + assert_eq!(val.arg2, 6); + assert_eq!(val.mode, DetectUintMode::DetectUintModeNegRg); + assert!(detect_match_uint(&val, 1)); + assert!(!detect_match_uint(&val, 2)); + assert!(!detect_match_uint(&val, 5)); + assert!(detect_match_uint(&val, 6)); + } + Err(_) => { + assert!(false); + } + } + } + #[test] fn test_parse_uint_unit() { match detect_parse_uint::(" 2kb") { From 5640816b88ac0ff854f9ea068357a010c8fa68b9 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 22 Dec 2023 11:59:35 +0100 Subject: [PATCH 03/12] detect/integer: rust derive for enumerations Ticket: 6647 Allows keywords using integers to use strings in signature parsing based on a rust enumeration with a derive. --- rust/derive/src/lib.rs | 6 +++ rust/derive/src/stringenum.rs | 96 +++++++++++++++++++++++++++++++++++ rust/src/detect/mod.rs | 16 ++++++ rust/src/detect/uint.rs | 17 +++++++ 4 files changed, 135 insertions(+) create mode 100644 rust/derive/src/stringenum.rs diff --git a/rust/derive/src/lib.rs b/rust/derive/src/lib.rs index a2b7a6ad0442..18d7e3faf3ae 100644 --- a/rust/derive/src/lib.rs +++ b/rust/derive/src/lib.rs @@ -23,6 +23,7 @@ use proc_macro::TokenStream; mod applayerevent; mod applayerframetype; +mod stringenum; /// The `AppLayerEvent` derive macro generates a `AppLayerEvent` trait /// implementation for enums that define AppLayerEvents. @@ -50,3 +51,8 @@ pub fn derive_app_layer_event(input: TokenStream) -> TokenStream { pub fn derive_app_layer_frame_type(input: TokenStream) -> TokenStream { applayerframetype::derive_app_layer_frame_type(input) } + +#[proc_macro_derive(EnumStringU8, attributes(name))] +pub fn derive_enum_string_u8(input: TokenStream) -> TokenStream { + stringenum::derive_enum_string::(input, "u8") +} diff --git a/rust/derive/src/stringenum.rs b/rust/derive/src/stringenum.rs new file mode 100644 index 000000000000..0f0905e0116f --- /dev/null +++ b/rust/derive/src/stringenum.rs @@ -0,0 +1,96 @@ +/* Copyright (C) 2023 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. + */ + +extern crate proc_macro; +use super::applayerevent::transform_name; +use proc_macro::TokenStream; +use quote::quote; +use syn::{self, parse_macro_input, DeriveInput}; +use std::str::FromStr; + +pub fn derive_enum_string(input: TokenStream, ustr: &str) -> TokenStream where ::Err: std::fmt::Display { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + let mut values = Vec::new(); + let mut names = Vec::new(); + let mut fields = Vec::new(); + + if let syn::Data::Enum(ref data) = input.data { + for v in (&data.variants).into_iter() { + if let Some((_, val)) = &v.discriminant { + let fname = transform_name(&v.ident.to_string()); + names.push(fname); + fields.push(v.ident.clone()); + if let syn::Expr::Lit(l) = val { + if let syn::Lit::Int(li) = &l.lit { + if let Ok(value) = li.base10_parse::() { + values.push(value); + } else { + panic!("EnumString requires explicit {}", ustr); + } + } else { + panic!("EnumString requires explicit literal integer"); + } + } else { + panic!("EnumString requires explicit literal"); + } + } else { + panic!("EnumString requires explicit values"); + } + } + } else { + panic!("EnumString can only be derived for enums"); + } + + let is_suricata = std::env::var("CARGO_PKG_NAME").map(|var| var == "suricata").unwrap_or(false); + let crate_id = if is_suricata { + syn::Ident::new("crate", proc_macro2::Span::call_site()) + } else { + syn::Ident::new("suricata", proc_macro2::Span::call_site()) + }; + + let utype_str = syn::Ident::new(ustr, proc_macro2::Span::call_site()); + + let expanded = quote! { + impl #crate_id::detect::Enum<#utype_str> for #name { + fn from_u(v: #utype_str) -> Option { + match v { + #( #values => Some(#name::#fields) ,)* + _ => None, + } + } + fn into_u(self) -> #utype_str { + match self { + #( #name::#fields => #values ,)* + } + } + fn to_str(&self) -> &'static str { + match *self { + #( #name::#fields => #names ,)* + } + } + fn from_str(s: &str) -> Option { + match s { + #( #names => Some(#name::#fields) ,)* + _ => None + } + } + } + }; + + proc_macro::TokenStream::from(expanded) +} diff --git a/rust/src/detect/mod.rs b/rust/src/detect/mod.rs index d33c9ae7fabf..38609e797a0a 100644 --- a/rust/src/detect/mod.rs +++ b/rust/src/detect/mod.rs @@ -25,3 +25,19 @@ pub mod stream_size; pub mod uint; pub mod uri; pub mod requires; + +/// Enum trait that will be implemented on enums that +/// derive StringEnum. +pub trait Enum { + /// Return the enum variant of the given numeric value. + fn from_u(v: T) -> Option where Self: Sized; + + /// Convert the enum variant to the numeric value. + fn into_u(self) -> T; + + /// Return the string for logging the enum value. + fn to_str(&self) -> &'static str; + + /// Get an enum variant from parsing a string. + fn from_str(s: &str) -> Option where Self: Sized; +} diff --git a/rust/src/detect/uint.rs b/rust/src/detect/uint.rs index cd22693ccc74..84b01609ea70 100644 --- a/rust/src/detect/uint.rs +++ b/rust/src/detect/uint.rs @@ -23,6 +23,8 @@ use nom7::error::{make_error, ErrorKind}; use nom7::Err; use nom7::IResult; +use super::Enum; + use std::ffi::CStr; #[derive(PartialEq, Eq, Clone, Debug)] @@ -46,6 +48,21 @@ pub struct DetectUintData { pub mode: DetectUintMode, } +pub fn detect_parse_uint_enum>(s: &str) -> Option> { + if let Ok((_, ctx)) = detect_parse_uint::(s) { + return Some(ctx); + } + if let Some(enum_val) = T2::from_str(s) { + let ctx = DetectUintData:: { + arg1: enum_val.into_u(), + arg2: T1::min_value(), + mode: DetectUintMode::DetectUintModeEqual, + }; + return Some(ctx); + } + return None; +} + pub trait DetectIntType: std::str::FromStr + std::cmp::PartialOrd From ea62c3b2408c3a444ed88974f63b4fad05ecb78f Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sat, 30 Dec 2023 21:46:54 +0100 Subject: [PATCH 04/12] detect: integer keywords now accept bitmasks Ticket: 6648 Like &0x40=0x40 to test for a specific bit set --- rust/src/detect/uint.rs | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/rust/src/detect/uint.rs b/rust/src/detect/uint.rs index 84b01609ea70..adac7da68778 100644 --- a/rust/src/detect/uint.rs +++ b/rust/src/detect/uint.rs @@ -38,6 +38,8 @@ pub enum DetectUintMode { DetectUintModeRange, DetectUintModeNe, DetectUintModeNegRg, + DetectUintModeBitmask, + DetectUintModeNegBitmask, } #[derive(Debug)] @@ -165,6 +167,33 @@ pub fn detect_parse_uint_start_interval( )) } +pub fn detect_parse_uint_bitmask( + i: &str, +) -> IResult<&str, DetectUintData> { + let (i, _) = opt(is_a(" "))(i)?; + let (i, _) = tag("&")(i)?; + let (i, _) = opt(is_a(" "))(i)?; + let (i, arg1) = detect_parse_uint_value(i)?; + let (i, _) = opt(is_a(" "))(i)?; + let (i, neg) = opt(tag("!"))(i)?; + let (i, _) = tag("=")(i)?; + let (i, _) = opt(is_a(" "))(i)?; + let (i, arg2) = detect_parse_uint_value(i)?; + let mode = if neg.is_none() { + DetectUintMode::DetectUintModeBitmask + } else { + DetectUintMode::DetectUintModeNegBitmask + }; + Ok(( + i, + DetectUintData { + arg1, + arg2, + mode, + }, + )) +} + fn detect_parse_uint_start_interval_inclusive( i: &str, ) -> IResult<&str, DetectUintData> { @@ -290,6 +319,16 @@ pub fn detect_match_uint(x: &DetectUintData, val: T) -> boo return true; } } + DetectUintMode::DetectUintModeBitmask => { + if val & x.arg1 == x.arg2 { + return true; + } + } + DetectUintMode::DetectUintModeNegBitmask => { + if val & x.arg1 != x.arg2 { + return true; + } + } } return false; } @@ -297,6 +336,7 @@ pub fn detect_match_uint(x: &DetectUintData, val: T) -> boo pub fn detect_parse_uint_notending(i: &str) -> IResult<&str, DetectUintData> { let (i, _) = opt(is_a(" "))(i)?; let (i, uint) = alt(( + detect_parse_uint_bitmask, detect_parse_uint_start_interval, detect_parse_uint_start_equal, detect_parse_uint_start_symbol, @@ -459,6 +499,34 @@ pub unsafe extern "C" fn rs_detect_u16_free(ctx: &mut DetectUintData) { mod tests { use super::*; + #[test] + fn test_parse_uint_bitmask() { + match detect_parse_uint::("&0x40!=0") { + Ok((_, val)) => { + assert_eq!(val.arg1, 0x40); + assert_eq!(val.arg2, 0); + assert_eq!(val.mode, DetectUintMode::DetectUintModeNegBitmask); + assert!(!detect_match_uint(&val, 0xBF)); + assert!(detect_match_uint(&val, 0x40)); + } + Err(_) => { + assert!(false); + } + } + match detect_parse_uint::("&0xc0=0x80") { + Ok((_, val)) => { + assert_eq!(val.arg1, 0xc0); + assert_eq!(val.arg2, 0x80); + assert_eq!(val.mode, DetectUintMode::DetectUintModeBitmask); + assert!(detect_match_uint(&val, 0x80)); + assert!(!detect_match_uint(&val, 0x40)); + assert!(!detect_match_uint(&val, 0xc0)); + } + Err(_) => { + assert!(false); + } + } + } #[test] fn test_parse_uint_hex() { match detect_parse_uint::("0x100") { From c70adb5904b0905a4c39f87f75a8003c4e377124 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 4 Jan 2024 11:00:51 +0100 Subject: [PATCH 05/12] doc: integer keywords Ticket: 6628 Document the generic detection capabilities for integer keywords. and make every integer keyword pointing to this section. --- doc/userguide/rules/dhcp-keywords.rst | 6 ++ doc/userguide/rules/file-keywords.rst | 2 + doc/userguide/rules/flow-keywords.rst | 10 ++++ doc/userguide/rules/header-keywords.rst | 10 ++++ doc/userguide/rules/http-keywords.rst | 2 + doc/userguide/rules/http2-keywords.rst | 6 ++ doc/userguide/rules/ike-keywords.rst | 6 ++ doc/userguide/rules/index.rst | 1 + doc/userguide/rules/integer-keywords.rst | 74 ++++++++++++++++++++++++ doc/userguide/rules/mqtt-keywords.rst | 2 + doc/userguide/rules/payload-keywords.rst | 4 ++ doc/userguide/rules/rfb-keywords.rst | 2 + doc/userguide/rules/tls-keywords.rst | 2 + 13 files changed, 127 insertions(+) create mode 100644 doc/userguide/rules/integer-keywords.rst diff --git a/doc/userguide/rules/dhcp-keywords.rst b/doc/userguide/rules/dhcp-keywords.rst index 05675a947e73..ac8e9c627921 100644 --- a/doc/userguide/rules/dhcp-keywords.rst +++ b/doc/userguide/rules/dhcp-keywords.rst @@ -6,6 +6,8 @@ dhcp.leasetime DHCP lease time (integer). +dhcp.leasetime uses an, :ref:` unsigned 64-bits integer `. + Syntax:: dhcp.leasetime:[op] @@ -25,6 +27,8 @@ dhcp.rebinding_time DHCP rebinding time (integer). +dhcp.rebinding_time uses an, :ref:` unsigned 64-bits integer `. + Syntax:: dhcp.rebinding_time:[op] @@ -44,6 +48,8 @@ dhcp.renewal_time DHCP renewal time (integer). +dhcp.renewal_time uses an, :ref:` unsigned 64-bits integer `. + Syntax:: dhcp.renewal_time:[op] diff --git a/doc/userguide/rules/file-keywords.rst b/doc/userguide/rules/file-keywords.rst index c708ee746c0d..5ba1adeb50fe 100644 --- a/doc/userguide/rules/file-keywords.rst +++ b/doc/userguide/rules/file-keywords.rst @@ -244,6 +244,8 @@ filesize Match on the size of the file as it is being transferred. +filesize uses an, :ref:` unsigned 64-bits integer `. + Syntax:: filesize:; diff --git a/doc/userguide/rules/flow-keywords.rst b/doc/userguide/rules/flow-keywords.rst index 6d451ce82aab..b8422e1e2e29 100644 --- a/doc/userguide/rules/flow-keywords.rst +++ b/doc/userguide/rules/flow-keywords.rst @@ -292,6 +292,8 @@ flow.age Flow age in seconds (integer) This keyword does not wait for the end of the flow, but will be checked at each packet. +flow.age uses an, :ref:` unsigned 32-bits integer `. + Syntax:: flow.age: [op] @@ -314,6 +316,8 @@ flow.pkts_toclient Flow number of packets to client (integer) This keyword does not wait for the end of the flow, but will be checked at each packet. +flow.pkts_toclient uses an, :ref:` unsigned 32-bits integer `. + Syntax:: flow.pkts_toclient: [op] @@ -334,6 +338,8 @@ flow.pkts_toserver Flow number of packets to server (integer) This keyword does not wait for the end of the flow, but will be checked at each packet. +flow.pkts_toserver uses an, :ref:` unsigned 32-bits integer `. + Syntax:: flow.pkts_toserver: [op] @@ -354,6 +360,8 @@ flow.bytes_toclient Flow number of bytes to client (integer) This keyword does not wait for the end of the flow, but will be checked at each packet. +flow.bytes_toclient uses an, :ref:` unsigned 64-bits integer `. + Syntax:: flow.bytes_toclient: [op] @@ -374,6 +382,8 @@ flow.bytes_toserver Flow number of bytes to server (integer) This keyword does not wait for the end of the flow, but will be checked at each packet. +flow.bytes_toserver uses an, :ref:` unsigned 64-bits integer `. + Syntax:: flow.bytes_toserver: [op] diff --git a/doc/userguide/rules/header-keywords.rst b/doc/userguide/rules/header-keywords.rst index 36d1437647f3..fd1809363049 100644 --- a/doc/userguide/rules/header-keywords.rst +++ b/doc/userguide/rules/header-keywords.rst @@ -15,6 +15,8 @@ For example:: ttl:10; +ttl uses an, :ref:` unsigned 8-bits integer `. + At the end of the ttl keyword you can enter the value on which you want to match. The Time-to-live value determines the maximal amount of time a packet can be in the Internet-system. If this field is set @@ -431,6 +433,8 @@ tcp.mss Match on the TCP MSS option value. Will not match if the option is not present. +tcp.mss uses an, :ref:` unsigned 16-bits integer `. + The format of the keyword:: tcp.mss:-; @@ -506,6 +510,8 @@ messages. The different messages are distinct by different names, but more important by numeric values. For more information see the table with message-types and codes. +itype uses an, :ref:` unsigned 8-bits integer `. + The format of the itype keyword:: itype:min<>max; @@ -565,6 +571,8 @@ code of a ICMP message clarifies the message. Together with the ICMP-type it indicates with what kind of problem you are dealing with. A code has a different purpose with every ICMP-type. +icode uses an, :ref:` unsigned 8-bits integer `. + The format of the icode keyword:: icode:min<>max; @@ -719,6 +727,8 @@ icmpv6.mtu Match on the ICMPv6 MTU optional value. Will not match if the MTU is not present. +icmpv6.mtu uses an, :ref:` unsigned 32-bits integer `. + The format of the keyword:: icmpv6.mtu:-; diff --git a/doc/userguide/rules/http-keywords.rst b/doc/userguide/rules/http-keywords.rst index ba0d7621f339..2a9308946090 100644 --- a/doc/userguide/rules/http-keywords.rst +++ b/doc/userguide/rules/http-keywords.rst @@ -237,6 +237,8 @@ The ``urilen`` keyword is used to match on the length of the request URI. It is possible to use the ``<`` and ``>`` operators, which indicate respectively *smaller than* and *larger than*. +urilen uses an, :ref:` unsigned 64-bits integer `. + The format of ``urilen`` is:: urilen:3; diff --git a/doc/userguide/rules/http2-keywords.rst b/doc/userguide/rules/http2-keywords.rst index 1ad83554c6ef..64d64a40c878 100644 --- a/doc/userguide/rules/http2-keywords.rst +++ b/doc/userguide/rules/http2-keywords.rst @@ -31,6 +31,8 @@ http2.priority Match on the value of the HTTP2 priority field present in a PRIORITY or HEADERS frame. +http2.priority uses an, :ref:` unsigned 8-bits integer `. + This keyword takes a numeric argument after a colon and supports additional qualifiers, such as: * ``>`` (greater than) @@ -49,6 +51,8 @@ http2.window Match on the value of the HTTP2 value field present in a WINDOWUPDATE frame. +http2.window uses an, :ref:` unsigned 32-bits integer `. + This keyword takes a numeric argument after a colon and supports additional qualifiers, such as: * ``>`` (greater than) @@ -68,6 +72,8 @@ Match on the size of the HTTP2 Dynamic Headers Table. More information on the protocol can be found here: ``_ +http2.size_update uses an, :ref:` unsigned 64-bits integer `. + This keyword takes a numeric argument after a colon and supports additional qualifiers, such as: * ``>`` (greater than) diff --git a/doc/userguide/rules/ike-keywords.rst b/doc/userguide/rules/ike-keywords.rst index e0d9557bc306..abb1e57a932f 100644 --- a/doc/userguide/rules/ike-keywords.rst +++ b/doc/userguide/rules/ike-keywords.rst @@ -61,6 +61,8 @@ ike.exchtype Match on the value of the Exchange Type. +ike.exchtype uses an, :ref:` unsigned 8-bits integer `. + This keyword takes a numeric argument after a colon and supports additional qualifiers, such as: * ``>`` (greater than) @@ -106,6 +108,8 @@ ike.key_exchange_payload_length Match against the length of the public key exchange payload (e.g. Diffie-Hellman) of the server or client. +ike.key_exchange_payload_length uses an, :ref:` unsigned 32-bits integer `. + This keyword takes a numeric argument after a colon and supports additional qualifiers, such as: * ``>`` (greater than) @@ -138,6 +142,8 @@ ike.nonce_payload_length Match against the length of the nonce of the server or client. +ike.nonce_payload_length uses an, :ref:` unsigned 32-bits integer `. + This keyword takes a numeric argument after a colon and supports additional qualifiers, such as: * ``>`` (greater than) diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index e174c6787bc5..2450f4486be9 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -7,6 +7,7 @@ Suricata Rules meta header-keywords payload-keywords + integer-keywords transforms prefilter-keywords flow-keywords diff --git a/doc/userguide/rules/integer-keywords.rst b/doc/userguide/rules/integer-keywords.rst new file mode 100644 index 000000000000..b97d1c40708b --- /dev/null +++ b/doc/userguide/rules/integer-keywords.rst @@ -0,0 +1,74 @@ +.. _rules-integer-keywords: + +Integer Keywords +================ + +Many keywords will match on an integer value on the network traffic. +These are unsigned integers that can be 8, 16, 32 or 64 bits. + +Simple example:: + + bsize:integer value; + +The integer value can be written as base-10 like ``100`` or as +an hexadecimal value like ``0x64``. + +The most direct exemple is to match for equality, but there are +different modes. + +Comparison modes +---------------- + +Integers can be matched for +* Equality +* Inequality +* Greater than +* Lesser than +* Range +* Negated range +* Bitmask +* Negated Bitmask + +Comparison are strict by default. +That means range between 1 and 4, will match 2 and 3, but not 1 neither 4. + +Examples:: + + bsize:integer value; # equality + bsize:=integer value; # equality + bsize:!integer value; # inequality + bsize:!=integer value; # inequality + bsize:>integer value; # greater than + bsize:>=integer value; # greater than or equal + bsize:`. + The format of the keyword:: mqtt.protocol_version:-; diff --git a/doc/userguide/rules/payload-keywords.rst b/doc/userguide/rules/payload-keywords.rst index 412f7b4fe0e4..7ee283c9b9cd 100644 --- a/doc/userguide/rules/payload-keywords.rst +++ b/doc/userguide/rules/payload-keywords.rst @@ -280,6 +280,8 @@ bsize With the ``bsize`` keyword, you can match on the length of the buffer. This adds precision to the content match, previously this could have been done with ``isdataat``. +bsize uses an, :ref:` unsigned 64-bits integer `. + An optional operator can be specified; if no operator is present, the operator will default to '='. When a relational operator is used, e.g., '<', '>' or '<>' (range), the bsize value will be compared using the relational operator. Ranges are inclusive. @@ -336,6 +338,8 @@ This may be convenient in detecting buffer overflows. dsize cannot be used when using app/streamlayer protocol keywords (i.e. http.uri) +dsize uses an, :ref:` unsigned 16-bits integer `. + Format:: dsize:[<>!]number; || dsize:min<>max; diff --git a/doc/userguide/rules/rfb-keywords.rst b/doc/userguide/rules/rfb-keywords.rst index 628b3d85c563..00a5de0153bd 100644 --- a/doc/userguide/rules/rfb-keywords.rst +++ b/doc/userguide/rules/rfb-keywords.rst @@ -36,6 +36,8 @@ rfb.sectype Match on the value of the RFB security type field, e.g. ``2`` for VNC challenge-response authentication, ``0`` for no authentication, and ``30`` for Apple's custom Remote Desktop authentication. +rfb.sectype uses an, :ref:` unsigned 32-bits integer `. + This keyword takes a numeric argument after a colon and supports additional qualifiers, such as: * ``>`` (greater than) diff --git a/doc/userguide/rules/tls-keywords.rst b/doc/userguide/rules/tls-keywords.rst index dc28c97cd583..855e996a9dbc 100644 --- a/doc/userguide/rules/tls-keywords.rst +++ b/doc/userguide/rules/tls-keywords.rst @@ -284,6 +284,8 @@ tls.cert_chain_len Matches on the TLS certificate chain length. +tls.cert_chain_len uses an, :ref:` unsigned 32-bits integer `. + tls.cert_chain_len supports `<, >, <>, !` and using an exact value. Example:: From 4e439bfacad09797178332656995352347b088ea Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 22 Dec 2023 10:05:17 +0100 Subject: [PATCH 06/12] enip: register on default 44818/tcp port if no config option is found, as is done for udp Ticket: 6304 --- src/app-layer-enip.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app-layer-enip.c b/src/app-layer-enip.c index f059774b6da5..5a7bce13230e 100644 --- a/src/app-layer-enip.c +++ b/src/app-layer-enip.c @@ -599,6 +599,14 @@ void RegisterENIPTCPParsers(void) proto_name, ALPROTO_ENIP, 0, sizeof(ENIPEncapHdr), ENIPProbingParser, ENIPProbingParser)) { + SCLogDebug("no ENIP TCP config found enabling ENIP detection on port 44818."); + + AppLayerProtoDetectPPRegister(IPPROTO_TCP, "44818", ALPROTO_ENIP, 0, + sizeof(ENIPEncapHdr), STREAM_TOSERVER, ENIPProbingParser, NULL); + + AppLayerProtoDetectPPRegister(IPPROTO_TCP, "44818", ALPROTO_ENIP, 0, + sizeof(ENIPEncapHdr), STREAM_TOCLIENT, ENIPProbingParser, NULL); + return; } } From 86186169aec5156f5dac78a56f02c54380705a47 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 22 Dec 2023 15:51:33 +0100 Subject: [PATCH 07/12] http2: add settings from newer RFCs Including the one for websocket over HTTP/2 --- rust/src/http2/parser.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/src/http2/parser.rs b/rust/src/http2/parser.rs index adabeb28c6e4..5982c1d215e1 100644 --- a/rust/src/http2/parser.rs +++ b/rust/src/http2/parser.rs @@ -695,6 +695,8 @@ pub enum HTTP2SettingsId { InitialWindowSize = 4, MaxFrameSize = 5, MaxHeaderListSize = 6, + EnableConnectProtocol = 8, // rfc8441 + NoRfc7540Priorities = 9, // rfc9218 } impl fmt::Display for HTTP2SettingsId { @@ -716,6 +718,8 @@ impl std::str::FromStr for HTTP2SettingsId { "SETTINGS_INITIAL_WINDOW_SIZE" => Ok(HTTP2SettingsId::InitialWindowSize), "SETTINGS_MAX_FRAME_SIZE" => Ok(HTTP2SettingsId::MaxFrameSize), "SETTINGS_MAX_HEADER_LIST_SIZE" => Ok(HTTP2SettingsId::MaxHeaderListSize), + "SETTINGS_ENABLE_CONNECT_PROTOCOL" => Ok(HTTP2SettingsId::EnableConnectProtocol), + "SETTINGS_NO_RFC7540_PRIORITIES" => Ok(HTTP2SettingsId::NoRfc7540Priorities), _ => Err(format!("'{}' is not a valid value for HTTP2SettingsId", s)), } } From 86fdfd701ffdc452f5ad5cbf05e2800ada8f0f8c Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sat, 30 Dec 2023 21:21:21 +0100 Subject: [PATCH 08/12] protodetect: remove unused field port is used in AppLayerProtoDetectProbingParserPort and not in AppLayerProtoDetectProbingParserElement --- src/app-layer-detect-proto.c | 54 ++++++------------------------------ 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/src/app-layer-detect-proto.c b/src/app-layer-detect-proto.c index 690950d34e72..99cf4756ff50 100644 --- a/src/app-layer-detect-proto.c +++ b/src/app-layer-detect-proto.c @@ -67,8 +67,6 @@ typedef struct AppLayerProtoDetectProbingParserElement_ { AppProto alproto; - /* \todo don't really need it. See if you can get rid of it */ - uint16_t port; /* \todo calculate at runtime and get rid of this var */ uint32_t alproto_mask; /* the min length of data that has to be supplied to invoke the parser */ @@ -776,16 +774,12 @@ static void AppLayerProtoDetectProbingParserFree(AppLayerProtoDetectProbingParse SCReturn; } -static AppLayerProtoDetectProbingParserElement * -AppLayerProtoDetectProbingParserElementCreate(AppProto alproto, - uint16_t port, - uint16_t min_depth, - uint16_t max_depth) +static AppLayerProtoDetectProbingParserElement *AppLayerProtoDetectProbingParserElementCreate( + AppProto alproto, uint16_t min_depth, uint16_t max_depth) { AppLayerProtoDetectProbingParserElement *pe = AppLayerProtoDetectProbingParserElementAlloc(); pe->alproto = alproto; - pe->port = port; pe->alproto_mask = AppLayerProtoDetectProbingParserGetMask(alproto); pe->min_depth = min_depth; pe->max_depth = max_depth; @@ -817,7 +811,6 @@ AppLayerProtoDetectProbingParserElementDuplicate(AppLayerProtoDetectProbingParse AppLayerProtoDetectProbingParserElement *new_pe = AppLayerProtoDetectProbingParserElementAlloc(); new_pe->alproto = pe->alproto; - new_pe->port = pe->port; new_pe->alproto_mask = pe->alproto_mask; new_pe->min_depth = pe->min_depth; new_pe->max_depth = pe->max_depth; @@ -860,7 +853,6 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar for ( ; pp_pe != NULL; pp_pe = pp_pe->next) { printf(" alproto: %s\n", AppProtoToString(pp_pe->alproto)); - printf(" port: %"PRIu16 "\n", pp_pe->port); printf(" mask: %"PRIu32 "\n", pp_pe->alproto_mask); printf(" min_depth: %"PRIu32 "\n", pp_pe->min_depth); printf(" max_depth: %"PRIu32 "\n", pp_pe->max_depth); @@ -881,7 +873,6 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar for ( ; pp_pe != NULL; pp_pe = pp_pe->next) { printf(" alproto: %s\n", AppProtoToString(pp_pe->alproto)); - printf(" port: %"PRIu16 "\n", pp_pe->port); printf(" mask: %"PRIu32 "\n", pp_pe->alproto_mask); printf(" min_depth: %"PRIu32 "\n", pp_pe->min_depth); printf(" max_depth: %"PRIu32 "\n", pp_pe->max_depth); @@ -902,35 +893,14 @@ static void AppLayerProtoDetectProbingParserElementAppend(AppLayerProtoDetectPro if (*head_pe == NULL) { *head_pe = new_pe; - goto end; + SCReturn; } - if ((*head_pe)->port == 0) { - if (new_pe->port != 0) { - new_pe->next = *head_pe; - *head_pe = new_pe; - } else { - AppLayerProtoDetectProbingParserElement *temp_pe = *head_pe; - while (temp_pe->next != NULL) - temp_pe = temp_pe->next; - temp_pe->next = new_pe; - } - } else { - AppLayerProtoDetectProbingParserElement *temp_pe = *head_pe; - if (new_pe->port == 0) { - while (temp_pe->next != NULL) - temp_pe = temp_pe->next; - temp_pe->next = new_pe; - } else { - while (temp_pe->next != NULL && temp_pe->next->port != 0) - temp_pe = temp_pe->next; - new_pe->next = temp_pe->next; - temp_pe->next = new_pe; - - } - } + AppLayerProtoDetectProbingParserElement *temp_pe = *head_pe; + while (temp_pe->next != NULL) + temp_pe = temp_pe->next; + temp_pe->next = new_pe; - end: SCReturn; } @@ -1087,9 +1057,7 @@ static void AppLayerProtoDetectInsertNewProbingParser(AppLayerProtoDetectProbing } /* Get a new parser element */ AppLayerProtoDetectProbingParserElement *new_pe = - AppLayerProtoDetectProbingParserElementCreate(alproto, - curr_port->port, - min_depth, max_depth); + AppLayerProtoDetectProbingParserElementCreate(alproto, min_depth, max_depth); if (new_pe == NULL) goto error; curr_pe = new_pe; @@ -2914,9 +2882,6 @@ static int AppLayerProtoDetectPPTestData(AppLayerProtoDetectProbingParser *pp, if (pp_element->alproto != ip_proto[i].port[k].toserver_element[j].alproto) { goto end; } - if (pp_element->port != ip_proto[i].port[k].toserver_element[j].port) { - goto end; - } if (pp_element->alproto_mask != ip_proto[i].port[k].toserver_element[j].alproto_mask) { goto end; } @@ -2938,9 +2903,6 @@ static int AppLayerProtoDetectPPTestData(AppLayerProtoDetectProbingParser *pp, if (pp_element->alproto != ip_proto[i].port[k].toclient_element[j].alproto) { goto end; } - if (pp_element->port != ip_proto[i].port[k].toclient_element[j].port) { - goto end; - } if (pp_element->alproto_mask != ip_proto[i].port[k].toclient_element[j].alproto_mask) { goto end; } From 7fa475565e557666c61af9ba2562faa9876b2b2c Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sat, 30 Dec 2023 21:28:50 +0100 Subject: [PATCH 09/12] protodetect: allows not port-based probing parsers As for WebSocket which is detected only by protocol change. --- src/app-layer-detect-proto.c | 53 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/app-layer-detect-proto.c b/src/app-layer-detect-proto.c index 99cf4756ff50..35fc39ced463 100644 --- a/src/app-layer-detect-proto.c +++ b/src/app-layer-detect-proto.c @@ -86,6 +86,9 @@ typedef struct AppLayerProtoDetectProbingParserElement_ { typedef struct AppLayerProtoDetectProbingParserPort_ { /* the port no for which probing parser(s) are invoked */ uint16_t port; + /* wether to use this probing parser port-based */ + // WebSocket has this set to false as it only works with protocol change + bool use_ports; uint32_t alproto_mask; @@ -441,7 +444,8 @@ static AppLayerProtoDetectProbingParserPort *AppLayerProtoDetectGetProbingParser pp_port = pp->port; while (pp_port != NULL) { - if (pp_port->port == port || pp_port->port == 0) { + // always check use_ports + if ((pp_port->port == port || pp_port->port == 0) && pp_port->use_ports) { break; } pp_port = pp_port->next; @@ -933,12 +937,14 @@ static void AppLayerProtoDetectProbingParserPortAppend(AppLayerProtoDetectProbin goto end; } - if ((*head_port)->port == 0) { + // port == 0 && use_ports is special run on any ports, kept at tail + if ((*head_port)->port == 0 && (*head_port)->use_ports) { new_port->next = *head_port; *head_port = new_port; } else { AppLayerProtoDetectProbingParserPort *temp_port = *head_port; - while (temp_port->next != NULL && temp_port->next->port != 0) { + while (temp_port->next != NULL && + !(temp_port->next->port == 0 && temp_port->next->use_ports)) { temp_port = temp_port->next; } new_port->next = temp_port->next; @@ -950,13 +956,9 @@ static void AppLayerProtoDetectProbingParserPortAppend(AppLayerProtoDetectProbin } static void AppLayerProtoDetectInsertNewProbingParser(AppLayerProtoDetectProbingParser **pp, - uint8_t ipproto, - uint16_t port, - AppProto alproto, - uint16_t min_depth, uint16_t max_depth, - uint8_t direction, - ProbingParserFPtr ProbingParser1, - ProbingParserFPtr ProbingParser2) + uint8_t ipproto, bool use_ports, uint16_t port, AppProto alproto, uint16_t min_depth, + uint16_t max_depth, uint8_t direction, ProbingParserFPtr ProbingParser1, + ProbingParserFPtr ProbingParser2) { SCEnter(); @@ -977,13 +979,15 @@ static void AppLayerProtoDetectInsertNewProbingParser(AppLayerProtoDetectProbing /* get the top level port pp */ AppLayerProtoDetectProbingParserPort *curr_port = curr_pp->port; while (curr_port != NULL) { - if (curr_port->port == port) + // when not use_ports, always insert a new AppLayerProtoDetectProbingParserPort + if (curr_port->port == port && use_ports) break; curr_port = curr_port->next; } if (curr_port == NULL) { AppLayerProtoDetectProbingParserPort *new_port = AppLayerProtoDetectProbingParserPortAlloc(); new_port->port = port; + new_port->use_ports = use_ports; AppLayerProtoDetectProbingParserPortAppend(&curr_pp->port, new_port); curr_port = new_port; if (direction & STREAM_TOSERVER) { @@ -995,7 +999,8 @@ static void AppLayerProtoDetectInsertNewProbingParser(AppLayerProtoDetectProbing AppLayerProtoDetectProbingParserPort *zero_port; zero_port = curr_pp->port; - while (zero_port != NULL && zero_port->port != 0) { + // get special run on any port if any, to add it to this port + while (zero_port != NULL && !(zero_port->port == 0 && zero_port->use_ports)) { zero_port = zero_port->next; } if (zero_port != NULL) { @@ -1091,9 +1096,10 @@ static void AppLayerProtoDetectInsertNewProbingParser(AppLayerProtoDetectProbing } AppLayerProtoDetectProbingParserElementAppend(head_pe, new_pe); - if (curr_port->port == 0) { + // when adding special run on any port, add it on all existing ones + if (curr_port->port == 0 && curr_port->use_ports) { AppLayerProtoDetectProbingParserPort *temp_port = curr_pp->port; - while (temp_port != NULL && temp_port->port != 0) { + while (temp_port != NULL && !(temp_port->port == 0 && temp_port->use_ports)) { if (direction & STREAM_TOSERVER) { if (temp_port->dp == NULL) temp_port->dp_max_depth = curr_pe->max_depth; @@ -1121,7 +1127,7 @@ static void AppLayerProtoDetectInsertNewProbingParser(AppLayerProtoDetectProbing } temp_port = temp_port->next; } /* while */ - } /* if */ + } /* if */ error: SCReturn; @@ -1542,6 +1548,13 @@ void AppLayerProtoDetectPPRegister(uint8_t ipproto, SCEnter(); DetectPort *head = NULL; + if (portstr == NULL) { + // WebSocket has a probing parser, but no port + // as it works only on HTTP1 protocol upgrade + AppLayerProtoDetectInsertNewProbingParser(&alpd_ctx.ctx_pp, ipproto, false, 0, alproto, + min_depth, max_depth, direction, ProbingParser1, ProbingParser2); + return; + } DetectPortParse(NULL,&head, portstr); DetectPort *temp_dp = head; while (temp_dp != NULL) { @@ -1549,14 +1562,8 @@ void AppLayerProtoDetectPPRegister(uint8_t ipproto, if (port == 0 && temp_dp->port2 != 0) port++; for (;;) { - AppLayerProtoDetectInsertNewProbingParser(&alpd_ctx.ctx_pp, - ipproto, - port, - alproto, - min_depth, max_depth, - direction, - ProbingParser1, - ProbingParser2); + AppLayerProtoDetectInsertNewProbingParser(&alpd_ctx.ctx_pp, ipproto, true, port, + alproto, min_depth, max_depth, direction, ProbingParser1, ProbingParser2); if (port == temp_dp->port2) { break; } else { From 704e34fa7c8e1d172a3bccab6c89258bbc2bd933 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sat, 30 Dec 2023 21:36:07 +0100 Subject: [PATCH 10/12] protodetect: run expected probing parser When there is a protocol change, and a specific protocol is expected, like WebSeocket, always run it, no matter the port. --- src/app-layer-detect-proto.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app-layer-detect-proto.c b/src/app-layer-detect-proto.c index 35fc39ced463..c47a437659fd 100644 --- a/src/app-layer-detect-proto.c +++ b/src/app-layer-detect-proto.c @@ -581,7 +581,10 @@ static AppProto AppLayerProtoDetectPPGetProto(Flow *f, const uint8_t *buf, uint3 } } - if (dir == STREAM_TOSERVER && f->alproto_tc != ALPROTO_UNKNOWN) { + if (f->alproto_expect != ALPROTO_UNKNOWN) { + // needed for websocket which does not use ports + pe0 = AppLayerProtoDetectGetProbingParser(alpd_ctx.ctx_pp, ipproto, f->alproto_expect); + } else if (dir == STREAM_TOSERVER && f->alproto_tc != ALPROTO_UNKNOWN) { pe0 = AppLayerProtoDetectGetProbingParser(alpd_ctx.ctx_pp, ipproto, f->alproto_tc); } else if (dir == STREAM_TOCLIENT && f->alproto_ts != ALPROTO_UNKNOWN) { pe0 = AppLayerProtoDetectGetProbingParser(alpd_ctx.ctx_pp, ipproto, f->alproto_ts); From 1b1fc8bff91cc0a363314e23dd0cc57ca8db38df Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 22 Dec 2023 12:01:33 +0100 Subject: [PATCH 11/12] app-layer: websockets protocol support Ticket: 2695 --- doc/userguide/rules/index.rst | 1 + doc/userguide/rules/intro.rst | 1 + doc/userguide/rules/websocket-keywords.rst | 57 +++ etc/schema.json | 24 ++ rules/Makefile.am | 3 +- rules/websocket-events.rules | 8 + rust/src/lib.rs | 1 + rust/src/websocket/detect.rs | 133 +++++++ rust/src/websocket/logger.rs | 45 +++ rust/src/websocket/mod.rs | 23 ++ rust/src/websocket/parser.rs | 94 +++++ rust/src/websocket/websocket.rs | 382 +++++++++++++++++++++ src/Makefile.am | 4 + src/app-layer-htp.c | 49 ++- src/app-layer-parser.c | 1 + src/app-layer-protos.c | 1 + src/app-layer-protos.h | 1 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 4 + src/detect-websocket.c | 251 ++++++++++++++ src/detect-websocket.h | 29 ++ src/output-json-websocket.c | 160 +++++++++ src/output-json-websocket.h | 29 ++ src/output.c | 4 + suricata.yaml.in | 5 + 25 files changed, 1296 insertions(+), 16 deletions(-) create mode 100644 doc/userguide/rules/websocket-keywords.rst create mode 100644 rules/websocket-events.rules create mode 100644 rust/src/websocket/detect.rs create mode 100644 rust/src/websocket/logger.rs create mode 100644 rust/src/websocket/mod.rs create mode 100644 rust/src/websocket/parser.rs create mode 100644 rust/src/websocket/websocket.rs create mode 100644 src/detect-websocket.c create mode 100644 src/detect-websocket.h create mode 100644 src/output-json-websocket.c create mode 100644 src/output-json-websocket.h diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index 2450f4486be9..2bd2a2d0ea48 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -36,6 +36,7 @@ Suricata Rules quic-keywords nfs-keywords smtp-keywords + websocket-keywords app-layer xbits thresholding diff --git a/doc/userguide/rules/intro.rst b/doc/userguide/rules/intro.rst index ab35f8a311ca..e2e9da58b610 100644 --- a/doc/userguide/rules/intro.rst +++ b/doc/userguide/rules/intro.rst @@ -110,6 +110,7 @@ you can pick from. These are: * snmp * tftp * sip +* websocket The availability of these protocols depends on whether the protocol is enabled in the configuration file, suricata.yaml. diff --git a/doc/userguide/rules/websocket-keywords.rst b/doc/userguide/rules/websocket-keywords.rst new file mode 100644 index 000000000000..e9f3104e85c3 --- /dev/null +++ b/doc/userguide/rules/websocket-keywords.rst @@ -0,0 +1,57 @@ +WebSocket Keywords +================== + +websocket.payload +----------------- + +A sticky buffer on the unmasked payload, +limited by suricata.yaml config value ``websocket.max-payload-size``. + +Examples:: + + websocket.payload; pcre:"/^123[0-9]*/"; + websocket.payload content:"swordfish"; + +``websocket.payload`` is a 'sticky buffer' and can be used as ``fast_pattern``. + +websocket.flags +--------------- + +Matches on the websocket flags. +It uses a 8-bit unsigned integer as value. +Only the four upper bits are used. + +The value can also be a list of strings (comma-separated), +where each string is the name of a specific bit like `fin` and `comp`, +and can be prefixed by `!` for negation. + +Examples:: + + websocket.flags:128; + websocket.flags:&0x40=0x40; + websocket.flags:fin,!comp; + +websocket.mask +-------------- + +Matches on the websocket mask if any. +It uses a 32-bit unsigned integer as value (big-endian). + +Examples:: + + websocket.mask:123456; + websocket.mask:>0; + +websocket.opcode +---------------- + +Matches on the websocket opcode. +It uses a 8-bit unsigned integer as value. +Only 16 values are relevant. +It can also be specified by text from the enumeration + +Examples:: + + websocket.opcode:1; + websocket.opcode:>8; + websocket.opcode:ping; diff --git a/etc/schema.json b/etc/schema.json index 0756acd00800..e78039d7b62c 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -3833,6 +3833,9 @@ }, "tls": { "$ref": "#/$defs/stats_applayer_error" + }, + "websocket": { + "$ref": "#/$defs/stats_applayer_error" } }, "additionalProperties": false @@ -3950,6 +3953,9 @@ }, "tls": { "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -4061,6 +4067,9 @@ }, "tls": { "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -5501,6 +5510,21 @@ } }, "additionalProperties": false + }, + "websocket": { + "type": "object", + "properties": { + "fin": { + "type": "boolean" + }, + "mask": { + "type": "integer" + }, + "opcode": { + "type": "string" + } + }, + "additionalProperties": false } }, "$defs": { diff --git a/rules/Makefile.am b/rules/Makefile.am index d0ea6eda622f..cba0aa370af3 100644 --- a/rules/Makefile.am +++ b/rules/Makefile.am @@ -22,4 +22,5 @@ smb-events.rules \ smtp-events.rules \ ssh-events.rules \ stream-events.rules \ -tls-events.rules +tls-events.rules \ +websocket-events.rules diff --git a/rules/websocket-events.rules b/rules/websocket-events.rules new file mode 100644 index 000000000000..3acc21132e0a --- /dev/null +++ b/rules/websocket-events.rules @@ -0,0 +1,8 @@ +# WebSocket app-layer event rules. +# +# These SIDs fall in the 2235000+ range. See: +# http://doc.emergingthreats.net/bin/view/Main/SidAllocation and +# https://redmine.openinfosecfoundation.org/projects/suricata/wiki/AppLayer + +alert websocket any any -> any any (msg:"SURICATA Websocket skipped end of payload"; app-layer-event:websocket.skip_end_of_payload; classtype:protocol-command-decode; sid:2235000; rev:1;) +alert websocket any any -> any any (msg:"SURICATA Websocket reassembly limit reached"; app-layer-event:websocket.reassembly_limit_reached; classtype:protocol-command-decode; sid:2235001; rev:1;) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 15e21c4057d1..c49fd1894dbb 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -106,6 +106,7 @@ pub mod rfb; pub mod mqtt; pub mod pgsql; pub mod telnet; +pub mod websocket; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/rust/src/websocket/detect.rs b/rust/src/websocket/detect.rs new file mode 100644 index 000000000000..6088bcfa2024 --- /dev/null +++ b/rust/src/websocket/detect.rs @@ -0,0 +1,133 @@ +/* Copyright (C) 2023 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 super::websocket::WebSocketTransaction; +use crate::detect::uint::{detect_parse_uint, detect_parse_uint_enum, DetectUintData, DetectUintMode}; +use crate::websocket::parser::WebSocketOpcode; + +use nom7::branch::alt; +use nom7::bytes::complete::{is_a, tag}; +use nom7::combinator::{opt, value}; +use nom7::multi::many1; +use nom7::IResult; + +use std::ffi::CStr; + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetOpcode(tx: &mut WebSocketTransaction) -> u8 { + return tx.pdu.opcode; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetFlags(tx: &mut WebSocketTransaction) -> u8 { + return tx.pdu.flags; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetPayload( + tx: &WebSocketTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> bool { + *buffer = tx.pdu.payload.as_ptr(); + *buffer_len = tx.pdu.payload.len() as u32; + return true; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetMask( + tx: &mut WebSocketTransaction, value: *mut u32, +) -> bool { + if let Some(xorkey) = tx.pdu.mask { + *value = xorkey; + return true; + } + return false; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketParseOpcode( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = detect_parse_uint_enum::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +struct WebSocketFlag { + neg: bool, + value: u8, +} + +fn parse_flag_list_item(s: &str) -> IResult<&str, WebSocketFlag> { + let (s, _) = opt(is_a(" "))(s)?; + let (s, neg) = opt(tag("!"))(s)?; + let neg = neg.is_some(); + let (s, value) = alt((value(0x80, tag("fin")), value(0x40, tag("comp"))))(s)?; + let (s, _) = opt(is_a(" ,"))(s)?; + Ok((s, WebSocketFlag { neg, value })) +} + +fn parse_flag_list(s: &str) -> IResult<&str, Vec> { + return many1(parse_flag_list_item)(s); +} + +fn parse_flags(s: &str) -> Option> { + // try first numerical value + if let Ok((_, ctx)) = detect_parse_uint::(s) { + return Some(ctx); + } + // otherwise, try strings for bitmask + if let Ok((_, l)) = parse_flag_list(s) { + let mut arg1 = 0; + let mut arg2 = 0; + for elem in l.iter() { + if elem.value & arg1 != 0 { + SCLogWarning!("Repeated bitflag for websocket.flags"); + return None; + } + arg1 |= elem.value; + if !elem.neg { + arg2 |= elem.value; + } + } + let ctx = DetectUintData:: { + arg1, + arg2, + mode: DetectUintMode::DetectUintModeBitmask, + }; + return Some(ctx); + } + return None; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketParseFlags( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = parse_flags(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} diff --git a/rust/src/websocket/logger.rs b/rust/src/websocket/logger.rs new file mode 100644 index 000000000000..794ee1bac25d --- /dev/null +++ b/rust/src/websocket/logger.rs @@ -0,0 +1,45 @@ +/* Copyright (C) 2023 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 super::parser::WebSocketOpcode; +use super::websocket::WebSocketTransaction; +use crate::detect::Enum; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; + +fn log_websocket(tx: &WebSocketTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("websocket")?; + js.set_bool("fin", tx.pdu.fin)?; + if let Some(xorkey) = tx.pdu.mask { + js.set_uint("mask", xorkey.into())?; + } + if let Some(opcode) = WebSocketOpcode::from_u(tx.pdu.opcode) { + js.set_string("opcode", opcode.to_str())?; + } else { + js.set_string("opcode", &format!("unknown-{}", tx.pdu.opcode))?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, WebSocketTransaction); + log_websocket(tx, js).is_ok() +} diff --git a/rust/src/websocket/mod.rs b/rust/src/websocket/mod.rs new file mode 100644 index 000000000000..c57660f2a44b --- /dev/null +++ b/rust/src/websocket/mod.rs @@ -0,0 +1,23 @@ +/* Copyright (C) 2023 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. + */ + +//! Application layer websocket parser and logger module. + +pub mod detect; +pub mod logger; +mod parser; +pub mod websocket; diff --git a/rust/src/websocket/parser.rs b/rust/src/websocket/parser.rs new file mode 100644 index 000000000000..dac2adde1fca --- /dev/null +++ b/rust/src/websocket/parser.rs @@ -0,0 +1,94 @@ +/* Copyright (C) 2023 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 nom7::bytes::streaming::take; +use nom7::combinator::cond; +use nom7::number::streaming::{be_u16, be_u32, be_u64, be_u8}; +use nom7::IResult; +use suricata_derive::EnumStringU8; + +#[derive(Clone, Debug, Default, EnumStringU8)] +#[repr(u8)] +pub enum WebSocketOpcode { + #[default] + Continuation = 0, + Text = 1, + Binary = 2, + Ping = 8, + Pong = 9, +} + +#[derive(Clone, Debug, Default)] +pub struct WebSocketPdu { + pub flags: u8, + pub fin: bool, + pub compress: bool, + pub opcode: u8, + pub mask: Option, + pub payload: Vec, + pub to_skip: u64, +} + +// cf rfc6455#section-5.2 +pub fn parse_message(i: &[u8], max_pl_size: u64) -> IResult<&[u8], WebSocketPdu> { + let (i, flags_op) = be_u8(i)?; + let fin = (flags_op & 0x80) != 0; + let compress = (flags_op & 0x40) != 0; + let flags = flags_op & 0xF0; + let opcode = flags_op & 0xF; + let (i, mask_plen) = be_u8(i)?; + let mask_flag = (mask_plen & 0x80) != 0; + let (i, payload_len) = match mask_plen & 0x7F { + 126 => { + let (i, val) = be_u16(i)?; + Ok((i, val.into())) + } + 127 => be_u64(i), + _ => Ok((i, (mask_plen & 0x7F).into())), + }?; + let (i, xormask) = cond(mask_flag, take(4usize))(i)?; + let mask = if mask_flag { + let (_, m) = be_u32(xormask.unwrap())?; + Some(m) + } else { + None + }; + let (to_skip, payload_len) = if payload_len < max_pl_size { + (0, payload_len) + } else { + (payload_len - max_pl_size, max_pl_size) + }; + let (i, payload_raw) = take(payload_len)(i)?; + let mut payload = payload_raw.to_vec(); + if let Some(xorkey) = xormask { + for i in 0..payload.len() { + payload[i] ^= xorkey[i % 4]; + } + } + Ok(( + i, + WebSocketPdu { + flags, + fin, + compress, + opcode, + mask, + payload, + to_skip, + }, + )) +} diff --git a/rust/src/websocket/websocket.rs b/rust/src/websocket/websocket.rs new file mode 100644 index 000000000000..4722c953ad4b --- /dev/null +++ b/rust/src/websocket/websocket.rs @@ -0,0 +1,382 @@ +/* Copyright (C) 2023 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 super::parser; +use crate::applayer::{self, *}; +use crate::conf::conf_get; +use crate::core::{AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use crate::frames::Frame; + +use nom7 as nom; +use nom7::Needed; + +use flate2::read::DeflateDecoder; + +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::io::Read; +use std::os::raw::{c_char, c_int, c_void}; + +static mut ALPROTO_WEBSOCKET: AppProto = ALPROTO_UNKNOWN; + +static mut WEBSOCKET_MAX_PAYLOAD_SIZE: u64 = 0xFFFF; + +// app-layer-frame-documentation tag start: FrameType enum +#[derive(AppLayerFrameType)] +pub enum WebSocketFrameType { + Header, + Pdu, +} + +#[derive(AppLayerEvent)] +pub enum WebSocketEvent { + SkipEndOfPayload, + ReassemblyLimitReached, +} + +#[derive(Default)] +pub struct WebSocketTransaction { + tx_id: u64, + pub pdu: parser::WebSocketPdu, + tx_data: AppLayerTxData, +} + +impl WebSocketTransaction { + pub fn new(direction: Direction) -> WebSocketTransaction { + Self { + tx_data: AppLayerTxData::for_direction(direction), + ..Default::default() + } + } +} + +impl Transaction for WebSocketTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +struct WebSocketReassemblyBuffer { + data: Vec, + compress: bool, +} + +#[derive(Default)] +pub struct WebSocketState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + + c2s_buf: WebSocketReassemblyBuffer, + s2c_buf: WebSocketReassemblyBuffer, + + to_skip_tc: u64, + to_skip_ts: u64, +} + +impl State for WebSocketState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&WebSocketTransaction> { + self.transactions.get(index) + } +} + +impl WebSocketState { + pub fn new() -> Self { + Default::default() + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&WebSocketTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self, direction: Direction) -> WebSocketTransaction { + let mut tx = WebSocketTransaction::new(direction); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn parse( + &mut self, stream_slice: StreamSlice, direction: Direction, flow: *const Flow, + ) -> AppLayerResult { + let to_skip = if direction == Direction::ToClient { + &mut self.to_skip_tc + } else { + &mut self.to_skip_ts + }; + let input = stream_slice.as_slice(); + let mut start = input; + if *to_skip > 0 { + if *to_skip >= input.len() as u64 { + *to_skip -= input.len() as u64; + return AppLayerResult::ok(); + } else { + start = &input[*to_skip as usize..]; + *to_skip = 0; + } + } + + let max_pl_size = unsafe { WEBSOCKET_MAX_PAYLOAD_SIZE }; + while !start.is_empty() { + match parser::parse_message(start, max_pl_size) { + Ok((rem, pdu)) => { + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len() - pdu.payload.len()) as i64, + WebSocketFrameType::Header as u8, + ); + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len()) as i64, + WebSocketFrameType::Pdu as u8, + ); + start = rem; + let mut tx = self.new_tx(direction); + if pdu.to_skip > 0 { + if direction == Direction::ToClient { + self.to_skip_tc = pdu.to_skip; + } else { + self.to_skip_ts = pdu.to_skip; + } + tx.tx_data.set_event(WebSocketEvent::SkipEndOfPayload as u8); + } + let buf = if direction == Direction::ToClient { + &mut self.s2c_buf + } else { + &mut self.c2s_buf + }; + if !buf.data.is_empty() || !pdu.fin { + if buf.data.is_empty() { + buf.compress = pdu.compress; + } + if buf.data.len() + pdu.payload.len() < max_pl_size as usize { + buf.data.extend(&pdu.payload); + } else if buf.data.len() < max_pl_size as usize { + buf.data + .extend(&pdu.payload[..max_pl_size as usize - buf.data.len()]); + tx.tx_data + .set_event(WebSocketEvent::ReassemblyLimitReached as u8); + } + } + tx.pdu = pdu; + if tx.pdu.fin && !buf.data.is_empty() { + // the final PDU gets the full reassembled payload + std::mem::swap(&mut tx.pdu.payload, &mut buf.data); + buf.data.clear(); + } + if buf.compress && tx.pdu.fin { + buf.compress = false; + // cf RFC 7692 section-7.2.2 + tx.pdu.payload.extend_from_slice(&[0, 0, 0xFF, 0xFF]); + let mut deflater = DeflateDecoder::new(&tx.pdu.payload[..]); + let mut v = Vec::new(); + // do not check result because + // deflate with rust backend fails on good input cf https://github.com/rust-lang/flate2-rs/issues/389 + let _ = deflater.read_to_end(&mut v); + if !v.is_empty() { + std::mem::swap(&mut tx.pdu.payload, &mut v); + } + } + self.transactions.push_back(tx); + } + Err(nom::Err::Incomplete(needed)) => { + if let Needed::Size(n) = needed { + let n = usize::from(n); + // Not enough data. just ask for one more byte. + let consumed = input.len() - start.len(); + let needed = start.len() + n; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + return AppLayerResult::err(); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + // Input was fully consumed. + return AppLayerResult::ok(); + } +} + +// C exports. + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + if !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if !slice.is_empty() { + // just check reserved bits are zeroed, except RSV1 + // as RSV1 is used for compression cf RFC 7692 + if slice[0] & 0x30 == 0 { + return ALPROTO_WEBSOCKET; + } + return ALPROTO_FAILED; + } + } + return ALPROTO_UNKNOWN; +} + +extern "C" fn rs_websocket_state_new( + _orig_state: *mut c_void, _orig_proto: AppProto, +) -> *mut c_void { + let state = WebSocketState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn rs_websocket_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut WebSocketState)); +} + +unsafe extern "C" fn rs_websocket_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, WebSocketState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn rs_websocket_parse_request( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, WebSocketState); + state.parse(stream_slice, Direction::ToServer, flow) +} + +unsafe extern "C" fn rs_websocket_parse_response( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, WebSocketState); + state.parse(stream_slice, Direction::ToClient, flow) +} + +unsafe extern "C" fn rs_websocket_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, WebSocketState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn rs_websocket_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, WebSocketState); + return state.tx_id; +} + +unsafe extern "C" fn rs_websocket_tx_get_alstate_progress( + _tx: *mut c_void, _direction: u8, +) -> c_int { + return 1; +} + +export_tx_data_get!(rs_websocket_get_tx_data, WebSocketTransaction); +export_state_data_get!(rs_websocket_get_state_data, WebSocketState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"websocket\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_register_parser() { + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: std::ptr::null(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_websocket_probing_parser), + probe_tc: Some(rs_websocket_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_websocket_state_new, + state_free: rs_websocket_state_free, + tx_free: rs_websocket_state_tx_free, + parse_ts: rs_websocket_parse_request, + parse_tc: rs_websocket_parse_response, + get_tx_count: rs_websocket_state_get_tx_count, + get_tx: rs_websocket_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_websocket_tx_get_alstate_progress, + get_eventinfo: Some(WebSocketEvent::get_event_info), + get_eventinfo_byid: Some(WebSocketEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some( + applayer::state_get_tx_iterator::, + ), + get_tx_data: rs_websocket_get_tx_data, + get_state_data: rs_websocket_get_state_data, + apply_tx_config: None, + flags: 0, // do not accept gaps as there is no good way to resync + truncate: None, + get_frame_id_by_name: Some(WebSocketFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(WebSocketFrameType::ffi_name_from_id), + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_WEBSOCKET = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + SCLogDebug!("Rust websocket parser registered."); + if let Some(val) = conf_get("app-layer.protocols.websocket.max-payload-size") { + if let Ok(v) = val.parse::() { + WEBSOCKET_MAX_PAYLOAD_SIZE = v; + } else { + SCLogError!("Invalid value for websocket.max-payload-size"); + } + } + } else { + SCLogDebug!("Protocol detector and parser disabled for WEBSOCKET."); + } +} diff --git a/src/Makefile.am b/src/Makefile.am index 133ed47cd1e8..58727a0f0464 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -358,6 +358,7 @@ noinst_HEADERS = \ detect-urilen.h \ detect-within.h \ detect-xbits.h \ + detect-websocket.h \ device-storage.h \ feature.h \ flow-bit.h \ @@ -431,6 +432,7 @@ noinst_HEADERS = \ output-json-snmp.h \ output-json-ssh.h \ output-json-stats.h \ + output-json-websocket.h \ output-json-template.h \ output-json-tftp.h \ output-json-tls.h \ @@ -973,6 +975,7 @@ libsuricata_c_a_SOURCES = \ detect-urilen.c \ detect-within.c \ detect-xbits.c \ + detect-websocket.c \ device-storage.c \ feature.c \ flow-bit.c \ @@ -1045,6 +1048,7 @@ libsuricata_c_a_SOURCES = \ output-json-snmp.c \ output-json-ssh.c \ output-json-stats.c \ + output-json-websocket.c \ output-json-template.c \ output-json-tftp.c \ output-json-tls.c \ diff --git a/src/app-layer-htp.c b/src/app-layer-htp.c index 5d48611812c1..5ccbac83cf1c 100644 --- a/src/app-layer-htp.c +++ b/src/app-layer-htp.c @@ -53,6 +53,7 @@ #include "app-layer-protos.h" #include "app-layer-parser.h" +#include "app-layer-expectation.h" #include "app-layer.h" #include "app-layer-detect-proto.h" @@ -975,11 +976,7 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa if (tx != NULL && tx->response_status_number == 101) { htp_header_t *h = (htp_header_t *)htp_table_get_c(tx->response_headers, "Upgrade"); - if (h == NULL || bstr_cmp_c(h->value, "h2c") != 0) { - break; - } - if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) { - // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode + if (h == NULL) { break; } uint16_t dp = 0; @@ -987,17 +984,39 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa dp = (uint16_t)tx->request_port_number; } consumed = htp_connp_res_data_consumed(hstate->connp); - hstate->slice = NULL; - if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) { - HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, - HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); - } - // During HTTP2 upgrade, we may consume the HTTP1 part of the data - // and we need to parser the remaining part with HTTP2 - if (consumed > 0 && consumed < input_len) { - SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + if (bstr_cmp_c(h->value, "h2c") == 0) { + if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) { + // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode + break; + } + hstate->slice = NULL; + if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) { + HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, + HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); + } + // During HTTP2 upgrade, we may consume the HTTP1 part of the data + // and we need to parser the remaining part with HTTP2 + if (consumed > 0 && consumed < input_len) { + SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + } + SCReturnStruct(APP_LAYER_OK); + } else if (bstr_cmp_c_nocase(h->value, "WebSocket") == 0) { + if (AppLayerProtoDetectGetProtoName(ALPROTO_WEBSOCKET) == NULL) { + // if WS is disabled, keep the HTP_STREAM_TUNNEL mode + break; + } + hstate->slice = NULL; + if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_WEBSOCKET)) { + HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, + HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); + } + // During WS upgrade, we may consume the HTTP1 part of the data + // and we need to parser the remaining part with WS + if (consumed > 0 && consumed < input_len) { + SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + } + SCReturnStruct(APP_LAYER_OK); } - SCReturnStruct(APP_LAYER_OK); } break; default: diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 96fc607fd257..5cbe10978915 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1763,6 +1763,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterSNMPParsers(); RegisterSIPParsers(); RegisterQuicParsers(); + rs_websocket_register_parser(); rs_template_register_parser(); RegisterRFBParsers(); SCMqttRegisterParser(); diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index 368efacd88d7..b6e1b73d08d4 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -60,6 +60,7 @@ const AppProtoStringTuple AppProtoStrings[ALPROTO_MAX] = { { ALPROTO_MQTT, "mqtt" }, { ALPROTO_PGSQL, "pgsql" }, { ALPROTO_TELNET, "telnet" }, + { ALPROTO_WEBSOCKET, "websocket" }, { ALPROTO_TEMPLATE, "template" }, { ALPROTO_RDP, "rdp" }, { ALPROTO_HTTP2, "http2" }, diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index dd372550cbf5..5ecc5d88d31a 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -56,6 +56,7 @@ enum AppProtoEnum { ALPROTO_MQTT, ALPROTO_PGSQL, ALPROTO_TELNET, + ALPROTO_WEBSOCKET, ALPROTO_TEMPLATE, ALPROTO_RDP, ALPROTO_HTTP2, diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 9f37e0945544..06465b49cfd1 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -237,6 +237,7 @@ #include "detect-quic-version.h" #include "detect-quic-cyu-hash.h" #include "detect-quic-cyu-string.h" +#include "detect-websocket.h" #include "detect-bypass.h" #include "detect-ftpdata.h" @@ -700,6 +701,7 @@ void SigTableSetup(void) DetectQuicVersionRegister(); DetectQuicCyuHashRegister(); DetectQuicCyuStringRegister(); + DetectWebsocketRegister(); DetectBypassRegister(); DetectConfigRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 9dd01f5fd487..7ce625cd59c6 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -316,6 +316,10 @@ enum DetectKeywordId { DETECT_AL_QUIC_UA, DETECT_AL_QUIC_CYU_HASH, DETECT_AL_QUIC_CYU_STRING, + DETECT_WEBSOCKET_MASK, + DETECT_WEBSOCKET_OPCODE, + DETECT_WEBSOCKET_FLAGS, + DETECT_WEBSOCKET_PAYLOAD, DETECT_BYPASS, diff --git a/src/detect-websocket.c b/src/detect-websocket.c new file mode 100644 index 000000000000..91f650c85c55 --- /dev/null +++ b/src/detect-websocket.c @@ -0,0 +1,251 @@ +/* Copyright (C) 2023 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. + */ + +/** + * \file + * + * \author Philippe Antoine + */ + +#include "suricata-common.h" +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-content-inspection.h" +#include "detect-engine-uint.h" +#include "detect-engine-prefilter.h" +#include "detect-websocket.h" + +#include "rust.h" + +static int websocket_tx_id = 0; +static int websocket_payload_id = 0; + +/** + * \internal + * \brief this function will free memory associated with DetectWebSocketOpcodeData + * + * \param de pointer to DetectWebSocketOpcodeData + */ +static void DetectWebSocketOpcodeFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u8_free(de_ptr); +} + +/** + * \internal + * \brief Function to match opcode of a websocket tx + * + * \param det_ctx Pointer to the pattern matcher thread. + * \param f Pointer to the current flow. + * \param flags Flags. + * \param state App layer state. + * \param txv Pointer to the transaction. + * \param s Pointer to the Signature. + * \param ctx Pointer to the sigmatch that we will cast into DetectWebSocketOpcodeData. + * + * \retval 0 no match. + * \retval 1 match. + */ +static int DetectWebSocketOpcodeMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectU8Data *de = (const DetectU8Data *)ctx; + uint8_t opc = SCWebSocketGetOpcode(txv); + return DetectU8Match(opc, de); +} + +/** + * \internal + * \brief this function is used to add the parsed sigmatch into the current signature + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the Current Signature + * \param rawstr pointer to the user provided options + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectWebSocketOpcodeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU8Data *de = SCWebSocketParseOpcode(rawstr); + if (de == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_OPCODE, (SigMatchCtx *)de, websocket_tx_id) == NULL) { + DetectWebSocketOpcodeFree(de_ctx, de); + return -1; + } + + return 0; +} + +/** + * \internal + * \brief this function will free memory associated with DetectWebSocketMaskData + * + * \param de pointer to DetectWebSocketMaskData + */ +static void DetectWebSocketMaskFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u32_free(de_ptr); +} + +static int DetectWebSocketMaskMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + uint32_t val; + const DetectU32Data *du32 = (const DetectU32Data *)ctx; + if (SCWebSocketGetMask(txv, &val)) { + return DetectU32Match(val, du32); + } + return 0; +} + +static int DetectWebSocketMaskSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU32Data *du32 = DetectU32Parse(rawstr); + if (du32 == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_MASK, (SigMatchCtx *)du32, websocket_tx_id) == NULL) { + DetectWebSocketMaskFree(de_ctx, du32); + return -1; + } + + return 0; +} + +static void DetectWebSocketFlagsFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u8_free(de_ptr); +} + +static int DetectWebSocketFlagsMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectU8Data *de = (const DetectU8Data *)ctx; + uint8_t val = SCWebSocketGetFlags(txv); + return DetectU8Match(val, de); +} + +static int DetectWebSocketFlagsSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU8Data *de = SCWebSocketParseFlags(rawstr); + if (de == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_FLAGS, (SigMatchCtx *)de, websocket_tx_id) == NULL) { + DetectWebSocketOpcodeFree(de_ctx, de); + return -1; + } + + return 0; +} + +static int DetectWebSocketPayloadSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rulestr) +{ + if (DetectBufferSetActiveList(de_ctx, s, websocket_payload_id) < 0) + return -1; + + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) != 0) + return -1; + + return 0; +} + +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv, + const int list_id) +{ + InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id); + if (buffer->inspect == NULL) { + const uint8_t *b = NULL; + uint32_t b_len = 0; + + if (!SCWebSocketGetPayload(txv, &b, &b_len)) + return NULL; + if (b == NULL || b_len == 0) + return NULL; + + InspectionBufferSetup(det_ctx, list_id, buffer, b, b_len); + InspectionBufferApplyTransforms(buffer, transforms); + } + return buffer; +} + +/** + * \brief Registration function for websocket.opcode: keyword + */ +void DetectWebsocketRegister(void) +{ + sigmatch_table[DETECT_WEBSOCKET_OPCODE].name = "websocket.opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].desc = "match WebSocket opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].url = "/rules/websocket-keywords.html#websocket-opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].AppLayerTxMatch = DetectWebSocketOpcodeMatch; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].Setup = DetectWebSocketOpcodeSetup; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].Free = DetectWebSocketOpcodeFree; + + DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, 1, + DetectEngineInspectGenericList, NULL); + DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, 1, + DetectEngineInspectGenericList, NULL); + + websocket_tx_id = DetectBufferTypeGetByName("websocket.tx"); + + sigmatch_table[DETECT_WEBSOCKET_MASK].name = "websocket.mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].desc = "match WebSocket mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].url = "/rules/websocket-keywords.html#websocket-mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].AppLayerTxMatch = DetectWebSocketMaskMatch; + sigmatch_table[DETECT_WEBSOCKET_MASK].Setup = DetectWebSocketMaskSetup; + sigmatch_table[DETECT_WEBSOCKET_MASK].Free = DetectWebSocketMaskFree; + + sigmatch_table[DETECT_WEBSOCKET_FLAGS].name = "websocket.flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].desc = "match WebSocket flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].url = "/rules/websocket-keywords.html#websocket-flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].AppLayerTxMatch = DetectWebSocketFlagsMatch; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].Setup = DetectWebSocketFlagsSetup; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].Free = DetectWebSocketFlagsFree; + + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].name = "websocket.payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].desc = "match WebSocket payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].url = + "/rules/websocket-keywords.html#websocket-payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].Setup = DetectWebSocketPayloadSetup; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].flags |= SIGMATCH_NOOPT; + DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, + 0, DetectEngineInspectBufferGeneric, GetData); + DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, + 0, DetectEngineInspectBufferGeneric, GetData); + DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOSERVER, 2, + PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1); + DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOCLIENT, 2, + PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1); + websocket_payload_id = DetectBufferTypeGetByName("websocket.payload"); +} diff --git a/src/detect-websocket.h b/src/detect-websocket.h new file mode 100644 index 000000000000..54e8a22ae4a8 --- /dev/null +++ b/src/detect-websocket.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 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. + */ + +/** + * \file + * + * \author Philippe Antoine + */ + +#ifndef __DETECT_WEBSOCKET_H__ +#define __DETECT_WEBSOCKET_H__ + +void DetectWebsocketRegister(void); + +#endif /* __DETECT_WEBSOCKET_H__ */ diff --git a/src/output-json-websocket.c b/src/output-json-websocket.c new file mode 100644 index 000000000000..9878bcc74ba6 --- /dev/null +++ b/src/output-json-websocket.c @@ -0,0 +1,160 @@ +/* Copyright (C) 2023 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. + */ + +/** + * \file + * + * \author Philippe Antoine + * + * Implement JSON/eve logging app-layer WebSocket. + */ + +#include "suricata-common.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-unittest.h" +#include "util-buffer.h" +#include "util-debug.h" +#include "util-byte.h" + +#include "output.h" +#include "output-json.h" + +#include "app-layer.h" +#include "app-layer-parser.h" + +#include "output-json-websocket.h" +#include "rust.h" + +typedef struct LogWebSocketFileCtx_ { + uint32_t flags; + OutputJsonCtx *eve_ctx; +} LogWebSocketFileCtx; + +typedef struct LogWebSocketLogThread_ { + LogWebSocketFileCtx *websocketlog_ctx; + OutputJsonThreadCtx *ctx; +} LogWebSocketLogThread; + +static int JsonWebSocketLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f, + void *state, void *tx, uint64_t tx_id) +{ + LogWebSocketLogThread *thread = thread_data; + + JsonBuilder *js = CreateEveHeader( + p, LOG_DIR_PACKET, "websocket", NULL, thread->websocketlog_ctx->eve_ctx); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + if (!rs_websocket_logger_log(tx, js)) { + goto error; + } + + OutputJsonBuilderBuffer(js, thread->ctx); + jb_free(js); + + return TM_ECODE_OK; + +error: + jb_free(js); + return TM_ECODE_FAILED; +} + +static void OutputWebSocketLogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogWebSocketFileCtx *websocketlog_ctx = (LogWebSocketFileCtx *)output_ctx->data; + SCFree(websocketlog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputWebSocketLogInitSub(ConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ajt = parent_ctx->data; + + LogWebSocketFileCtx *websocketlog_ctx = SCCalloc(1, sizeof(*websocketlog_ctx)); + if (unlikely(websocketlog_ctx == NULL)) { + return result; + } + websocketlog_ctx->eve_ctx = ajt; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(websocketlog_ctx); + return result; + } + output_ctx->data = websocketlog_ctx; + output_ctx->DeInit = OutputWebSocketLogDeInitCtxSub; + + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_WEBSOCKET); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +static TmEcode JsonWebSocketLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogWebSocketLogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogWebSocket. \"initdata\" is NULL."); + goto error_exit; + } + + thread->websocketlog_ctx = ((OutputCtx *)initdata)->data; + thread->ctx = CreateEveThreadCtx(t, thread->websocketlog_ctx->eve_ctx); + if (!thread->ctx) { + goto error_exit; + } + *data = (void *)thread; + + return TM_ECODE_OK; + +error_exit: + SCFree(thread); + return TM_ECODE_FAILED; +} + +static TmEcode JsonWebSocketLogThreadDeinit(ThreadVars *t, void *data) +{ + LogWebSocketLogThread *thread = (LogWebSocketLogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + FreeEveThreadCtx(thread->ctx); + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonWebSocketLogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketLog", "eve-log.websocket", + OutputWebSocketLogInitSub, ALPROTO_WEBSOCKET, JsonWebSocketLogger, + JsonWebSocketLogThreadInit, JsonWebSocketLogThreadDeinit, NULL); +} diff --git a/src/output-json-websocket.h b/src/output-json-websocket.h new file mode 100644 index 000000000000..481df78c0aee --- /dev/null +++ b/src/output-json-websocket.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 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. + */ + +/** + * \file + * + * \author FirstName LastName + */ + +#ifndef __OUTPUT_JSON_WEBSOCKET_H__ +#define __OUTPUT_JSON_WEBSOCKET_H__ + +void JsonWebSocketLogRegister(void); + +#endif /* __OUTPUT_JSON_WEBSOCKET_H__ */ diff --git a/src/output.c b/src/output.c index 149dda58c284..df8a18f91ffd 100644 --- a/src/output.c +++ b/src/output.c @@ -80,6 +80,7 @@ #include "output-json-rfb.h" #include "output-json-mqtt.h" #include "output-json-pgsql.h" +#include "output-json-websocket.h" #include "output-json-template.h" #include "output-json-rdp.h" #include "output-json-http2.h" @@ -1117,6 +1118,8 @@ void OutputRegisterLoggers(void) JsonMQTTLogRegister(); /* Pgsql JSON logger. */ JsonPgsqlLogRegister(); + /* WebSocket JSON logger. */ + JsonWebSocketLogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); /* RDP JSON logger. */ @@ -1159,6 +1162,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = { { ALPROTO_MQTT, JsonMQTTAddMetadata }, { ALPROTO_PGSQL, NULL }, // TODO missing { ALPROTO_TELNET, NULL }, // no logging + { ALPROTO_WEBSOCKET, rs_websocket_logger_log }, { ALPROTO_TEMPLATE, rs_template_logger_log }, { ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json }, { ALPROTO_HTTP2, rs_http2_log_json }, diff --git a/suricata.yaml.in b/suricata.yaml.in index 630399126dbe..181008b7086c 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -279,6 +279,7 @@ outputs: #md5: [body, subject] #- dnp3 + - websocket - ftp - rdp - nfs @@ -923,6 +924,10 @@ app-layer: ftp: enabled: yes # memcap: 64mb + websocket: + #enabled: yes + # Maximum used payload size, the rest is skipped + # max-payload-size: 65535 rdp: #enabled: yes ssh: From e685ad0efb312b3cf28609859711fd335b5d3f3b Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sat, 6 Jan 2024 23:09:05 +0100 Subject: [PATCH 12/12] websocket: configurable logging of payload in alerts --- etc/schema.json | 6 ++++++ rust/src/jsonbuilder.rs | 9 +++++++++ rust/src/websocket/logger.rs | 20 ++++++++++++++++++++ src/output-json-alert.c | 18 +++++++++++++++++- suricata.yaml.in | 2 ++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/etc/schema.json b/etc/schema.json index e78039d7b62c..5049e6daf7d4 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -5522,6 +5522,12 @@ }, "opcode": { "type": "string" + }, + "payload_base64": { + "type": "string" + }, + "payload_printable": { + "type": "string" } }, "additionalProperties": false diff --git a/rust/src/jsonbuilder.rs b/rust/src/jsonbuilder.rs index 9ff623429509..6ffd3bb0b350 100644 --- a/rust/src/jsonbuilder.rs +++ b/rust/src/jsonbuilder.rs @@ -186,6 +186,15 @@ impl JsonBuilder { } } + // Unclose an object which must not be empty. + pub fn unclose(&mut self) -> Result<&mut Self, JsonError> { + match self.buf.pop() { + Some('}') => {} + _ => {return Err(JsonError::InvalidState);} + } + self.push_state(State::ObjectNth)?; + Ok(self) + } // Closes the currently open datatype (object or array). pub fn close(&mut self) -> Result<&mut Self, JsonError> { match self.current_state() { diff --git a/rust/src/websocket/logger.rs b/rust/src/websocket/logger.rs index 794ee1bac25d..20bdce6a9f66 100644 --- a/rust/src/websocket/logger.rs +++ b/rust/src/websocket/logger.rs @@ -36,6 +36,18 @@ fn log_websocket(tx: &WebSocketTransaction, js: &mut JsonBuilder) -> Result<(), Ok(()) } +fn log_websocket_details(tx: &WebSocketTransaction, js: &mut JsonBuilder, pp: bool, pb64: bool) -> Result<(), JsonError> { + js.unclose()?; + if pp { + js.set_string("payload_printable", &String::from_utf8_lossy(&tx.pdu.payload))?; + } + if pb64 { + js.set_base64("payload_base64", &tx.pdu.payload)?; + } + js.close()?; + Ok(()) +} + #[no_mangle] pub unsafe extern "C" fn rs_websocket_logger_log( tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, @@ -43,3 +55,11 @@ pub unsafe extern "C" fn rs_websocket_logger_log( let tx = cast_pointer!(tx, WebSocketTransaction); log_websocket(tx, js).is_ok() } + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketLogDetails( + tx: &WebSocketTransaction, js: &mut JsonBuilder, + pp: bool, pb64: bool, +) -> bool { + log_websocket_details(tx, js, pp, pb64).is_ok() +} diff --git a/src/output-json-alert.c b/src/output-json-alert.c index 5f511b962d29..c17e592c4a7c 100644 --- a/src/output-json-alert.c +++ b/src/output-json-alert.c @@ -88,12 +88,16 @@ #define LOG_JSON_RULE_METADATA BIT_U16(8) #define LOG_JSON_RULE BIT_U16(9) #define LOG_JSON_VERDICT BIT_U16(10) +#define LOG_JSON_WEBSOCKET_PAYLOAD BIT_U16(11) +#define LOG_JSON_WEBSOCKET_PAYLOAD_BASE64 BIT_U16(12) #define METADATA_DEFAULTS ( LOG_JSON_FLOW | \ LOG_JSON_APP_LAYER | \ LOG_JSON_RULE_METADATA) -#define JSON_BODY_LOGGING (LOG_JSON_HTTP_BODY | LOG_JSON_HTTP_BODY_BASE64) +#define JSON_BODY_LOGGING \ + (LOG_JSON_HTTP_BODY | LOG_JSON_HTTP_BODY_BASE64 | LOG_JSON_WEBSOCKET_PAYLOAD | \ + LOG_JSON_WEBSOCKET_PAYLOAD_BASE64) #define JSON_STREAM_BUFFER_SIZE 4096 @@ -310,6 +314,16 @@ static void AlertAddAppLayer(const Packet *p, JsonBuilder *jb, if (!al->LogTx(tx, jb)) { jb_restore_mark(jb, &mark); } + if (proto == ALPROTO_WEBSOCKET) { + if (option_flags & + (LOG_JSON_WEBSOCKET_PAYLOAD | LOG_JSON_WEBSOCKET_PAYLOAD_BASE64)) { + bool pp = (option_flags & LOG_JSON_WEBSOCKET_PAYLOAD) != 0; + bool pb64 = (option_flags & LOG_JSON_WEBSOCKET_PAYLOAD_BASE64) != 0; + if (!SCWebSocketLogDetails(tx, jb, pp, pb64)) { + jb_restore_mark(jb, &mark); + } + } + } } } return; @@ -854,6 +868,8 @@ static void JsonAlertLogSetupMetadata(AlertJsonOutputCtx *json_output_ctx, SetFlag(conf, "payload-printable", LOG_JSON_PAYLOAD, &flags); SetFlag(conf, "http-body-printable", LOG_JSON_HTTP_BODY, &flags); SetFlag(conf, "http-body", LOG_JSON_HTTP_BODY_BASE64, &flags); + SetFlag(conf, "websocket-payload-printable", LOG_JSON_WEBSOCKET_PAYLOAD, &flags); + SetFlag(conf, "websocket-payload", LOG_JSON_WEBSOCKET_PAYLOAD_BASE64, &flags); SetFlag(conf, "verdict", LOG_JSON_VERDICT, &flags); /* Check for obsolete flags and warn that they have no effect. */ diff --git a/suricata.yaml.in b/suricata.yaml.in index 181008b7086c..dbe8b065168f 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -164,6 +164,8 @@ outputs: # metadata: no # enable inclusion of app layer metadata with alert. Default yes # http-body: yes # Requires metadata; enable dumping of HTTP body in Base64 # http-body-printable: yes # Requires metadata; enable dumping of HTTP body in printable format + # websocket-payload: yes # Requires metadata; enable dumping of WebSocket Payload in Base64 + # websocket-payload-printable: yes # Requires metadata; enable dumping of WebSocket Payload in printable format # Enable the logging of tagged packets for rules using the # "tag" keyword.