Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rules/quic-events.rules
Original file line number Diff line number Diff line change
Expand Up @@ -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;)
63 changes: 54 additions & 9 deletions rust/src/quic/frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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())),
Expand All @@ -540,12 +550,15 @@ impl Frame {
Ok((rest, value))
}

pub(crate) fn decode_frames(input: &[u8]) -> IResult<&[u8], Vec<Frame>, 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<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 {
Expand All @@ -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))
Expand Down
15 changes: 11 additions & 4 deletions rust/src/quic/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -392,8 +396,10 @@ impl QuicHeader {
}

impl QuicData {
pub(crate) fn from_bytes(input: &[u8]) -> Result<QuicData, QuicError> {
let (_, frames) = all_consuming(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 })
}
}
Expand Down Expand Up @@ -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 {
Expand Down
104 changes: 87 additions & 17 deletions rust/src/quic/quic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -103,8 +105,17 @@ 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,
has_retried: bool,
transactions: VecDeque<QuicTransaction>,
}

Expand All @@ -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(),
}
}
Expand Down Expand Up @@ -144,10 +160,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);
}

Expand Down Expand Up @@ -225,6 +245,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) => {
Expand All @@ -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());
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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);
}
Expand Down
Loading