quic: ja3 computation and logging and detection

Logging as is done in TLS.

Detection using the generic generic ja3.string keyword

Ticket: #5143
pull/7678/head
Philippe Antoine 3 years ago committed by Philippe Antoine
parent c6cf61a39b
commit 018fef5ef8

@ -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,

@ -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 })));
}
_ => {}
}

@ -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 {

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

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

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

Loading…
Cancel
Save