From 018fef5ef8f000778d6e3203612dbeec4d42ae9e Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 25 Mar 2022 14:53:09 +0100 Subject: [PATCH] quic: ja3 computation and logging and detection Logging as is done in TLS. Detection using the generic generic ja3.string keyword Ticket: #5143 --- rust/src/quic/detect.rs | 15 +++++++ rust/src/quic/frames.rs | 78 +++++++++++++++++++++++++++++++++--- rust/src/quic/logger.rs | 14 +++++++ rust/src/quic/quic.rs | 16 ++++++-- src/detect-tls-ja3-string.c | 30 +++++++++++++- src/detect-tls-ja3s-string.c | 30 +++++++++++++- 6 files changed, 172 insertions(+), 11 deletions(-) diff --git a/rust/src/quic/detect.rs b/rust/src/quic/detect.rs index cfdd43fc16..7e9019bef0 100644 --- a/rust/src/quic/detect.rs +++ b/rust/src/quic/detect.rs @@ -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, diff --git a/rust/src/quic/frames.rs b/rust/src/quic/frames.rs index d44e3e1951..e1dedba5af 100644 --- a/rust/src/quic/frames.rs +++ b/rust/src/quic/frames.rs @@ -136,6 +136,7 @@ pub(crate) struct Crypto { // We remap the Vec from tls_parser::parse_tls_extensions because of // the lifetime of TlsExtension due to references to the slice used for parsing pub extv: Vec, + pub ja3: String, } #[derive(Debug, PartialEq)] @@ -190,13 +191,59 @@ pub struct QuicTlsExtension { pub values: Vec>, } +fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec) { + 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 { +fn quic_get_tls_extensions( + input: Option<&[u8]>, ja3: &mut String, client: bool, +) -> Vec { 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 { } 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 }))); } _ => {} } diff --git a/rust/src/quic/logger.rs b/rust/src/quic/logger.rs index 8ec70f058e..c6df260fc1 100644 --- a/rust/src/quic/logger.rs +++ b/rust/src/quic/logger.rs @@ -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 { 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 { diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index b51b4e1681..573579209f 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -39,13 +39,15 @@ pub struct QuicTransaction { pub sni: Option>, pub ua: Option>, pub extv: Vec, + pub ja3: Option, + pub client: bool, tx_data: AppLayerTxData, } impl QuicTransaction { fn new( header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, - extv: Vec, + extv: Vec, ja3: Option, 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>, ua: Option>, - extb: Vec, + extb: Vec, ja3: Option, 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> = None; let mut ua: Option> = None; + let mut ja3: Option = None; let mut extv: Vec = 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; } diff --git a/src/detect-tls-ja3-string.c b/src/detect-tls-ja3-string.c index 76c44c8c0d..5c7b5e5919 100644 --- a/src/detect-tls-ja3-string.c +++ b/src/detect-tls-ja3-string.c @@ -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(); diff --git a/src/detect-tls-ja3s-string.c b/src/detect-tls-ja3s-string.c index 7b22fce6c2..355513aa3e 100644 --- a/src/detect-tls-ja3s-string.c +++ b/src/detect-tls-ja3s-string.c @@ -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();