]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
quic: ja3 computation and logging and detection
authorPhilippe Antoine <contact@catenacyber.fr>
Fri, 25 Mar 2022 13:53:09 +0000 (14:53 +0100)
committerPhilippe Antoine <pantoine@oisf.net>
Tue, 2 Aug 2022 12:54:27 +0000 (14:54 +0200)
Logging as is done in TLS.

Detection using the generic generic ja3.string keyword

Ticket: #5143

rust/src/quic/detect.rs
rust/src/quic/frames.rs
rust/src/quic/logger.rs
rust/src/quic/quic.rs
src/detect-tls-ja3-string.c
src/detect-tls-ja3s-string.c

index cfdd43fc16b6e09b07817c7f3ec959653c732d7a..7e9019bef0047997616d11b7fe0440fb9f4aa4fd 100644 (file)
@@ -48,6 +48,21 @@ pub unsafe extern "C" fn rs_quic_tx_get_sni(
     }
 }
 
+#[no_mangle]
+pub unsafe extern "C" fn rs_quic_tx_get_ja3(
+    tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
+) -> u8 {
+    if let Some(ja3) = &tx.ja3 {
+        *buffer = ja3.as_ptr();
+        *buffer_len = ja3.len() as u32;
+        1
+    } else {
+        *buffer = ptr::null();
+        *buffer_len = 0;
+        0
+    }
+}
+
 #[no_mangle]
 pub unsafe extern "C" fn rs_quic_tx_get_version(
     tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
index d44e3e1951f376d949c0105d24c9d0992c6ca444..e1dedba5af9ce1bd6ba7c88ada30d8f2de2f31e4 100644 (file)
@@ -136,6 +136,7 @@ pub(crate) struct Crypto {
     // We remap the Vec<TlsExtension> from tls_parser::parse_tls_extensions because of
     // the lifetime of TlsExtension due to references to the slice used for parsing
     pub extv: Vec<QuicTlsExtension>,
+    pub ja3: String,
 }
 
 #[derive(Debug, PartialEq)]
@@ -190,13 +191,59 @@ pub struct QuicTlsExtension {
     pub values: Vec<Vec<u8>>,
 }
 
+fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec<TlsExtension>) {
+    ja3.push_str(",");
+    let mut dash = false;
+    for e in &exts {
+        match e {
+            TlsExtension::EllipticCurves(x) => {
+                for ec in x {
+                    if dash {
+                        ja3.push_str("-");
+                    } else {
+                        dash = true;
+                    }
+                    ja3.push_str(&ec.0.to_string());
+                }
+            }
+            _ => {}
+        }
+    }
+    ja3.push_str(",");
+    dash = false;
+    for e in &exts {
+        match e {
+            TlsExtension::EcPointFormats(x) => {
+                for ec in *x {
+                    if dash {
+                        ja3.push_str("-");
+                    } else {
+                        dash = true;
+                    }
+                    ja3.push_str(&ec.to_string());
+                }
+            }
+            _ => {}
+        }
+    }
+}
+
 // get interesting stuff out of parsed tls extensions
