diff --git a/rules/quic-events.rules b/rules/quic-events.rules index 41e9628265..2267ad6234 100644 --- a/rules/quic-events.rules +++ b/rules/quic-events.rules @@ -6,3 +6,4 @@ alert quic any any -> any any (msg:"SURICATA QUIC failed decrypt"; app-layer-event:quic.failed_decrypt; classtype:protocol-command-decode; sid:2231000; rev:1;) alert quic any any -> any any (msg:"SURICATA QUIC error on data"; app-layer-event:quic.error_on_data; classtype:protocol-command-decode; sid:2231001; rev:1;) +alert quic any any -> any any (msg:"SURICATA QUIC crypto fragments too long"; app-layer-event:quic.crypto_frag_too_long; classtype:protocol-command-decode; sid:2231002; rev:1;) diff --git a/rust/src/quic/frames.rs b/rust/src/quic/frames.rs index c91b5b8a2e..a4010f4488 100644 --- a/rust/src/quic/frames.rs +++ b/rust/src/quic/frames.rs @@ -16,6 +16,7 @@ */ use super::error::QuicError; +use super::quic::QUIC_MAX_CRYPTO_FRAG_LEN; use crate::ja4::*; use crate::quic::parser::quic_var_uint; use nom7::bytes::complete::take; @@ -549,12 +550,15 @@ impl Frame { Ok((rest, value)) } - pub(crate) fn decode_frames(input: &[u8]) -> IResult<&[u8], Vec, QuicError> { + pub(crate) fn decode_frames<'a>( + input: &'a [u8], past_frag: &'a [u8], past_fraglen: u32, + ) -> IResult<&'a [u8], Vec, QuicError> { let (rest, mut frames) = all_consuming(many0(complete(Frame::decode_frame)))(input)?; - // reassemble crypto fragments : first find total size - let mut crypto_max_size = 0; + // we use the already seen past fragment data + let mut crypto_max_size = past_frag.len() as u64; let mut crypto_total_size = 0; + // reassemble crypto fragments : first find total size for f in &frames { if let Frame::CryptoFrag(c) = f { if crypto_max_size < c.offset + c.length { @@ -563,20 +567,52 @@ impl Frame { crypto_total_size += c.length; } } - if crypto_max_size > 0 && crypto_total_size == crypto_max_size { + if crypto_max_size > 0 && crypto_max_size < QUIC_MAX_CRYPTO_FRAG_LEN { // we have some, and no gaps from offset 0 let mut d = vec![0; crypto_max_size as usize]; + d[..past_frag.len()].clone_from_slice(past_frag); for f in &frames { if let Frame::CryptoFrag(c) = f { d[c.offset as usize..(c.offset + c.length) as usize].clone_from_slice(&c.data); } } - if let Ok((_, msg)) = parse_tls_message_handshake(&d) { - if let Some(c) = parse_quic_handshake(msg) { - // add a parsed crypto frame - frames.push(c); + // check that we have enough data, some new data, and data for the first byte + if crypto_total_size + past_fraglen as u64 >= crypto_max_size && crypto_total_size > 0 { + match parse_tls_message_handshake(&d) { + Ok((_, msg)) => { + if let Some(c) = parse_quic_handshake(msg) { + // add a parsed crypto frame + frames.push(c); + } + } + Err(nom7::Err::Incomplete(_)) => { + // this means the current packet does not have all the hanshake data yet + let frag = CryptoFrag { + offset: crypto_total_size + past_fraglen as u64, + length: d.len() as u64, + data: d.to_vec(), + }; + frames.push(Frame::CryptoFrag(frag)); + } + _ => {} } + } else { + // pass in offset the number of bytes set in data + let frag = CryptoFrag { + offset: crypto_total_size + past_fraglen as u64, + length: d.len() as u64, + data: d.to_vec(), + }; + frames.push(Frame::CryptoFrag(frag)); } + } else if crypto_max_size >= QUIC_MAX_CRYPTO_FRAG_LEN { + // just notice the engine that we have a big crypto fragment without supplying data + let frag = CryptoFrag { + offset: 0, + length: crypto_max_size, + data: Vec::new(), + }; + frames.push(Frame::CryptoFrag(frag)); } Ok((rest, frames)) diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs index fbb8195ce1..2552743994 100644 --- a/rust/src/quic/parser.rs +++ b/rust/src/quic/parser.rs @@ -392,8 +392,10 @@ impl QuicHeader { } impl QuicData { - pub(crate) fn from_bytes(input: &[u8]) -> Result { - let (_, frames) = Frame::decode_frames(input)?; + pub(crate) fn from_bytes( + input: &[u8], past_frag: &[u8], past_fraglen: u32, + ) -> Result { + let (_, frames) = Frame::decode_frames(input, past_frag, past_fraglen)?; Ok(QuicData { frames }) } } @@ -467,7 +469,8 @@ mod tests { header ); - let data = QuicData::from_bytes(rest).unwrap(); + let past_frag = Vec::new(); + let data = QuicData::from_bytes(rest, &past_frag, 0).unwrap(); assert_eq!( QuicData { frames: vec![Frame::Stream(Stream { diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 404d70f3ec..606f962beb 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -36,12 +36,14 @@ static mut ALPROTO_QUIC: AppProto = ALPROTO_UNKNOWN; const DEFAULT_DCID_LEN: usize = 16; const PKT_NUM_BUF_MAX_LEN: usize = 4; +pub(super) const QUIC_MAX_CRYPTO_FRAG_LEN: u64 = 65535; #[derive(FromPrimitive, Debug, AppLayerEvent)] pub enum QuicEvent { FailedDecrypt, ErrorOnData, ErrorOnHeader, + CryptoFragTooLong, } #[derive(Debug)] @@ -108,6 +110,14 @@ pub struct QuicState { state_data: AppLayerStateData, max_tx_id: u64, keys: Option, + /// crypto fragment data already seen and reassembled to client + crypto_frag_tc: Vec, + /// number of bytes set in crypto fragment data to client + crypto_fraglen_tc: u32, + /// crypto fragment data already seen and reassembled to server + crypto_frag_ts: Vec, + /// number of bytes set in crypto fragment data to server + crypto_fraglen_ts: u32, hello_tc: bool, hello_ts: bool, transactions: VecDeque, @@ -119,6 +129,10 @@ impl Default for QuicState { state_data: AppLayerStateData::new(), max_tx_id: 0, keys: None, + crypto_frag_tc: Vec::new(), + crypto_frag_ts: Vec::new(), + crypto_fraglen_tc: 0, + crypto_fraglen_ts: 0, hello_tc: false, hello_ts: false, transactions: VecDeque::new(), @@ -149,10 +163,14 @@ impl QuicState { fn new_tx( &mut self, header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, extb: Vec, ja3: Option, ja4: Option, client: bool, + frag_long: bool, ) { let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, ja4, client); self.max_tx_id += 1; tx.tx_id = self.max_tx_id; + if frag_long { + tx.tx_data.set_event(QuicEvent::CryptoFragTooLong as u8); + } self.transactions.push_back(tx); } @@ -230,6 +248,7 @@ impl QuicState { let mut ja3: Option = None; let mut ja4: Option = None; let mut extv: Vec = Vec::new(); + let mut frag_long = false; for frame in &data.frames { match frame { Frame::Stream(s) => { @@ -246,6 +265,24 @@ impl QuicState { } } } + Frame::CryptoFrag(frag) => { + // means we had some fragments but not full TLS hello + // save it for a later packet + if to_server { + // use a hardcoded limit to not grow indefinitely + if frag.length < QUIC_MAX_CRYPTO_FRAG_LEN { + self.crypto_frag_ts.clone_from(&frag.data); + self.crypto_fraglen_ts = frag.offset as u32; + } else { + frag_long = true; + } + } else if frag.length < QUIC_MAX_CRYPTO_FRAG_LEN { + self.crypto_frag_tc.clone_from(&frag.data); + self.crypto_fraglen_tc = frag.offset as u32; + } else { + frag_long = true; + } + } Frame::Crypto(c) => { if let Some(ja3str) = &c.ja3 { ja3 = Some(ja3str.clone()); @@ -273,7 +310,7 @@ impl QuicState { _ => {} } } - self.new_tx(header, data, sni, ua, extv, ja3, ja4, to_server); + self.new_tx(header, data, sni, ua, extv, ja3, ja4, to_server, frag_long); } fn set_event_notx(&mut self, event: QuicEvent, header: QuicHeader, client: bool) { @@ -332,11 +369,31 @@ impl QuicState { None, None, to_server, + false, ); continue; } - match QuicData::from_bytes(framebuf) { + let mut frag = Vec::new(); + // take the current fragment and reset it in the state + let past_frag = if to_server { + std::mem::swap(&mut self.crypto_frag_ts, &mut frag); + &frag + } else { + std::mem::swap(&mut self.crypto_frag_tc, &mut frag); + &frag + }; + let past_fraglen = if to_server { + self.crypto_fraglen_ts + } else { + self.crypto_fraglen_tc + }; + if to_server { + self.crypto_fraglen_ts = 0 + } else { + self.crypto_fraglen_tc = 0 + } + match QuicData::from_bytes(framebuf, past_frag, past_fraglen) { Ok(data) => { self.handle_frames(data, header, to_server); }