From 8dbe67e2b3d9ecff3f50310b571d53f7e5c71155 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Mon, 14 Feb 2022 09:06:02 +0100 Subject: [PATCH 01/15] quic: rustfmt --- rust/src/quic/detect.rs | 2 +- rust/src/quic/quic.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rust/src/quic/detect.rs b/rust/src/quic/detect.rs index d5bb75c956a7..cfdd43fc16b6 100644 --- a/rust/src/quic/detect.rs +++ b/rust/src/quic/detect.rs @@ -15,7 +15,7 @@ * 02110-1301, USA. */ -use crate::quic::quic::{QuicTransaction}; +use crate::quic::quic::QuicTransaction; use std::ptr; #[no_mangle] diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 8b16765f877b..74ba8e2dd602 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -17,8 +17,8 @@ use super::{ cyu::Cyu, - parser::{QuicType, QuicData, QuicHeader}, frames::{Frame, StreamTag}, + parser::{QuicData, QuicHeader, QuicType}, }; use crate::applayer::{self, *}; use crate::core::{AppProto, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP}; @@ -86,7 +86,9 @@ impl QuicState { self.transactions.iter().find(|&tx| tx.tx_id == tx_id + 1) } - fn new_tx(&mut self, header: QuicHeader, data: QuicData, sni: Option>, ua: Option>) -> QuicTransaction { + fn new_tx( + &mut self, header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, + ) -> QuicTransaction { let mut tx = QuicTransaction::new(header, data, sni, ua); self.max_tx_id += 1; tx.tx_id = self.max_tx_id; @@ -118,8 +120,8 @@ impl QuicState { Ok(data) => { // no tx for the short header (data) frames if header.ty != QuicType::Short { - let mut sni : Option> = None; - let mut ua : Option> = None; + let mut sni: Option> = None; + let mut ua: Option> = None; for frame in &data.frames { if let Frame::Stream(s) = frame { if let Some(tags) = &s.tags { @@ -190,8 +192,7 @@ pub unsafe extern "C" fn rs_quic_probing_parser( #[no_mangle] pub unsafe extern "C" fn rs_quic_parse( _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, - stream_slice: StreamSlice, - _data: *const std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, ) -> AppLayerResult { let state = cast_pointer!(state, QuicState); let buf = stream_slice.as_slice(); @@ -293,7 +294,6 @@ pub unsafe extern "C" fn rs_quic_register_parser() { truncate: None, get_frame_id_by_name: None, get_frame_name_by_id: None, - }; let ip_proto_str = CString::new("udp").unwrap(); From 6d6f589ac05f184daae367aaa3d8db3f5737c5e2 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Mon, 14 Feb 2022 09:38:10 +0100 Subject: [PATCH 02/15] quic: complete parsing of initial for non gquic Ticket: 4967 The format of initial packet for quic ietf, ie quic v1, is described in rfc 9000, section 17.2.2 --- configure.ac | 2 +- rust/Cargo.toml.in | 2 + rust/src/quic/parser.rs | 94 +++++++++++++++++++++++++-- rust/src/quic/quic.rs | 141 +++++++++++++++++++++++++++++++--------- 4 files changed, 201 insertions(+), 38 deletions(-) diff --git a/configure.ac b/configure.ac index a34b71f4f04f..90421921fa5a 100644 --- a/configure.ac +++ b/configure.ac @@ -296,7 +296,7 @@ LUA_LIB_NAME="lua-5.1" CFLAGS="${CFLAGS} -DOS_DARWIN" CPPFLAGS="${CPPFLAGS} -I/opt/local/include" - LDFLAGS="${LDFLAGS} -L/opt/local/lib" + LDFLAGS="${LDFLAGS} -L/opt/local/lib -framework Security" ;; *-*-linux*) # Always compile with -fPIC on Linux for shared library support. diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index b53a40d2a932..a8773be1fda7 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -53,6 +53,8 @@ md-5 = "~0.9.1" regex = "~1.4.2" lazy_static = "~1.4.0" base64 = "~0.13.0" +# the feature quic is needed by rustls::quic::Keys to "decrypt" the first quic packets +rustls = { version="~0.20.3", default-features = false, features=["quic"] } suricata-derive = { path = "./derive" } diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs index f4626563cd7c..c3d069d42315 100644 --- a/rust/src/quic/parser.rs +++ b/rust/src/quic/parser.rs @@ -18,9 +18,11 @@ use super::error::QuicError; use super::frames::Frame; use nom::bytes::complete::take; use nom::combinator::{all_consuming, map}; -use nom::number::complete::{be_u32, be_u8}; +use nom::number::complete::{be_u24, be_u32, be_u8}; use nom::IResult; +use rustls::quic::Version; + /* gQUIC is the Google version of QUIC. @@ -75,6 +77,14 @@ impl From for QuicVersion { } } +pub fn quic_rustls_version(version: u32) -> Option { + match version { + 0xff00_001d..=0xff00_0020 => Some(Version::V1Draft), + 0x0000_0001 | 0xff00_0021..=0xff00_0022 => Some(Version::V1), + _ => None, + } +} + #[derive(Debug, PartialEq)] pub enum QuicType { Initial, @@ -98,6 +108,58 @@ impl PublicFlags { } } +pub fn quic_pkt_num(input: &[u8]) -> u64 { + // There must be a more idiomatic way sigh... + match input.len() { + 1 => { + return input[0] as u64; + } + 2 => { + return input[0] as u64 | ((input[1] as u64) << 8); + } + 3 => { + return input[0] as u64 | ((input[1] as u64) << 8) | ((input[2] as u64) << 16); + } + 4 => { + return input[0] as u64 + | ((input[1] as u64) << 8) + | ((input[2] as u64) << 16) + | ((input[3] as u64) << 24); + } + _ => { + // should not be reachable because rustls errors first + debug_validate_fail!("Unexpected length for quic pkt num"); + return 0; + } + } +} + +fn quic_var_uint(input: &[u8]) -> IResult<&[u8], u64, QuicError> { + let (rest, first) = be_u8(input)?; + let msb = first >> 6; + let lsb = (first & 0x3F) as u64; + match msb { + 3 => { + // nom does not have be_u56 + let (rest, second) = be_u24(rest)?; + let (rest, third) = be_u32(rest)?; + return Ok((rest, (lsb << 56) | ((second as u64) << 32) | (third as u64))); + } + 2 => { + let (rest, second) = be_u24(rest)?; + return Ok((rest, (lsb << 24) | (second as u64))); + } + 1 => { + let (rest, second) = be_u8(rest)?; + return Ok((rest, (lsb << 8) | (second as u64))); + } + _ => { + // only remaining case is msb==0 + return Ok((rest, lsb)); + } + } +} + /// A QUIC packet's header. #[derive(Debug, PartialEq)] pub struct QuicHeader { @@ -107,6 +169,7 @@ pub struct QuicHeader { pub version_buf: Vec, pub dcid: Vec, pub scid: Vec, + pub length: usize, } #[derive(Debug, PartialEq)] @@ -126,6 +189,7 @@ impl QuicHeader { version_buf: Vec::new(), dcid, scid, + length: 0, } } @@ -148,6 +212,7 @@ impl QuicHeader { version_buf: Vec::new(), dcid: dcid.to_vec(), scid: Vec::new(), + length: 0, }, )); } else { @@ -212,15 +277,29 @@ impl QuicHeader { (rest, dcid.to_vec(), scid.to_vec()) }; + let mut has_length = false; let rest = match ty { QuicType::Initial => { - let (rest, _pkt_num) = be_u32(rest)?; - let (rest, _msg_auth_hash) = take(12usize)(rest)?; - - rest + if version.is_gquic() { + let (rest, _pkt_num) = be_u32(rest)?; + let (rest, _msg_auth_hash) = take(12usize)(rest)?; + + rest + } else { + let (rest, token_length) = quic_var_uint(rest)?; + let (rest, _token) = take(token_length as usize)(rest)?; + has_length = true; + rest + } } _ => rest, }; + let (rest, length) = if has_length { + let (rest, plength) = quic_var_uint(rest)?; + (rest, plength as usize) + } else { + (rest, rest.len()) + }; Ok(( rest, @@ -231,6 +310,7 @@ impl QuicHeader { version_buf: version_buf.to_vec(), dcid, scid, + length, }, )) } @@ -275,7 +355,8 @@ mod tests { .to_vec(), scid: hex::decode("09b15e86dd0990aaf906c5de620c4538398ffa58") .unwrap() - .to_vec() + .to_vec(), + length: 1154, }, value ); @@ -295,6 +376,7 @@ mod tests { version_buf: vec![0x51, 0x30, 0x34, 0x34], dcid: hex::decode("05cad2cc06c4d0e4").unwrap().to_vec(), scid: Vec::new(), + length: 1042, }, header ); diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 74ba8e2dd602..9f65d010f143 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -18,7 +18,7 @@ use super::{ cyu::Cyu, frames::{Frame, StreamTag}, - parser::{QuicData, QuicHeader, QuicType}, + parser::{quic_pkt_num, quic_rustls_version, QuicData, QuicHeader, QuicType}, }; use crate::applayer::{self, *}; use crate::core::{AppProto, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP}; @@ -54,6 +54,7 @@ impl QuicTransaction { pub struct QuicState { max_tx_id: u64, + keys: Option, transactions: Vec, } @@ -61,6 +62,7 @@ impl Default for QuicState { fn default() -> Self { Self { max_tx_id: 0, + keys: None, transactions: Vec::new(), } } @@ -114,44 +116,107 @@ impl QuicState { return None; } - fn parse(&mut self, input: &[u8]) -> bool { - match QuicHeader::from_bytes(input, DEFAULT_DCID_LEN) { - Ok((rest, header)) => match QuicData::from_bytes(rest) { - Ok(data) => { - // no tx for the short header (data) frames - if header.ty != QuicType::Short { - let mut sni: Option> = None; - let mut ua: Option> = None; - for frame in &data.frames { - if let Frame::Stream(s) = frame { - if let Some(tags) = &s.tags { - for (tag, value) in tags { - if tag == &StreamTag::Sni { - sni = Some(value.to_vec()); - } else if tag == &StreamTag::Uaid { - ua = Some(value.to_vec()); - } - if sni.is_some() && ua.is_some() { - break; + fn parse(&mut self, input: &[u8], to_server: bool) -> bool { + // so as to loop over multiple quic headers in one packet + let mut buf = input; + while buf.len() > 0 { + match QuicHeader::from_bytes(buf, DEFAULT_DCID_LEN) { + Ok((rest, header)) => { + // unprotect + if self.keys.is_none() && header.ty == QuicType::Initial { + if let Some(version) = quic_rustls_version(u32::from(header.version)) { + let keys = rustls::quic::Keys::initial(version, &header.dcid, false); + self.keys = Some(keys) + } + } + if header.length > rest.len() { + //TODO event whenever an error condition is met + return false; + } + let (mut framebuf, next_buf) = rest.split_at(header.length); + let mut rest2; + if let Some(keys) = &self.keys { + let hkey = if to_server { + &keys.remote.header + } else { + &keys.local.header + }; + if framebuf.len() < 4 + hkey.sample_len() { + return false; + } + let hlen = buf.len() - rest.len(); + let mut h2 = Vec::with_capacity(hlen + header.length); + h2.extend_from_slice(&buf[..hlen + header.length]); + let mut h20 = h2[0]; + let mut pktnum_buf = Vec::with_capacity(4); + pktnum_buf.extend_from_slice(&h2[hlen..hlen + 4]); + let r1 = hkey.decrypt_in_place( + &h2[hlen + 4..hlen + 4 + hkey.sample_len()], + &mut h20, + &mut pktnum_buf, + ); + if !r1.is_ok() { + return false; + } + // mutate one at a time + h2[0] = h20; + let _ = &h2[hlen..hlen + 4].copy_from_slice(&pktnum_buf); + let pkt_num = quic_pkt_num(&h2[hlen..hlen + 1 + ((h20 & 3) as usize)]); + rest2 = Vec::with_capacity(framebuf.len() - (1 + ((h20 & 3) as usize))); + if framebuf.len() < 1 + ((h20 & 3) as usize) { + return false; + } + rest2.extend_from_slice(&framebuf[1 + ((h20 & 3) as usize)..]); + let pkey = if to_server { + &keys.remote.packet + } else { + &keys.local.packet + }; + let r = pkey.decrypt_in_place(pkt_num, &h2[..hlen + 4], &mut rest2); + if !r.is_ok() { + return false; + } + framebuf = &rest2; + } + buf = next_buf; + match QuicData::from_bytes(framebuf) { + Ok(data) => { + // no tx for the short header (data) frames + if header.ty != QuicType::Short { + let mut sni: Option> = None; + let mut ua: Option> = None; + for frame in &data.frames { + if let Frame::Stream(s) = frame { + if let Some(tags) = &s.tags { + for (tag, value) in tags { + if tag == &StreamTag::Sni { + sni = Some(value.to_vec()); + } else if tag == &StreamTag::Uaid { + ua = Some(value.to_vec()); + } + if sni.is_some() && ua.is_some() { + break; + } + } } } } + + let transaction = self.new_tx(header, data, sni, ua); + self.transactions.push(transaction); } } - - let transaction = self.new_tx(header, data, sni, ua); - self.transactions.push(transaction); + Err(_e) => { + return false; + } } - return true; } Err(_e) => { return false; } - }, - Err(_e) => { - return false; } } + return true; } } @@ -190,14 +255,28 @@ pub unsafe extern "C" fn rs_quic_probing_parser( } #[no_mangle] -pub unsafe extern "C" fn rs_quic_parse( +pub unsafe extern "C" fn rs_quic_parse_tc( + _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, QuicState); + let buf = stream_slice.as_slice(); + + if state.parse(buf, false) { + return AppLayerResult::ok(); + } + return AppLayerResult::err(); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_parse_ts( _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, stream_slice: StreamSlice, _data: *const std::os::raw::c_void, ) -> AppLayerResult { let state = cast_pointer!(state, QuicState); let buf = stream_slice.as_slice(); - if state.parse(buf) { + if state.parse(buf, true) { return AppLayerResult::ok(); } return AppLayerResult::err(); @@ -275,8 +354,8 @@ pub unsafe extern "C" fn rs_quic_register_parser() { state_new: rs_quic_state_new, state_free: rs_quic_state_free, tx_free: rs_quic_state_tx_free, - parse_ts: rs_quic_parse, - parse_tc: rs_quic_parse, + parse_ts: rs_quic_parse_ts, + parse_tc: rs_quic_parse_tc, get_tx_count: rs_quic_state_get_tx_count, get_tx: rs_quic_state_get_tx, tx_comp_st_ts: 1, From 9c5f21715fb36cffe334157eea0fc2822f12a142 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Tue, 22 Feb 2022 08:49:46 +0100 Subject: [PATCH 03/15] rdp: bump up tls-parser crate version so that we can use new functions in quic parser --- rust/Cargo.toml.in | 2 +- rust/src/rdp/rdp.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index a8773be1fda7..d0b2eb9edacb 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -43,7 +43,7 @@ kerberos-parser = "~0.5.0" ntp-parser = "~0.6.0" ipsec-parser = "~0.7.0" snmp-parser = "~0.6.0" -tls-parser = "~0.9.4" +tls-parser = "~0.11.0" x509-parser = "~0.6.5" libc = "~0.2.82" sha2 = "~0.9.2" diff --git a/rust/src/rdp/rdp.rs b/rust/src/rdp/rdp.rs index b6d266f22097..8d2904b2a600 100644 --- a/rust/src/rdp/rdp.rs +++ b/rust/src/rdp/rdp.rs @@ -25,6 +25,7 @@ use crate::rdp::parser::*; use nom; use std; use tls_parser::{parse_tls_plaintext, TlsMessage, TlsMessageHandshake, TlsRecordType}; +use tls_parser::nom::Err; static mut ALPROTO_RDP: AppProto = ALPROTO_UNKNOWN; @@ -179,7 +180,7 @@ impl RdpState { available = remainder; } - Err(nom::Err::Incomplete(_)) => { + Err(Err::Incomplete(_)) => { // nom need not compatible with applayer need, request one more byte return AppLayerResult::incomplete( (input.len() - available.len()) as u32, @@ -187,7 +188,7 @@ impl RdpState { ); } - Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => { + Err(Err::Failure(_)) | Err(Err::Error(_)) => { return AppLayerResult::err(); } } @@ -291,7 +292,7 @@ impl RdpState { } } - Err(nom::Err::Incomplete(_)) => { + Err(Err::Incomplete(_)) => { // nom need not compatible with applayer need, request one more byte return AppLayerResult::incomplete( (input.len() - available.len()) as u32, @@ -299,7 +300,7 @@ impl RdpState { ); } - Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => { + Err(Err::Failure(_)) | Err(Err::Error(_)) => { return AppLayerResult::err(); } } From 6feb585f47b79ee151c75462002ba6dc48c46f10 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Tue, 22 Feb 2022 08:50:38 +0100 Subject: [PATCH 04/15] quic: parse more frames and logs interesting extensions from crypto frame --- rust/src/quic/error.rs | 2 + rust/src/quic/frames.rs | 117 ++++++++++++++++++++++++++++++++++++++-- rust/src/quic/logger.rs | 37 ++++++++++--- rust/src/quic/parser.rs | 2 +- rust/src/quic/quic.rs | 44 +++++++++------ 5 files changed, 175 insertions(+), 27 deletions(-) diff --git a/rust/src/quic/error.rs b/rust/src/quic/error.rs index 4099fe4cab15..be3d9b54a506 100644 --- a/rust/src/quic/error.rs +++ b/rust/src/quic/error.rs @@ -24,6 +24,7 @@ pub enum QuicError { StreamTagNoMatch(u32), InvalidPacket, Incomplete, + NotSupported, NomError(ErrorKind), } @@ -45,6 +46,7 @@ impl fmt::Display for QuicError { } QuicError::Incomplete => write!(f, "Incomplete data"), QuicError::InvalidPacket => write!(f, "Invalid packet"), + QuicError::NotSupported => write!(f, "Not supported case yet"), QuicError::NomError(e) => write!(f, "Internal parser error {:?}", e), } } diff --git a/rust/src/quic/frames.rs b/rust/src/quic/frames.rs index 3f880dcd1b96..786b0f88e1cd 100644 --- a/rust/src/quic/frames.rs +++ b/rust/src/quic/frames.rs @@ -16,6 +16,7 @@ */ use super::error::QuicError; +use crate::quic::parser::quic_var_uint; use nom::bytes::complete::take; use nom::combinator::{all_consuming, complete}; use nom::multi::{count, many0}; @@ -24,6 +25,12 @@ use nom::sequence::pair; use nom::IResult; use num::FromPrimitive; use std::fmt; +use tls_parser::TlsMessage::Handshake; +use tls_parser::TlsMessageHandshake::{ClientHello, ServerHello}; +use tls_parser::{ + parse_tls_extensions, parse_tls_message_handshake, TlsCipherSuiteID, TlsExtension, + TlsExtensionType, +}; /// Tuple of StreamTag and offset type TagOffset = (StreamTag, u32); @@ -115,13 +122,117 @@ impl fmt::Display for StreamTag { } } +#[derive(Debug, PartialEq)] +pub(crate) struct Ack { + pub largest_acknowledged: u64, + pub ack_delay: u64, + pub ack_range_count: u64, + pub first_ack_range: u64, +} + +#[derive(Debug, PartialEq)] +pub(crate) struct Crypto { + pub ciphers: Vec, + // We remap the Vec from tls_parser::parse_tls_extensions because of + // the lifetime of TlsExtension due to references to the slice used for parsing + pub extv: Vec, +} + #[derive(Debug, PartialEq)] pub(crate) enum Frame { Padding, + Ack(Ack), + Crypto(Crypto), Stream(Stream), Unknown(Vec), } +fn parse_ack_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { + let (rest, largest_acknowledged) = quic_var_uint(input)?; + let (rest, ack_delay) = quic_var_uint(rest)?; + let (rest, ack_range_count) = quic_var_uint(rest)?; + let (rest, first_ack_range) = quic_var_uint(rest)?; + + if ack_range_count != 0 { + //TODO RFC9000 section 19.3.1. ACK Ranges + return Err(nom::Err::Error(QuicError::NotSupported)); + } + + Ok(( + rest, + Frame::Ack(Ack { + largest_acknowledged, + ack_delay, + ack_range_count, + first_ack_range, + }), + )) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct QuicTlsExtension { + pub etype: TlsExtensionType, + pub values: Vec>, +} + +// get interesting stuff out of parsed tls extensions +fn quic_get_tls_extensions(input: Option<&[u8]>) -> Vec { + let mut extv = Vec::new(); + if let Some(extr) = input { + if let Ok((_, exts)) = parse_tls_extensions(extr) { + for e in &exts { + let etype = TlsExtensionType::from(e); + let mut values = Vec::new(); + match e { + TlsExtension::SNI(x) => { + for sni in x { + let mut value = Vec::new(); + value.extend_from_slice(sni.1); + values.push(value); + } + } + TlsExtension::ALPN(x) => { + for alpn in x { + let mut value = Vec::new(); + value.extend_from_slice(alpn); + values.push(value); + } + } + _ => {} + } + extv.push(QuicTlsExtension { etype, values }) + } + } + } + return extv; +} + +fn parse_crypto_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { + let (rest, offset) = quic_var_uint(input)?; + let (rest, length) = quic_var_uint(rest)?; + let (rest, _) = take(offset as usize)(rest)?; + let (_, data) = take(length as usize)(rest)?; + + if let Ok((rest, msg)) = parse_tls_message_handshake(data) { + if let Handshake(hs) = msg { + match hs { + ClientHello(ch) => { + let ciphers = ch.ciphers; + let extv = quic_get_tls_extensions(ch.ext); + return Ok((rest, Frame::Crypto(Crypto { ciphers, extv }))); + } + ServerHello(sh) => { + let ciphers = vec![sh.cipher]; + let extv = quic_get_tls_extensions(sh.ext); + return Ok((rest, Frame::Crypto(Crypto { ciphers, extv }))); + } + _ => {} + } + } + } + return Err(nom::Err::Error(QuicError::InvalidPacket)); +} + fn parse_tag(input: &[u8]) -> IResult<&[u8], StreamTag, QuicError> { let (rest, tag) = be_u32(input)?; @@ -164,8 +275,6 @@ fn parse_crypto_stream(input: &[u8]) -> IResult<&[u8], Vec, QuicError> } fn parse_stream_frame(input: &[u8], frame_ty: u8) -> IResult<&[u8], Frame, QuicError> { - let rest = input; - // 0b1_f_d_ooo_ss let fin = frame_ty & 0x40 == 0x40; let has_data_length = frame_ty & 0x20 == 0x20; @@ -180,7 +289,7 @@ fn parse_stream_frame(input: &[u8], frame_ty: u8) -> IResult<&[u8], Frame, QuicE let stream_id_hdr_length = usize::from((frame_ty & 0x03) + 1); - let (rest, stream_id) = take(stream_id_hdr_length)(rest)?; + let (rest, stream_id) = take(stream_id_hdr_length)(input)?; let (rest, offset) = take(offset_hdr_length)(rest)?; let (rest, data_length) = if has_data_length { @@ -221,6 +330,8 @@ impl Frame { } else { match frame_ty { 0x00 => (rest, Frame::Padding), + 0x02 => parse_ack_frame(rest)?, + 0x06 => parse_crypto_frame(rest)?, _ => ([].as_ref(), Frame::Unknown(rest.to_vec())), } }; diff --git a/rust/src/quic/logger.rs b/rust/src/quic/logger.rs index 19426776dd01..43eb35f24d2d 100644 --- a/rust/src/quic/logger.rs +++ b/rust/src/quic/logger.rs @@ -17,6 +17,7 @@ use super::quic::QuicTransaction; use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std::fmt::Write; fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { js.open_object("quic")?; @@ -30,14 +31,38 @@ fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonEr js.set_string("ua", &String::from_utf8_lossy(&ua))?; } } - js.open_array("cyu")?; - for cyu in &tx.cyu { - js.start_object()?; - js.set_string("hash", &cyu.hash)?; - js.set_string("string", &cyu.string)?; + if tx.cyu.len() > 0 { + js.open_array("cyu")?; + for cyu in &tx.cyu { + js.start_object()?; + js.set_string("hash", &cyu.hash)?; + js.set_string("string", &cyu.string)?; + js.close()?; + } + js.close()?; + } + + if tx.extv.len() > 0 { + js.open_array("extensions")?; + for e in &tx.extv { + let mut etypes = String::with_capacity(32); + match write!(&mut etypes, "{}", e.etype) { + _ => {} + } + js.start_object()?; + js.set_string("type", &etypes)?; + + if e.values.len() > 0 { + js.open_array("values")?; + for i in 0..e.values.len() { + js.append_string(&String::from_utf8_lossy(&e.values[i]))?; + } + js.close()?; + } + js.close()?; + } js.close()?; } - js.close()?; js.close()?; Ok(()) diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs index c3d069d42315..d0f912bfccd2 100644 --- a/rust/src/quic/parser.rs +++ b/rust/src/quic/parser.rs @@ -134,7 +134,7 @@ pub fn quic_pkt_num(input: &[u8]) -> u64 { } } -fn quic_var_uint(input: &[u8]) -> IResult<&[u8], u64, QuicError> { +pub fn quic_var_uint(input: &[u8]) -> IResult<&[u8], u64, QuicError> { let (rest, first) = be_u8(input)?; let msb = first >> 6; let lsb = (first & 0x3F) as u64; diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 9f65d010f143..f1e3e67953fa 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -17,7 +17,7 @@ use super::{ cyu::Cyu, - frames::{Frame, StreamTag}, + frames::{Frame, QuicTlsExtension, StreamTag}, parser::{quic_pkt_num, quic_rustls_version, QuicData, QuicHeader, QuicType}, }; use crate::applayer::{self, *}; @@ -35,11 +35,15 @@ pub struct QuicTransaction { pub cyu: Vec, pub sni: Option>, pub ua: Option>, + pub extv: Vec, tx_data: AppLayerTxData, } impl QuicTransaction { - fn new(header: QuicHeader, data: QuicData, sni: Option>, ua: Option>) -> Self { + fn new( + header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, + extv: Vec, + ) -> Self { let cyu = Cyu::generate(&header, &data.frames); QuicTransaction { tx_id: 0, @@ -47,6 +51,7 @@ impl QuicTransaction { cyu, sni, ua, + extv, tx_data: AppLayerTxData::new(), } } @@ -90,11 +95,12 @@ impl QuicState { fn new_tx( &mut self, header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, - ) -> QuicTransaction { - let mut tx = QuicTransaction::new(header, data, sni, ua); + extb: Vec, + ) { + let mut tx = QuicTransaction::new(header, data, sni, ua, extb); self.max_tx_id += 1; tx.tx_id = self.max_tx_id; - return tx; + self.transactions.push(tx); } fn tx_iterator( @@ -185,25 +191,29 @@ impl QuicState { if header.ty != QuicType::Short { let mut sni: Option> = None; let mut ua: Option> = None; + let mut extv: Vec = Vec::new(); for frame in &data.frames { - if let Frame::Stream(s) = frame { - if let Some(tags) = &s.tags { - for (tag, value) in tags { - if tag == &StreamTag::Sni { - sni = Some(value.to_vec()); - } else if tag == &StreamTag::Uaid { - ua = Some(value.to_vec()); - } - if sni.is_some() && ua.is_some() { - break; + match frame { + Frame::Stream(s) => { + if let Some(tags) = &s.tags { + for (tag, value) in tags { + if tag == &StreamTag::Sni { + sni = Some(value.to_vec()); + } else if tag == &StreamTag::Uaid { + ua = Some(value.to_vec()); + } + if sni.is_some() && ua.is_some() { + break; + } } } } + Frame::Crypto(c) => extv.extend_from_slice(&c.extv), + _ => {} } } - let transaction = self.new_tx(header, data, sni, ua); - self.transactions.push(transaction); + self.new_tx(header, data, sni, ua, extv); } } Err(_e) => { From 38c90ee03a86eaee28865f3092fff1c6ced0a983 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Mon, 28 Feb 2022 14:50:23 +0100 Subject: [PATCH 05/15] quic: use packet num length when decrypting As it can be 4, but it can also be 1, based on the first decrypted byte --- rust/src/quic/quic.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index f1e3e67953fa..3d925a264484 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -166,7 +166,8 @@ impl QuicState { } // mutate one at a time h2[0] = h20; - let _ = &h2[hlen..hlen + 4].copy_from_slice(&pktnum_buf); + let _ = &h2[hlen..hlen + 1 + ((h20 & 3) as usize)] + .copy_from_slice(&pktnum_buf[..1 + ((h20 & 3) as usize)]); let pkt_num = quic_pkt_num(&h2[hlen..hlen + 1 + ((h20 & 3) as usize)]); rest2 = Vec::with_capacity(framebuf.len() - (1 + ((h20 & 3) as usize))); if framebuf.len() < 1 + ((h20 & 3) as usize) { @@ -178,7 +179,11 @@ impl QuicState { } else { &keys.local.packet }; - let r = pkey.decrypt_in_place(pkt_num, &h2[..hlen + 4], &mut rest2); + let r = pkey.decrypt_in_place( + pkt_num, + &h2[..hlen + 1 + ((h20 & 3) as usize)], + &mut rest2, + ); if !r.is_ok() { return false; } From a6f8a2b5aa4427d9da06aa1ca7a20d7d71ead0ad Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Mon, 28 Feb 2022 15:05:02 +0100 Subject: [PATCH 06/15] quic: do not try to parse encrypted data The way to determine if the payload is encrypted is by storing in the state if we have seen a crypto frame in both directions... --- rust/src/quic/quic.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 3d925a264484..430b8296b099 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -60,6 +60,8 @@ impl QuicTransaction { pub struct QuicState { max_tx_id: u64, keys: Option, + hello_tc: bool, + hello_ts: bool, transactions: Vec, } @@ -68,6 +70,8 @@ impl Default for QuicState { Self { max_tx_id: 0, keys: None, + hello_tc: false, + hello_ts: false, transactions: Vec::new(), } } @@ -129,6 +133,10 @@ impl QuicState { match QuicHeader::from_bytes(buf, DEFAULT_DCID_LEN) { Ok((rest, header)) => { // unprotect + if self.hello_tc && self.hello_ts { + // payload is encrypted, stop parsing here + return true; + } if self.keys.is_none() && header.ty == QuicType::Initial { if let Some(version) = quic_rustls_version(u32::from(header.version)) { let keys = rustls::quic::Keys::initial(version, &header.dcid, false); @@ -213,7 +221,14 @@ impl QuicState { } } } - Frame::Crypto(c) => extv.extend_from_slice(&c.extv), + Frame::Crypto(c) => { + extv.extend_from_slice(&c.extv); + if to_server { + self.hello_ts = true + } else { + self.hello_tc = true + } + } _ => {} } } From 2d7778285acefe1d15560f1e2d66a3823e0a0102 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Mon, 28 Feb 2022 15:48:48 +0100 Subject: [PATCH 07/15] quic: decrypt in its own function So as to keep parse not too big --- rust/src/quic/quic.rs | 102 +++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 430b8296b099..2faa23a6528d 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -126,6 +126,56 @@ impl QuicState { return None; } + fn decrypt<'a>( + &mut self, to_server: bool, header: &QuicHeader, framebuf: &'a [u8], buf: &'a [u8], + hlen: usize, output: &'a mut Vec, + ) -> bool { + if let Some(keys) = &self.keys { + let hkey = if to_server { + &keys.remote.header + } else { + &keys.local.header + }; + if framebuf.len() < 4 + hkey.sample_len() { + return false; + } + let mut h2 = Vec::with_capacity(hlen + header.length); + h2.extend_from_slice(&buf[..hlen + header.length]); + let mut h20 = h2[0]; + let mut pktnum_buf = Vec::with_capacity(4); + pktnum_buf.extend_from_slice(&h2[hlen..hlen + 4]); + let r1 = hkey.decrypt_in_place( + &h2[hlen + 4..hlen + 4 + hkey.sample_len()], + &mut h20, + &mut pktnum_buf, + ); + if !r1.is_ok() { + return false; + } + // mutate one at a time + h2[0] = h20; + let _ = &h2[hlen..hlen + 1 + ((h20 & 3) as usize)] + .copy_from_slice(&pktnum_buf[..1 + ((h20 & 3) as usize)]); + let pkt_num = quic_pkt_num(&h2[hlen..hlen + 1 + ((h20 & 3) as usize)]); + if framebuf.len() < 1 + ((h20 & 3) as usize) { + return false; + } + output.extend_from_slice(&framebuf[1 + ((h20 & 3) as usize)..]); + let pkey = if to_server { + &keys.remote.packet + } else { + &keys.local.packet + }; + let r = pkey.decrypt_in_place(pkt_num, &h2[..hlen + 1 + ((h20 & 3) as usize)], output); + if !r.is_ok() { + return false; + } + return true; + } + // unreachable by the way + return false; + } + fn parse(&mut self, input: &[u8], to_server: bool) -> bool { // so as to loop over multiple quic headers in one packet let mut buf = input; @@ -148,54 +198,14 @@ impl QuicState { return false; } let (mut framebuf, next_buf) = rest.split_at(header.length); - let mut rest2; - if let Some(keys) = &self.keys { - let hkey = if to_server { - &keys.remote.header - } else { - &keys.local.header - }; - if framebuf.len() < 4 + hkey.sample_len() { - return false; - } - let hlen = buf.len() - rest.len(); - let mut h2 = Vec::with_capacity(hlen + header.length); - h2.extend_from_slice(&buf[..hlen + header.length]); - let mut h20 = h2[0]; - let mut pktnum_buf = Vec::with_capacity(4); - pktnum_buf.extend_from_slice(&h2[hlen..hlen + 4]); - let r1 = hkey.decrypt_in_place( - &h2[hlen + 4..hlen + 4 + hkey.sample_len()], - &mut h20, - &mut pktnum_buf, - ); - if !r1.is_ok() { - return false; - } - // mutate one at a time - h2[0] = h20; - let _ = &h2[hlen..hlen + 1 + ((h20 & 3) as usize)] - .copy_from_slice(&pktnum_buf[..1 + ((h20 & 3) as usize)]); - let pkt_num = quic_pkt_num(&h2[hlen..hlen + 1 + ((h20 & 3) as usize)]); - rest2 = Vec::with_capacity(framebuf.len() - (1 + ((h20 & 3) as usize))); - if framebuf.len() < 1 + ((h20 & 3) as usize) { - return false; - } - rest2.extend_from_slice(&framebuf[1 + ((h20 & 3) as usize)..]); - let pkey = if to_server { - &keys.remote.packet - } else { - &keys.local.packet - }; - let r = pkey.decrypt_in_place( - pkt_num, - &h2[..hlen + 1 + ((h20 & 3) as usize)], - &mut rest2, - ); - if !r.is_ok() { + let hlen = buf.len() - rest.len(); + let mut output; + if self.keys.is_some() { + output = Vec::with_capacity(framebuf.len()+4); + if !self.decrypt(to_server, &header, framebuf, buf, hlen, &mut output) { return false; } - framebuf = &rest2; + framebuf = &output; } buf = next_buf; match QuicData::from_bytes(framebuf) { From 5babeaaf094d1b3c19fdee6a13ae12668412e8cb Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Wed, 2 Mar 2022 21:39:55 +0100 Subject: [PATCH 08/15] quic: use sni from crypto frame with tls for detection --- rust/src/quic/quic.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 2faa23a6528d..0f57c7239f00 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -23,6 +23,7 @@ use super::{ use crate::applayer::{self, *}; use crate::core::{AppProto, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP}; use std::ffi::CString; +use tls_parser::TlsExtensionType; static mut ALPROTO_QUIC: AppProto = ALPROTO_UNKNOWN; @@ -201,7 +202,7 @@ impl QuicState { let hlen = buf.len() - rest.len(); let mut output; if self.keys.is_some() { - output = Vec::with_capacity(framebuf.len()+4); + output = Vec::with_capacity(framebuf.len() + 4); if !self.decrypt(to_server, &header, framebuf, buf, hlen, &mut output) { return false; } @@ -232,6 +233,13 @@ impl QuicState { } } Frame::Crypto(c) => { + for e in &c.extv { + if e.etype == TlsExtensionType::ServerName + && e.values.len() > 0 + { + sni = Some(e.values[0].to_vec()); + } + } extv.extend_from_slice(&c.extv); if to_server { self.hello_ts = true From 556a131170dfcc2a2eee00262ef16ba6d54b2431 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Wed, 2 Mar 2022 22:41:26 +0100 Subject: [PATCH 09/15] quic: ja3 computation and logging and detection Ticket: 5143 --- rust/src/quic/detect.rs | 15 ++++ rust/src/quic/frames.rs | 77 ++++++++++++++-- rust/src/quic/logger.rs | 3 + rust/src/quic/quic.rs | 12 ++- src/Makefile.am | 2 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 1 + src/detect-quic-ja3.c | 169 +++++++++++++++++++++++++++++++++++ src/detect-quic-ja3.h | 28 ++++++ 9 files changed, 300 insertions(+), 9 deletions(-) create mode 100644 src/detect-quic-ja3.c create mode 100644 src/detect-quic-ja3.h diff --git a/rust/src/quic/detect.rs b/rust/src/quic/detect.rs index cfdd43fc16b6..7e9019bef004 100644 --- a/rust/src/quic/detect.rs +++ b/rust/src/quic/detect.rs @@ -48,6 +48,21 @@ pub unsafe extern "C" fn rs_quic_tx_get_sni( } } +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_ja3( + tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if let Some(ja3) = &tx.ja3 { + *buffer = ja3.as_ptr(); + *buffer_len = ja3.len() as u32; + 1 + } else { + *buffer = ptr::null(); + *buffer_len = 0; + 0 + } +} + #[no_mangle] pub unsafe extern "C" fn rs_quic_tx_get_version( tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32, diff --git a/rust/src/quic/frames.rs b/rust/src/quic/frames.rs index 786b0f88e1cd..48b68a2df634 100644 --- a/rust/src/quic/frames.rs +++ b/rust/src/quic/frames.rs @@ -136,6 +136,7 @@ pub(crate) struct Crypto { // We remap the Vec from tls_parser::parse_tls_extensions because of // the lifetime of TlsExtension due to references to the slice used for parsing pub extv: Vec, + pub ja3: String, } #[derive(Debug, PartialEq)] @@ -175,13 +176,59 @@ pub struct QuicTlsExtension { pub values: Vec>, } +fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec) { + ja3.push_str(","); + let mut dash = false; + for e in &exts { + match e { + TlsExtension::EllipticCurves(x) => { + for ec in x { + if dash { + ja3.push_str("-"); + } else { + dash = true; + } + ja3.push_str(&ec.0.to_string()); + } + } + _ => {} + } + } + ja3.push_str(","); + dash = false; + for e in &exts { + match e { + TlsExtension::EcPointFormats(x) => { + for ec in *x { + if dash { + ja3.push_str("-"); + } else { + dash = true; + } + ja3.push_str(&ec.to_string()); + } + } + _ => {} + } + } +} + // get interesting stuff out of parsed tls extensions -fn quic_get_tls_extensions(input: Option<&[u8]>) -> Vec { +fn quic_get_tls_extensions( + input: Option<&[u8]>, ja3: &mut String, client: bool, +) -> Vec { let mut extv = Vec::new(); if let Some(extr) = input { if let Ok((_, exts)) = parse_tls_extensions(extr) { + let mut dash = false; for e in &exts { let etype = TlsExtensionType::from(e); + if dash { + ja3.push_str("-"); + } else { + dash = true; + } + ja3.push_str(&u16::from(etype).to_string()); let mut values = Vec::new(); match e { TlsExtension::SNI(x) => { @@ -202,6 +249,9 @@ fn quic_get_tls_extensions(input: Option<&[u8]>) -> Vec { } extv.push(QuicTlsExtension { etype, values }) } + if client { + quic_tls_ja3_client_extends(ja3, exts); + } } } return extv; @@ -217,14 +267,31 @@ fn parse_crypto_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { if let Handshake(hs) = msg { match hs { ClientHello(ch) => { + let mut ja3 = String::with_capacity(256); + ja3.push_str(&u16::from(ch.version).to_string()); + let mut dash = false; + for c in &ch.ciphers { + if dash { + ja3.push_str("-"); + } else { + dash = true; + } + ja3.push_str(&u16::from(*c).to_string()); + } + ja3.push_str(","); let ciphers = ch.ciphers; - let extv = quic_get_tls_extensions(ch.ext); - return Ok((rest, Frame::Crypto(Crypto { ciphers, extv }))); + let extv = quic_get_tls_extensions(ch.ext, &mut ja3, true); + return Ok((rest, Frame::Crypto(Crypto { ciphers, extv, ja3 }))); } ServerHello(sh) => { + let mut ja3 = String::with_capacity(256); + ja3.push_str(&u16::from(sh.version).to_string()); + ja3.push_str(","); + ja3.push_str(&u16::from(sh.cipher).to_string()); + ja3.push_str(","); let ciphers = vec![sh.cipher]; - let extv = quic_get_tls_extensions(sh.ext); - return Ok((rest, Frame::Crypto(Crypto { ciphers, extv }))); + let extv = quic_get_tls_extensions(sh.ext, &mut ja3, false); + return Ok((rest, Frame::Crypto(Crypto { ciphers, extv, ja3 }))); } _ => {} } diff --git a/rust/src/quic/logger.rs b/rust/src/quic/logger.rs index 43eb35f24d2d..4d32487d647d 100644 --- a/rust/src/quic/logger.rs +++ b/rust/src/quic/logger.rs @@ -42,6 +42,9 @@ fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonEr js.close()?; } + if let Some(ja3) = &tx.ja3 { + js.set_string("ja3", ja3)?; + } if tx.extv.len() > 0 { js.open_array("extensions")?; for e in &tx.extv { diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 0f57c7239f00..00672c5ad9fc 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -37,13 +37,14 @@ pub struct QuicTransaction { pub sni: Option>, pub ua: Option>, pub extv: Vec, + pub ja3: Option, tx_data: AppLayerTxData, } impl QuicTransaction { fn new( header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, - extv: Vec, + extv: Vec, ja3: Option, ) -> Self { let cyu = Cyu::generate(&header, &data.frames); QuicTransaction { @@ -53,6 +54,7 @@ impl QuicTransaction { sni, ua, extv, + ja3, tx_data: AppLayerTxData::new(), } } @@ -100,9 +102,9 @@ impl QuicState { fn new_tx( &mut self, header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, - extb: Vec, + extb: Vec, ja3: Option, ) { - let mut tx = QuicTransaction::new(header, data, sni, ua, extb); + let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3); self.max_tx_id += 1; tx.tx_id = self.max_tx_id; self.transactions.push(tx); @@ -215,6 +217,7 @@ impl QuicState { if header.ty != QuicType::Short { let mut sni: Option> = None; let mut ua: Option> = None; + let mut ja3: Option = None; let mut extv: Vec = Vec::new(); for frame in &data.frames { match frame { @@ -233,6 +236,7 @@ impl QuicState { } } Frame::Crypto(c) => { + ja3 = Some(c.ja3.clone()); for e in &c.extv { if e.etype == TlsExtensionType::ServerName && e.values.len() > 0 @@ -251,7 +255,7 @@ impl QuicState { } } - self.new_tx(header, data, sni, ua, extv); + self.new_tx(header, data, sni, ua, extv, ja3); } } Err(_e) => { diff --git a/src/Makefile.am b/src/Makefile.am index 013d4dad1eb2..506f192ff0f9 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -242,6 +242,7 @@ noinst_HEADERS = \ detect-mark.h \ detect-metadata.h \ detect-modbus.h \ + detect-quic-ja3.h \ detect-quic-sni.h \ detect-quic-ua.h \ detect-quic-version.h \ @@ -835,6 +836,7 @@ libsuricata_c_a_SOURCES = \ detect-mark.c \ detect-metadata.c \ detect-modbus.c \ + detect-quic-ja3.c \ detect-quic-sni.c \ detect-quic-ua.c \ detect-quic-version.c \ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 4a54e429455d..4762df878c32 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -217,6 +217,7 @@ #include "detect-mqtt-publish-message.h" #include "detect-mqtt-subscribe-topic.h" #include "detect-mqtt-unsubscribe-topic.h" +#include "detect-quic-ja3.h" #include "detect-quic-sni.h" #include "detect-quic-ua.h" #include "detect-quic-version.h" @@ -652,6 +653,7 @@ void SigTableSetup(void) DetectMQTTPublishMessageRegister(); DetectMQTTSubscribeTopicRegister(); DetectMQTTUnsubscribeTopicRegister(); + DetectQuicJa3Register(); DetectQuicSniRegister(); DetectQuicUaRegister(); DetectQuicVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 57e30bb4d7f8..4c5edb49fd04 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -291,6 +291,7 @@ enum DetectKeywordId { DETECT_AL_QUIC_UA, DETECT_AL_QUIC_CYU_HASH, DETECT_AL_QUIC_CYU_STRING, + DETECT_AL_QUIC_JA3, DETECT_AL_TEMPLATE_BUFFER, DETECT_BYPASS, diff --git a/src/detect-quic-ja3.c b/src/detect-quic-ja3.c new file mode 100644 index 000000000000..973f2f959e0e --- /dev/null +++ b/src/detect-quic-ja3.c @@ -0,0 +1,169 @@ +/* Copyright (C) 2022 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. + */ + +/** + * + * Implements the quic.ja3 + */ + +#include "suricata-common.h" +#include "conf.h" +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-prefilter.h" +#include "detect-engine-mpm.h" +#include "detect-engine-content-inspection.h" +#include "detect-engine-uint.h" +#include "detect-quic-ja3.h" +#include "util-byte.h" +#include "util-unittest.h" +#include "rust.h" + +#ifdef UNITTESTS +static void DetectQuicJa3RegisterTests(void); +#endif + +#define BUFFER_NAME "quic_ja3" +#define KEYWORD_NAME "quic.ja3" +#define KEYWORD_ID DETECT_AL_QUIC_JA3 + +static int quic_ja3_id = 0; + +static int DetectQuicJa3Setup(DetectEngineCtx *, Signature *, const char *); + +static InspectionBuffer *GetJa3Data(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) { + uint32_t b_len = 0; + const uint8_t *b = NULL; + + if (rs_quic_tx_get_ja3(txv, &b, &b_len) != 1) + 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 quic.ja3: keyword + */ +void DetectQuicJa3Register(void) +{ + sigmatch_table[DETECT_AL_QUIC_JA3].name = KEYWORD_NAME; + sigmatch_table[DETECT_AL_QUIC_JA3].desc = "match Quic ja3"; + sigmatch_table[DETECT_AL_QUIC_JA3].url = "/rules/quic-keywords.html#quic-ja3"; + sigmatch_table[DETECT_AL_QUIC_JA3].Setup = DetectQuicJa3Setup; + sigmatch_table[DETECT_AL_QUIC_JA3].flags |= SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER; +#ifdef UNITTESTS + sigmatch_table[DETECT_AL_QUIC_JA3].RegisterTests = DetectQuicJa3RegisterTests; +#endif + + DetectAppLayerMpmRegister2(BUFFER_NAME, SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister, + GetJa3Data, ALPROTO_QUIC, 1); + + DetectAppLayerInspectEngineRegister2(BUFFER_NAME, ALPROTO_QUIC, SIG_FLAG_TOSERVER, 1, + DetectEngineInspectBufferGeneric, GetJa3Data); + + quic_ja3_id = DetectBufferTypeGetByName(BUFFER_NAME); +} + +/** + * \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 DetectQuicJa3Setup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectBufferSetActiveList(s, quic_ja3_id) < 0) + return -1; + + if (DetectSignatureSetAppProto(s, ALPROTO_QUIC) < 0) + return -1; + + return 0; +} + +#ifdef UNITTESTS + +/** + * \test QuicJa3TestParse01 is a test for a valid value + * + * \retval 1 on success + * \retval 0 on failure + */ +static int QuicJa3TestParse01(void) +{ + DetectEngineCtx *de_ctx = DetectEngineCtxInit(); + FAIL_IF_NULL(de_ctx); + + Signature *sig = DetectEngineAppendSig( + de_ctx, "alert ip any any -> any any (quic.ja3; content:\"googe.com\"; sid:1; rev:1;)"); + FAIL_IF_NULL(sig); + + sig = DetectEngineAppendSig( + de_ctx, "alert ip any any -> any any (quic.ja3; content:\"|00|\"; sid:2; rev:1;)"); + FAIL_IF_NULL(sig); + + DetectEngineCtxFree(de_ctx); + + PASS; +} + +/** + * \test QuicJa3TestParse03 is a test for an invalid value + * + * \retval 1 on success + * \retval 0 on failure + */ +static int QuicJa3TestParse03(void) +{ + DetectEngineCtx *de_ctx = DetectEngineCtxInit(); + FAIL_IF_NULL(de_ctx); + + Signature *sig = + DetectEngineAppendSig(de_ctx, "alert ip any any -> any any (quic.ja3:; sid:1; rev:1;)"); + FAIL_IF_NOT_NULL(sig); + + DetectEngineCtxFree(de_ctx); + + PASS; +} + +/** + * \brief this function registers unit tests for QuicJa3 + */ +void DetectQuicJa3RegisterTests(void) +{ + UtRegisterTest("QuicJa3TestParse01", QuicJa3TestParse01); + UtRegisterTest("QuicJa3TestParse03", QuicJa3TestParse03); +} + +#endif /* UNITTESTS */ diff --git a/src/detect-quic-ja3.h b/src/detect-quic-ja3.h new file mode 100644 index 000000000000..c94e1f08fd07 --- /dev/null +++ b/src/detect-quic-ja3.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2022 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 + * + */ + +#ifndef __DETECT_QUIC_JA3_H__ +#define __DETECT_QUIC_JA3_H__ + +void DetectQuicJa3Register(void); + +#endif /* __DETECT_QUIC_JA3_H__ */ From 2fcce927fc6fd45388857b7786e29ed131c63af6 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 3 Mar 2022 21:22:19 +0100 Subject: [PATCH 10/15] quic: function split in 2 and other style improvements --- rust/src/quic/quic.rs | 96 ++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 00672c5ad9fc..ad33702f309d 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -179,6 +179,51 @@ impl QuicState { return false; } + fn handle_frames(&mut self, data: QuicData, header: QuicHeader, to_server: bool) { + // no tx for the short header (data) frames + if header.ty != QuicType::Short { + let mut sni: Option> = None; + let mut ua: Option> = None; + let mut ja3: Option = None; + let mut extv: Vec = Vec::new(); + for frame in &data.frames { + match frame { + Frame::Stream(s) => { + if let Some(tags) = &s.tags { + for (tag, value) in tags { + if tag == &StreamTag::Sni { + sni = Some(value.to_vec()); + } else if tag == &StreamTag::Uaid { + ua = Some(value.to_vec()); + } + if sni.is_some() && ua.is_some() { + break; + } + } + } + } + Frame::Crypto(c) => { + ja3 = Some(c.ja3.clone()); + for e in &c.extv { + if e.etype == TlsExtensionType::ServerName && e.values.len() > 0 { + sni = Some(e.values[0].to_vec()); + } + } + extv.extend_from_slice(&c.extv); + if to_server { + self.hello_ts = true + } else { + self.hello_tc = true + } + } + _ => {} + } + } + + self.new_tx(header, data, sni, ua, extv, ja3); + } + } + fn parse(&mut self, input: &[u8], to_server: bool) -> bool { // so as to loop over multiple quic headers in one packet let mut buf = input; @@ -213,50 +258,7 @@ impl QuicState { buf = next_buf; match QuicData::from_bytes(framebuf) { Ok(data) => { - // no tx for the short header (data) frames - if header.ty != QuicType::Short { - let mut sni: Option> = None; - let mut ua: Option> = None; - let mut ja3: Option = None; - let mut extv: Vec = Vec::new(); - for frame in &data.frames { - match frame { - Frame::Stream(s) => { - if let Some(tags) = &s.tags { - for (tag, value) in tags { - if tag == &StreamTag::Sni { - sni = Some(value.to_vec()); - } else if tag == &StreamTag::Uaid { - ua = Some(value.to_vec()); - } - if sni.is_some() && ua.is_some() { - break; - } - } - } - } - Frame::Crypto(c) => { - ja3 = Some(c.ja3.clone()); - for e in &c.extv { - if e.etype == TlsExtensionType::ServerName - && e.values.len() > 0 - { - sni = Some(e.values[0].to_vec()); - } - } - extv.extend_from_slice(&c.extv); - if to_server { - self.hello_ts = true - } else { - self.hello_tc = true - } - } - _ => {} - } - } - - self.new_tx(header, data, sni, ua, extv, ja3); - } + self.handle_frames(data, header, to_server); } Err(_e) => { return false; @@ -316,8 +318,9 @@ pub unsafe extern "C" fn rs_quic_parse_tc( if state.parse(buf, false) { return AppLayerResult::ok(); + } else { + return AppLayerResult::err(); } - return AppLayerResult::err(); } #[no_mangle] @@ -330,8 +333,9 @@ pub unsafe extern "C" fn rs_quic_parse_ts( if state.parse(buf, true) { return AppLayerResult::ok(); + } else { + return AppLayerResult::err(); } - return AppLayerResult::err(); } #[no_mangle] From 48ed7853a218ee272b8c4f033eff35357a0bd651 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 4 Mar 2022 09:16:37 +0100 Subject: [PATCH 11/15] quic: move earlier the check against header.length --- rust/src/quic/parser.rs | 20 ++++++++++++++++---- rust/src/quic/quic.rs | 12 +++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs index d0f912bfccd2..bbc6cb70b45f 100644 --- a/rust/src/quic/parser.rs +++ b/rust/src/quic/parser.rs @@ -20,6 +20,7 @@ use nom::bytes::complete::take; use nom::combinator::{all_consuming, map}; use nom::number::complete::{be_u24, be_u32, be_u8}; use nom::IResult; +use std::convert::TryFrom; use rustls::quic::Version; @@ -169,7 +170,7 @@ pub struct QuicHeader { pub version_buf: Vec, pub dcid: Vec, pub scid: Vec, - pub length: usize, + pub length: u16, } #[derive(Debug, PartialEq)] @@ -295,10 +296,21 @@ impl QuicHeader { _ => rest, }; let (rest, length) = if has_length { - let (rest, plength) = quic_var_uint(rest)?; - (rest, plength as usize) + let (rest2, plength) = quic_var_uint(rest)?; + if plength > rest.len() as u64 { + return Err(nom::Err::Error(QuicError::InvalidPacket)); + } + if let Ok(length) = u16::try_from(plength) { + (rest2, length) + } else { + return Err(nom::Err::Error(QuicError::InvalidPacket)); + } } else { - (rest, rest.len()) + if let Ok(length) = u16::try_from(rest.len()) { + (rest, length) + } else { + return Err(nom::Err::Error(QuicError::InvalidPacket)); + } }; Ok(( diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index ad33702f309d..2a0047e4600d 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -142,8 +142,9 @@ impl QuicState { if framebuf.len() < 4 + hkey.sample_len() { return false; } - let mut h2 = Vec::with_capacity(hlen + header.length); - h2.extend_from_slice(&buf[..hlen + header.length]); + let h2len = hlen + usize::from(header.length); + let mut h2 = Vec::with_capacity(h2len); + h2.extend_from_slice(&buf[..h2len]); let mut h20 = h2[0]; let mut pktnum_buf = Vec::with_capacity(4); pktnum_buf.extend_from_slice(&h2[hlen..hlen + 4]); @@ -241,11 +242,8 @@ impl QuicState { self.keys = Some(keys) } } - if header.length > rest.len() { - //TODO event whenever an error condition is met - return false; - } - let (mut framebuf, next_buf) = rest.split_at(header.length); + // header.length was checked against rest.len() during parsing + let (mut framebuf, next_buf) = rest.split_at(header.length.into()); let hlen = buf.len() - rest.len(); let mut output; if self.keys.is_some() { From 9e0d20b14f175898b8fcf7a6cbec87f8ca6eaf92 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 4 Mar 2022 09:38:02 +0100 Subject: [PATCH 12/15] quic: use Suricata's own quic_tls_extension_name --- rust/src/quic/logger.rs | 77 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/rust/src/quic/logger.rs b/rust/src/quic/logger.rs index 4d32487d647d..2480c1995ac3 100644 --- a/rust/src/quic/logger.rs +++ b/rust/src/quic/logger.rs @@ -17,7 +17,72 @@ use super::quic::QuicTransaction; use crate::jsonbuilder::{JsonBuilder, JsonError}; -use std::fmt::Write; + +fn quic_tls_extension_name(e: u16) -> Option { + match e { + 0 => Some("server_name".to_string()), + 1 => Some("max_fragment_length".to_string()), + 2 => Some("client_certificate_url".to_string()), + 3 => Some("trusted_ca_keys".to_string()), + 4 => Some("truncated_hmac".to_string()), + 5 => Some("status_request".to_string()), + 6 => Some("user_mapping".to_string()), + 7 => Some("client_authz".to_string()), + 8 => Some("server_authz".to_string()), + 9 => Some("cert_type".to_string()), + 10 => Some("supported_groups".to_string()), + 11 => Some("ec_point_formats".to_string()), + 12 => Some("srp".to_string()), + 13 => Some("signature_algorithms".to_string()), + 14 => Some("use_srtp".to_string()), + 15 => Some("heartbeat".to_string()), + 16 => Some("alpn".to_string()), + 17 => Some("status_request_v2".to_string()), + 18 => Some("signed_certificate_timestamp".to_string()), + 19 => Some("client_certificate_type".to_string()), + 20 => Some("server_certificate_type".to_string()), + 21 => Some("padding".to_string()), + 22 => Some("encrypt_then_mac".to_string()), + 23 => Some("extended_master_secret".to_string()), + 24 => Some("token_binding".to_string()), + 25 => Some("cached_info".to_string()), + 26 => Some("tls_lts".to_string()), + 27 => Some("compress_certificate".to_string()), + 28 => Some("record_size_limit".to_string()), + 29 => Some("pwd_protect".to_string()), + 30 => Some("pwd_clear".to_string()), + 31 => Some("password_salt".to_string()), + 32 => Some("ticket_pinning".to_string()), + 33 => Some("tls_cert_with_extern_psk".to_string()), + 34 => Some("delegated_credentials".to_string()), + 35 => Some("session_ticket".to_string()), + 36 => Some("tlmsp".to_string()), + 37 => Some("tlmsp_proxying".to_string()), + 38 => Some("tlmsp_delegate".to_string()), + 39 => Some("supported_ekt_ciphers".to_string()), + 41 => Some("pre_shared_key".to_string()), + 42 => Some("early_data".to_string()), + 43 => Some("supported_versions".to_string()), + 44 => Some("cookie".to_string()), + 45 => Some("psk_key_exchange_modes".to_string()), + 47 => Some("certificate_authorities".to_string()), + 48 => Some("oid_filters".to_string()), + 49 => Some("post_handshake_auth".to_string()), + 50 => Some("signature_algorithms_cert".to_string()), + 51 => Some("key_share".to_string()), + 52 => Some("transparency_info".to_string()), + 53 => Some("connection_id_deprecated".to_string()), + 54 => Some("connection_id".to_string()), + 55 => Some("external_id_hash".to_string()), + 56 => Some("external_session_id".to_string()), + 57 => Some("quic_transport_parameters".to_string()), + 58 => Some("ticket_request".to_string()), + 59 => Some("dnssec_chain".to_string()), + 13172 => Some("next_protocol_negotiation".to_string()), + 65281 => Some("renegotiation_info".to_string()), + _ => None, + } +} fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { js.open_object("quic")?; @@ -48,12 +113,12 @@ fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonEr if tx.extv.len() > 0 { js.open_array("extensions")?; for e in &tx.extv { - let mut etypes = String::with_capacity(32); - match write!(&mut etypes, "{}", e.etype) { - _ => {} - } js.start_object()?; - js.set_string("type", &etypes)?; + let etype = u16::from(e.etype); + if let Some(s) = quic_tls_extension_name(etype) { + js.set_string("name", &s)?; + } + js.set_uint("type", etype.into())?; if e.values.len() > 0 { js.open_array("values")?; From 6bd8d801fcf4c26dbe341bbe6d7f7da8572cc3e3 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 4 Mar 2022 10:31:16 +0100 Subject: [PATCH 13/15] quic: parse gquic version Q039 Ticket: 5166 --- rust/src/quic/logger.rs | 3 +- rust/src/quic/parser.rs | 84 ++++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/rust/src/quic/logger.rs b/rust/src/quic/logger.rs index 2480c1995ac3..aaf60a874368 100644 --- a/rust/src/quic/logger.rs +++ b/rust/src/quic/logger.rs @@ -15,6 +15,7 @@ * 02110-1301, USA. */ +use super::parser::QuicType; use super::quic::QuicTransaction; use crate::jsonbuilder::{JsonBuilder, JsonError}; @@ -86,7 +87,7 @@ fn quic_tls_extension_name(e: u16) -> Option { fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { js.open_object("quic")?; - if tx.header.flags.is_long { + if tx.header.ty != QuicType::Short { js.set_string("version", String::from(tx.header.version).as_str())?; if let Some(sni) = &tx.sni { diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs index bbc6cb70b45f..0bf440ef1148 100644 --- a/rust/src/quic/parser.rs +++ b/rust/src/quic/parser.rs @@ -42,6 +42,7 @@ use rustls::quic::Version; pub struct QuicVersion(pub u32); impl QuicVersion { + pub const Q039: QuicVersion = QuicVersion(0x51303339); pub const Q043: QuicVersion = QuicVersion(0x51303433); pub const Q044: QuicVersion = QuicVersion(0x51303434); pub const Q045: QuicVersion = QuicVersion(0x51303435); @@ -58,6 +59,7 @@ impl QuicVersion { impl From for String { fn from(from: QuicVersion) -> Self { match from { + QuicVersion(0x51303339) => "Q039".to_string(), QuicVersion(0x51303433) => "Q043".to_string(), QuicVersion(0x51303434) => "Q044".to_string(), QuicVersion(0x51303435) => "Q045".to_string(), @@ -96,16 +98,22 @@ pub enum QuicType { Short, } +const QUIC_FLAG_MULTIPATH: u8 = 0x40; +const QUIC_FLAG_DCID_LEN: u8 = 0x8; +const QUIC_FLAG_VERSION: u8 = 0x1; + #[derive(Debug, PartialEq)] pub struct PublicFlags { pub is_long: bool, + pub raw: u8, } impl PublicFlags { pub fn new(value: u8) -> Self { let is_long = value & 0x80 == 0x80; + let raw = value; - PublicFlags { is_long } + PublicFlags { is_long, raw } } } @@ -200,22 +208,52 @@ impl QuicHeader { let (rest, first) = be_u8(input)?; let flags = PublicFlags::new(first); - if !flags.is_long { + if !flags.is_long && (flags.raw & QUIC_FLAG_MULTIPATH) == 0 { + if (flags.raw & QUIC_FLAG_VERSION) == 0 || (flags.raw & QUIC_FLAG_DCID_LEN) == 0 { + return Err(nom::Err::Error(QuicError::InvalidPacket)); + } + let (rest, dcid) = take(8_usize)(rest)?; + let (_, version_buf) = take(4_usize)(rest)?; + let (rest, version) = map(be_u32, QuicVersion)(rest)?; + let pkt_num_len = 1 + (flags.raw & 0x30) >> 4; + let (rest, _pkt_num) = take(pkt_num_len)(rest)?; + let (rest, _msg_auth_hash) = take(12_usize)(rest)?; + if let Ok(plength) = u16::try_from(rest.len()) { + return Ok(( + rest, + QuicHeader { + flags, + ty: QuicType::Initial, + version: version, + version_buf: version_buf.to_vec(), + dcid: dcid.to_vec(), + scid: Vec::new(), + length: plength, + }, + )); + } else { + return Err(nom::Err::Error(QuicError::InvalidPacket)); + } + } else if !flags.is_long { // Decode short header let (rest, dcid) = take(dcid_len)(rest)?; - return Ok(( - rest, - QuicHeader { - flags, - ty: QuicType::Short, - version: QuicVersion(0), - version_buf: Vec::new(), - dcid: dcid.to_vec(), - scid: Vec::new(), - length: 0, - }, - )); + if let Ok(plength) = u16::try_from(rest.len()) { + return Ok(( + rest, + QuicHeader { + flags, + ty: QuicType::Short, + version: QuicVersion(0), + version_buf: Vec::new(), + dcid: dcid.to_vec(), + scid: Vec::new(), + length: plength, + }, + )); + } else { + return Err(nom::Err::Error(QuicError::InvalidPacket)); + } } else { // Decode Long header let (_, version_buf) = take(4_usize)(rest)?; @@ -345,7 +383,13 @@ mod tests { #[test] fn public_flags_test() { let pf = PublicFlags::new(0xcb); - assert_eq!(PublicFlags { is_long: true }, pf); + assert_eq!( + PublicFlags { + is_long: true, + raw: 0xcb + }, + pf + ); } const TEST_DEFAULT_CID_LENGTH: usize = 8; @@ -358,7 +402,10 @@ mod tests { QuicHeader::from_bytes(data.as_ref(), TEST_DEFAULT_CID_LENGTH).unwrap(); assert_eq!( QuicHeader { - flags: PublicFlags { is_long: true }, + flags: PublicFlags { + is_long: true, + raw: 0xcb + }, ty: QuicType::Initial, version: QuicVersion(0xff00001d), version_buf: vec![0xff, 0x00, 0x00, 0x1d], @@ -382,7 +429,10 @@ mod tests { assert_eq!( QuicHeader { - flags: PublicFlags { is_long: true }, + flags: PublicFlags { + is_long: true, + raw: 0xff + }, ty: QuicType::Initial, version: QuicVersion::Q044, version_buf: vec![0x51, 0x30, 0x34, 0x34], From 190d858ca3f7fc16b5dbc5b85fc1e04f258eb158 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sun, 6 Mar 2022 22:01:39 +0100 Subject: [PATCH 14/15] quic: check against right remaining buffer --- rust/src/quic/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs index 0bf440ef1148..c20e273166f2 100644 --- a/rust/src/quic/parser.rs +++ b/rust/src/quic/parser.rs @@ -335,7 +335,7 @@ impl QuicHeader { }; let (rest, length) = if has_length { let (rest2, plength) = quic_var_uint(rest)?; - if plength > rest.len() as u64 { + if plength > rest2.len() as u64 { return Err(nom::Err::Error(QuicError::InvalidPacket)); } if let Ok(length) = u16::try_from(plength) { From 71bc1fd2d340f2350e269df8c43450accb728a86 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Thu, 10 Mar 2022 13:47:03 +0100 Subject: [PATCH 15/15] quic: detect ja3 from server with keyword quic.ja3s As the ja3 standard names it so, with an s, when it comes from the server to the client. --- src/detect-engine-register.h | 1 + src/detect-quic-ja3.c | 29 ++++++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 4c5edb49fd04..28597bb366de 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -292,6 +292,7 @@ enum DetectKeywordId { DETECT_AL_QUIC_CYU_HASH, DETECT_AL_QUIC_CYU_STRING, DETECT_AL_QUIC_JA3, + DETECT_AL_QUIC_JA3S, DETECT_AL_TEMPLATE_BUFFER, DETECT_BYPASS, diff --git a/src/detect-quic-ja3.c b/src/detect-quic-ja3.c index 973f2f959e0e..05df3c495b25 100644 --- a/src/detect-quic-ja3.c +++ b/src/detect-quic-ja3.c @@ -38,11 +38,8 @@ static void DetectQuicJa3RegisterTests(void); #endif -#define BUFFER_NAME "quic_ja3" -#define KEYWORD_NAME "quic.ja3" -#define KEYWORD_ID DETECT_AL_QUIC_JA3 - static int quic_ja3_id = 0; +static int quic_ja3s_id = 0; static int DetectQuicJa3Setup(DetectEngineCtx *, Signature *, const char *); @@ -71,8 +68,8 @@ static InspectionBuffer *GetJa3Data(DetectEngineThreadCtx *det_ctx, */ void DetectQuicJa3Register(void) { - sigmatch_table[DETECT_AL_QUIC_JA3].name = KEYWORD_NAME; - sigmatch_table[DETECT_AL_QUIC_JA3].desc = "match Quic ja3"; + sigmatch_table[DETECT_AL_QUIC_JA3].name = "quic.ja3"; + sigmatch_table[DETECT_AL_QUIC_JA3].desc = "match Quic ja3 from client to server"; sigmatch_table[DETECT_AL_QUIC_JA3].url = "/rules/quic-keywords.html#quic-ja3"; sigmatch_table[DETECT_AL_QUIC_JA3].Setup = DetectQuicJa3Setup; sigmatch_table[DETECT_AL_QUIC_JA3].flags |= SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER; @@ -80,13 +77,27 @@ void DetectQuicJa3Register(void) sigmatch_table[DETECT_AL_QUIC_JA3].RegisterTests = DetectQuicJa3RegisterTests; #endif - DetectAppLayerMpmRegister2(BUFFER_NAME, SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister, + DetectAppLayerMpmRegister2("quic_ja3", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister, + GetJa3Data, ALPROTO_QUIC, 1); + + DetectAppLayerInspectEngineRegister2("quic_ja3", ALPROTO_QUIC, SIG_FLAG_TOSERVER, 1, + DetectEngineInspectBufferGeneric, GetJa3Data); + + quic_ja3_id = DetectBufferTypeGetByName("quic_ja3"); + + sigmatch_table[DETECT_AL_QUIC_JA3S].name = "quic.ja3s"; + sigmatch_table[DETECT_AL_QUIC_JA3S].desc = "match Quic ja3 from server to client"; + sigmatch_table[DETECT_AL_QUIC_JA3S].url = "/rules/quic-keywords.html#quic-ja3"; + sigmatch_table[DETECT_AL_QUIC_JA3S].Setup = DetectQuicJa3Setup; + sigmatch_table[DETECT_AL_QUIC_JA3S].flags |= SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER; + + DetectAppLayerMpmRegister2("quic_ja3s", SIG_FLAG_TOCLIENT, 2, PrefilterGenericMpmRegister, GetJa3Data, ALPROTO_QUIC, 1); - DetectAppLayerInspectEngineRegister2(BUFFER_NAME, ALPROTO_QUIC, SIG_FLAG_TOSERVER, 1, + DetectAppLayerInspectEngineRegister2("quic_ja3s", ALPROTO_QUIC, SIG_FLAG_TOCLIENT, 1, DetectEngineInspectBufferGeneric, GetJa3Data); - quic_ja3_id = DetectBufferTypeGetByName(BUFFER_NAME); + quic_ja3s_id = DetectBufferTypeGetByName("quic_ja3s"); } /**