-fn quic_get_tls_extensions(input: Option<&[u8]>) -> Vec<QuicTlsExtension> {
+fn quic_get_tls_extensions(
+    input: Option<&[u8]>, ja3: &mut String, client: bool,
+) -> Vec<QuicTlsExtension> {
     let mut extv = Vec::new();
     if let Some(extr) = input {
         if let Ok((_, exts)) = parse_tls_extensions(extr) {
+            let mut dash = false;
             for e in &exts {
                 let etype = TlsExtensionType::from(e);
+                if dash {
+                    ja3.push_str("-");
+                } else {
+                    dash = true;
+                }
+                ja3.push_str(&u16::from(etype).to_string());
                 let mut values = Vec::new();
                 match e {
                     TlsExtension::SNI(x) => {
@@ -217,6 +264,9 @@ fn quic_get_tls_extensions(input: Option<&[u8]>) -> Vec<QuicTlsExtension> {
                 }
                 extv.push(QuicTlsExtension { etype, values })
             }
+            if client {
+                quic_tls_ja3_client_extends(ja3, exts);
+            }
         }
     }
     return extv;
@@ -231,14 +281,32 @@ fn parse_crypto_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> {
         if let Handshake(hs) = msg {
             match hs {
                 ClientHello(ch) => {
+                    let mut ja3 = String::with_capacity(256);
+                    ja3.push_str(&u16::from(ch.version).to_string());
+                    ja3.push_str(",");
+                    let mut dash = false;
+                    for c in &ch.ciphers {
+                        if dash {
+                            ja3.push_str("-");
+                        } else {
+                            dash = true;
+                        }
+                        ja3.push_str(&u16::from(*c).to_string());
+                    }
+                    ja3.push_str(",");
                     let ciphers = ch.ciphers;
-                    let extv = quic_get_tls_extensions(ch.ext);
-                    return Ok((rest, Frame::Crypto(Crypto { ciphers, extv })));
+                    let extv = quic_get_tls_extensions(ch.ext, &mut ja3, true);
+                    return Ok((rest, Frame::Crypto(Crypto { ciphers, extv, ja3 })));
                 }
                 ServerHello(sh) => {
+                    let mut ja3 = String::with_capacity(256);
+                    ja3.push_str(&u16::from(sh.version).to_string());
+                    ja3.push_str(",");
+                    ja3.push_str(&u16::from(sh.cipher).to_string());
+                    ja3.push_str(",");
                     let ciphers = vec![sh.cipher];
-                    let extv = quic_get_tls_extensions(sh.ext);
-                    return Ok((rest, Frame::Crypto(Crypto { ciphers, extv })));
+                    let extv = quic_get_tls_extensions(sh.ext, &mut ja3, false);
+                    return Ok((rest, Frame::Crypto(Crypto { ciphers, extv, ja3 })));
                 }
                 _ => {}
             }
index 8ec70f058e1de6fa63e3e562babe2ee5e55d52bd..c6df260fc193540d9174f49029cfdc5905e0af2d 100644 (file)
@@ -18,6 +18,9 @@
 use super::parser::QuicType;
 use super::quic::QuicTransaction;
 use crate::jsonbuilder::{JsonBuilder, JsonError};
+use digest::Digest;
+use digest::Update;
+use md5::Md5;
 
 fn quic_tls_extension_name(e: u16) -> Option<String> {
     match e {
@@ -108,6 +111,17 @@ fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonEr
         js.close()?;
     }
 
+    if let Some(ja3) = &tx.ja3 {
+        if tx.client {
+            js.open_object("ja3")?;
+        } else {
+            js.open_object("ja3s")?;
+        }
+        let hash = format!("{:x}", Md5::new().chain(&ja3).finalize());
+        js.set_string("hash", &hash)?;
+        js.set_string("string", ja3)?;
+        js.close()?;
+    }
     if tx.extv.len() > 0 {
         js.open_array("extensions")?;
         for e in &tx.extv {
index b51b4e1681563f1ec1e3546b1f2a641dcbbc98ff..573579209f5978386e8f3eca3f97771fb195fe95 100644 (file)
@@ -39,13 +39,15 @@ pub struct QuicTransaction {
     pub sni: Option<Vec<u8>>,
     pub ua: Option<Vec<u8>>,
     pub extv: Vec<QuicTlsExtension>,
+    pub ja3: Option<String>,
+    pub client: bool,
     tx_data: AppLayerTxData,
 }
 
 impl QuicTransaction {
     fn new(
         header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>,
-        extv: Vec<QuicTlsExtension>,
+        extv: Vec<QuicTlsExtension>, ja3: Option<String>, client: bool,
     ) -> Self {
         let cyu = Cyu::generate(&header, &data.frames);
         QuicTransaction {
@@ -55,6 +57,8 @@ impl QuicTransaction {
             sni,
             ua,
             extv,
+            ja3,
+            client,
             tx_data: AppLayerTxData::new(),
         }
     }
@@ -102,9 +106,9 @@ impl QuicState {
 
     fn new_tx(
         &mut self, header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>,
-        extb: Vec<QuicTlsExtension>,
+        extb: Vec<QuicTlsExtension>, ja3: Option<String>, client: bool,
     ) {
-        let mut tx = QuicTransaction::new(header, data, sni, ua, extb);
+        let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, client);
         self.max_tx_id += 1;
         tx.tx_id = self.max_tx_id;
         self.transactions.push(tx);
@@ -181,6 +185,7 @@ impl QuicState {
     fn handle_frames(&mut self, data: QuicData, header: QuicHeader, to_server: bool) {
         let mut sni: Option<Vec<u8>> = None;
         let mut ua: Option<Vec<u8>> = None;
+        let mut ja3: Option<String> = None;
         let mut extv: Vec<QuicTlsExtension> = Vec::new();
         for frame in &data.frames {
             match frame {
@@ -199,6 +204,7 @@ impl QuicState {
                     }
                 }
                 Frame::Crypto(c) => {
+                    ja3 = Some(c.ja3.clone());
                     for e in &c.extv {
                         if e.etype == TlsExtensionType::ServerName && e.values.len() > 0 {
                             sni = Some(e.values[0].to_vec());
@@ -214,7 +220,7 @@ impl QuicState {
                 _ => {}
             }
         }
-        self.new_tx(header, data, sni, ua, extv);
+        self.new_tx(header, data, sni, ua, extv, ja3, to_server);
     }
 
     fn parse(&mut self, input: &[u8], to_server: bool) -> bool {
@@ -261,6 +267,8 @@ impl QuicState {
                             None,
                             None,
                             Vec::new(),
+                            None,
+                            to_server,
                         );
                         continue;
                     }
index 76c44c8c0d56c0080cf2f94a3b6fb8d6248dda57..5c7b5e5919c5448ddd1138040831f8f88f9bdc42 100644 (file)
@@ -68,6 +68,26 @@ static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx,
        void *txv, const int list_id);
 static int g_tls_ja3_str_buffer_id = 0;
 
+static InspectionBuffer *GetJa3Data(DetectEngineThreadCtx *det_ctx,
+        const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv,
+        const int list_id)
+{
+    InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id);
+    if (buffer->inspect == NULL) {
+        uint32_t b_len = 0;
+        const uint8_t *b = NULL;
+
+        if (rs_quic_tx_get_ja3(txv, &b, &b_len) != 1)
+            return NULL;
+        if (b == NULL || b_len == 0)
+            return NULL;
+
+        InspectionBufferSetup(det_ctx, list_id, buffer, b, b_len);
+        InspectionBufferApplyTransforms(buffer, transforms);
+    }
+    return buffer;
+}
+
 /**
  * \brief Registration function for keyword: ja3.string
  */
@@ -90,6 +110,12 @@ void DetectTlsJa3StringRegister(void)
     DetectAppLayerMpmRegister2("ja3.string", SIG_FLAG_TOSERVER, 2,
             PrefilterGenericMpmRegister, GetData, ALPROTO_TLS, 0);
 
+    DetectAppLayerMpmRegister2("ja3.string", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister,
+            GetJa3Data, ALPROTO_QUIC, 1);
+
+    DetectAppLayerInspectEngineRegister2("ja3.string", ALPROTO_QUIC, SIG_FLAG_TOSERVER, 1,
+            DetectEngineInspectBufferGeneric, GetJa3Data);
+
     DetectBufferTypeSetDescriptionByName("ja3.string", "TLS JA3 string");
 
     g_tls_ja3_str_buffer_id = DetectBufferTypeGetByName("ja3.string");
@@ -110,8 +136,10 @@ static int DetectTlsJa3StringSetup(DetectEngineCtx *de_ctx, Signature *s, const
     if (DetectBufferSetActiveList(s, g_tls_ja3_str_buffer_id) < 0)
         return -1;
 
-    if (DetectSignatureSetAppProto(s, ALPROTO_TLS) < 0)
+    if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_TLS && s->alproto != ALPROTO_QUIC) {
+        SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting protocols.");
         return -1;
+    }
 
     /* try to enable JA3 */
     SSLEnableJA3();
index 7b22fce6c29ebf11f6f2404272674510d36491be..355513aa3e7dbf524f87d962156145af8034f19a 100644 (file)
@@ -68,6 +68,26 @@ static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx,
        void *txv, const int list_id);
 static int g_tls_ja3s_str_buffer_id = 0;
 
+static InspectionBuffer *GetJa3Data(DetectEngineThreadCtx *det_ctx,
+        const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv,
+        const int list_id)
+{
+    InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id);
+    if (buffer->inspect == NULL) {
+        uint32_t b_len = 0;
+        const uint8_t *b = NULL;
+
+        if (rs_quic_tx_get_ja3(txv, &b, &b_len) != 1)
+            return NULL;
+        if (b == NULL || b_len == 0)
+            return NULL;
+
+        InspectionBufferSetup(det_ctx, list_id, buffer, b, b_len);
+        InspectionBufferApplyTransforms(buffer, transforms);
+    }
+    return buffer;
+}
+
 /**
  * \brief Registration function for keyword: ja3s.string
  */
@@ -89,6 +109,12 @@ void DetectTlsJa3SStringRegister(void)
     DetectAppLayerMpmRegister2("ja3s.string", SIG_FLAG_TOCLIENT, 2,
             PrefilterGenericMpmRegister, GetData, ALPROTO_TLS, 0);
 
+    DetectAppLayerMpmRegister2("ja3s.string", SIG_FLAG_TOCLIENT, 2, PrefilterGenericMpmRegister,
+            GetJa3Data, ALPROTO_QUIC, 1);
+
+    DetectAppLayerInspectEngineRegister2("ja3s.string", ALPROTO_QUIC, SIG_FLAG_TOCLIENT, 1,
+            DetectEngineInspectBufferGeneric, GetJa3Data);
+
     DetectBufferTypeSetDescriptionByName("ja3s.string", "TLS JA3S string");
 
     g_tls_ja3s_str_buffer_id = DetectBufferTypeGetByName("ja3s.string");
@@ -109,8 +135,10 @@ static int DetectTlsJa3SStringSetup(DetectEngineCtx *de_ctx, Signature *s, const
     if (DetectBufferSetActiveList(s, g_tls_ja3s_str_buffer_id) < 0)
         return -1;
 
-    if (DetectSignatureSetAppProto(s, ALPROTO_TLS) < 0)
+    if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_TLS && s->alproto != ALPROTO_QUIC) {
+        SCLogError(SC_ERR_CONFLICTING_RULE_KEYWORDS, "rule contains conflicting protocols.");
         return -1;
+    }
 
     /* try to enable JA3 */
     SSLEnableJA3();