]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
quic: handle fragmented hello over multiple packets
authorPhilippe Antoine <pantoine@oisf.net>
Mon, 17 Feb 2025 10:13:20 +0000 (11:13 +0100)
committerVictor Julien <victor@inliniac.net>
Wed, 19 Feb 2025 15:34:12 +0000 (16:34 +0100)
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

rules/quic-events.rules
rust/src/quic/frames.rs
rust/src/quic/parser.rs
rust/src/quic/quic.rs

index 41e9628265cc212861c25ff9d54b8a780e5a5b83..2267ad6234203d297c8eaf0ce9db5feafc406ad8 100644 (file)
@@ -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;)
index c91b5b8a2e013eb30b9c066cd223dcf58965b7f9..a4010f4488a989bc5644a9ad6f04eff657df2157 100644 (file)
@@ -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))
index fbb8195ce12adfa43f4765afea97d3d6d2dc15c4..25527439941333c14d23e2818b7ff0e0d00457cc 100644 (file)
@@ -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 {
index 404d70f3ec07546a85ba230e2c9c18c5c50d33d2..606f962bebb1f29df6358ebba22982496a91e3fb 100644 (file)
@@ -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);
                         }