diff --git a/rules/quic-events.rules b/rules/quic-events.rules
index 41e9628265cc..2267ad623420 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 0266850fd38a..a4010f4488a9 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;
@@ -198,6 +199,14 @@ fn parse_ack_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> {
))
}
+fn parse_ack3_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> {
+ let (rest, ack) = parse_ack_frame(input)?;
+ let (rest, _ect0_count) = quic_var_uint(rest)?;
+ let (rest, _ect1_count) = quic_var_uint(rest)?;
+ let (rest, _ecn_count) = quic_var_uint(rest)?;
+ Ok((rest, ack))
+}
+
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QuicTlsExtension {
pub etype: TlsExtensionType,
@@ -531,6 +540,7 @@ impl Frame {
0x00 => parse_padding_frame(rest)?,
0x01 => (rest, Frame::Ping),
0x02 => parse_ack_frame(rest)?,
+ 0x03 => parse_ack3_frame(rest)?,
0x06 => parse_crypto_frame(rest)?,
0x08 => parse_crypto_stream_frame(rest)?,
_ => ([].as_ref(), Frame::Unknown(rest.to_vec())),
@@ -540,12 +550,15 @@ impl Frame {
Ok((rest, value))
}
- pub(crate) fn decode_frames(input: &[u8]) -> IResult<&[u8], Vec, QuicError> {
- let (rest, mut frames) = many0(complete(Frame::decode_frame))(input)?;
+ 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 {
@@ -554,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 126973633bac..3fba85040b37 100644
--- a/rust/src/quic/parser.rs
+++ b/rust/src/quic/parser.rs
@@ -17,7 +17,7 @@
use super::error::QuicError;
use super::frames::Frame;
use nom7::bytes::complete::take;
-use nom7::combinator::{all_consuming, map};
+use nom7::combinator::map;
use nom7::number::complete::{be_u24, be_u32, be_u8};
use nom7::IResult;
use std::convert::TryFrom;
@@ -357,6 +357,10 @@ impl QuicHeader {
rest
}
}
+ QuicType::Retry => {
+ // opaque retry token and 16 bytes retry integrity tag
+ &rest[rest.len()..]
+ }
_ => rest,
};
let (rest, length) = if has_length {
@@ -392,8 +396,10 @@ impl QuicHeader {
}
impl QuicData {
- pub(crate) fn from_bytes(input: &[u8]) -> Result {
- let (_, frames) = all_consuming(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 +473,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 d0bff24cec67..2d0e9da95d5c 100644
--- a/rust/src/quic/quic.rs
+++ b/rust/src/quic/quic.rs
@@ -31,12 +31,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)]
@@ -103,8 +105,17 @@ 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,
+ has_retried: bool,
transactions: VecDeque,
}
@@ -114,8 +125,13 @@ 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,
+ has_retried: false,
transactions: VecDeque::new(),
}
}
@@ -144,10 +160,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);
}
@@ -225,6 +245,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) => {
@@ -241,6 +262,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());
@@ -268,7 +307,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) {
@@ -297,12 +336,39 @@ impl QuicState {
// unprotect/decrypt packet
if self.keys.is_none() && header.ty == QuicType::Initial {
self.keys = quic_keys_initial(u32::from(header.version), &header.dcid);
+ } else if !to_server
+ && self.keys.is_some()
+ && header.ty == QuicType::Retry
+ && !self.has_retried
+ {
+ // a retry packet discards the current keys, client will resend an initial packet with new keys
+ self.hello_ts = false;
+ self.keys = None;
+ // RFC 9000 17.2.5.2 After the client has received and processed an Initial or Retry packet
+ // from the server, it MUST discard any subsequent Retry packets that it receives.
+ self.has_retried = true;
}
// header.length was checked against rest.len() during parsing
let (mut framebuf, next_buf) = rest.split_at(header.length.into());
+ if header.ty != QuicType::Initial {
+ // only version is interesting, no frames
+ self.new_tx(
+ header,
+ QuicData { frames: Vec::new() },
+ None,
+ None,
+ Vec::new(),
+ None,
+ None,
+ to_server,
+ false,
+ );
+ buf = next_buf;
+ continue;
+ }
let hlen = buf.len() - rest.len();
let mut output;
- if self.keys.is_some() {
+ if self.keys.is_some() && !framebuf.is_empty() {
output = Vec::with_capacity(framebuf.len() + 4);
if let Ok(dlen) =
self.decrypt(to_server, &header, framebuf, buf, hlen, &mut output)
@@ -316,22 +382,26 @@ impl QuicState {
}
buf = next_buf;
- if header.ty != QuicType::Initial {
- // only version is interesting, no frames
- self.new_tx(
- header,
- QuicData { frames: Vec::new() },
- None,
- None,
- Vec::new(),
- None,
- None,
- to_server,
- );
- continue;
+ 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) {
+ match QuicData::from_bytes(framebuf, past_frag, past_fraglen) {
Ok(data) => {
self.handle_frames(data, header, to_server);
}