quic: handle fragmented hello over multiple packets

Ticket: 7556

To do so, we need to add 2 buffers (one for each direction)
to the QuicState structure, so that on parsing the second packet
with hello/crypto fragment, we still have the data of the first
hello/crypto fragment.

Use a hardcoded limit so that these buffers cannot grow indefinitely
and set an event when reaching the limit
pull/12631/head
Philippe Antoine 2 weeks ago committed by Victor Julien
parent 68adc87bd2
commit f295cc059d

@ -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;)

@ -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<Frame>, QuicError> {
pub(crate) fn decode_frames<'a>(
input: &'a [u8], past_frag: &'a [u8], past_fraglen: u32,
) -> IResult<&'a [u8], Vec<Frame>, 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))

@ -392,8 +392,10 @@ impl QuicHeader {
}
impl QuicData {
pub(crate) fn from_bytes(input: &[u8]) -> Result<QuicData, QuicError> {
let (_, frames) = Frame::decode_frames(input)?;
pub(crate) fn from_bytes(
input: &[u8], past_frag: &[u8], past_fraglen: u32,
) -> Result<QuicData, QuicError> {
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 {

@ -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<QuicKeys>,
/// crypto fragment data already seen and reassembled to client
crypto_frag_tc: Vec<u8>,
/// 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<u8>,
/// number of bytes set in crypto fragment data to server
crypto_fraglen_ts: u32,
hello_tc: bool,
hello_ts: bool,
transactions: VecDeque<QuicTransaction>,
@ -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<Vec<u8>>, ua: Option<Vec<u8>>,
extb: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<String>, 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<String> = None;
let mut ja4: Option<String> = None;
let mut extv: Vec<QuicTlsExtension> = 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);
}

Loading…
Cancel
Save