diff --git a/configure.ac b/configure.ac index 95613b674182..e6af306fef6d 100644 --- a/configure.ac +++ b/configure.ac @@ -2227,6 +2227,27 @@ fi fi + AC_ARG_ENABLE(ja3, + AS_HELP_STRING([--disable-ja3], [Disable JA3 support]), + [enable_ja3="$enableval"], + [enable_ja3=yes]) + if test "$enable_ja3" = "yes"; then + AC_DEFINE([HAVE_JA3],[1],[JA3 enabled]) + enable_ja3="yes" + fi + AM_CONDITIONAL([HAVE_JA3], [test "x$enable_ja3" != "xno"]) + + AC_ARG_ENABLE(ja4, + AS_HELP_STRING([--disable-ja4], [Disable JA4 support]), + [enable_ja4="$enableval"], + [enable_ja4=yes]) + if test "$enable_ja4" = "yes"; then + AC_DEFINE([HAVE_JA4],[1],[JA4 enabled]) + enable_ja4="yes" + fi + AM_CONDITIONAL([HAVE_JA4], [test "x$enable_ja4" != "xno"]) + + # Check for lz4 enable_liblz4="yes" AC_CHECK_LIB(lz4, LZ4F_createCompressionContext, , enable_liblz4="no") @@ -2669,6 +2690,8 @@ SURICATA_BUILD_CONF="Suricata Configuration: LUA support: ${enable_lua} libluajit: ${enable_luajit} GeoIP2 support: ${enable_geoip} + JA3 support: ${enable_ja3} + JA4 support: ${enable_ja4} Non-bundled htp: ${enable_non_bundled_htp} Hyperscan support: ${enable_hyperscan} Libnet support: ${enable_libnet} diff --git a/doc/userguide/output/eve/eve-json-format.rst b/doc/userguide/output/eve/eve-json-format.rst index 3184426b30cc..a2140149eb04 100644 --- a/doc/userguide/output/eve/eve-json-format.rst +++ b/doc/userguide/output/eve/eve-json-format.rst @@ -1045,8 +1045,9 @@ If extended logging is enabled the following fields are also included: * "notafter": The NotAfter field from the TLS certificate * "ja3": The JA3 fingerprint consisting of both a JA3 hash and a JA3 string * "ja3s": The JA3S fingerprint consisting of both a JA3 hash and a JA3 string +* "ja4": The JA4 client fingerprint for TLS -JA3 must be enabled in the Suricata config file (set 'app-layer.protocols.tls.ja3-fingerprints' to 'yes'). +JA3 and JA4 must be enabled in the Suricata config file (set 'app-layer.protocols.tls.ja3-fingerprints'/'app-layer.protocols.tls.ja4-fingerprints' to 'yes'). In addition to this, custom logging also allows the following fields: @@ -2915,11 +2916,14 @@ Fields * "cyu": List of found CYUs in the packet * "cyu[].hash": CYU hash * "cyu[].string": CYU string +* "ja3": The JA3 fingerprint consisting of both a JA3 hash and a JA3 string +* "ja3s": The JA3S fingerprint consisting of both a JA3 hash and a JA3 string +* "ja4": The JA4 client fingerprint for QUIC Examples ~~~~~~~~ -Example of QUIC logging with a CYU hash: +Example of QUIC logging with CYU, JA3 and JA4 hashes (note that the JA4 hash is only an example to illustrate the format and does not correlate with the others): :: @@ -2931,7 +2935,12 @@ Example of QUIC logging with a CYU hash: "hash": "7b3ceb1adc974ad360cfa634e8d0a730", "string": "46,PAD-SNI-STK-SNO-VER-CCS-NONC-AEAD-UAID-SCID-TCID-PDMD-SMHL-ICSL-NONP-PUBS-MIDS-SCLS-KEXS-XLCT-CSCT-COPT-CCRT-IRTT-CFCW-SFCW" } - ] + ], + "ja3": { + "hash": "324f8c50e267adba4b5dd06c964faf67", + "string": "771,4865-4866-4867,51-43-13-27-17513-16-45-0-10-57,29-23-24," + }, + "ja4": "q13d0310h3_55b375c5d22e_cd85d2d88918" } Event type: DHCP diff --git a/doc/userguide/output/eve/eve-json-output.rst b/doc/userguide/output/eve/eve-json-output.rst index 2730f543bbf5..364a80418d39 100644 --- a/doc/userguide/output/eve/eve-json-output.rst +++ b/doc/userguide/output/eve/eve-json-output.rst @@ -259,7 +259,7 @@ YAML:: extended: yes # enable this for extended logging information # custom allows to control which tls fields that are included # in eve-log - #custom: [subject, issuer, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s] + #custom: [subject, issuer, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s, ja4] The default is to log certificate subject and issuer. If ``extended`` is enabled, then the log gets more verbose. diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index 2450f4486be9..444cc3fe07d4 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -18,6 +18,7 @@ Suricata Rules tls-keywords ssh-keywords ja3-keywords + ja4-keywords modbus-keyword dcerpc-keywords dhcp-keywords diff --git a/doc/userguide/rules/ja4-keywords.rst b/doc/userguide/rules/ja4-keywords.rst new file mode 100644 index 000000000000..c0fc0fca35b7 --- /dev/null +++ b/doc/userguide/rules/ja4-keywords.rst @@ -0,0 +1,30 @@ +JA4 Keywords +============ + +Suricata comes with a JA4 integration (https://github.com/FoxIO-LLC/ja4). JA4, +as part of the larger JA4+ suite of fingerprints, is used to fingerprint TLS +clients. + +We might in the future consider adding raw and original order strings (JA4_r, +JA4_o, JA4_ro) as non-hashed versions. + +JA4 support must be enabled in the Suricata config file (set +``app-layer.protocols.tls.ja4-fingerprints`` to ``yes``). If it is not +explicitly disabled (``no``) , it will enabled if a loaded rule requires it. +It also needs to be enabled at compile time (``--enable-ja4``). + +ja4.hash +-------- + +Match on JA4 hash (e.g. ``q13d0310h3_55b375c5d22e_cd85d2d88918``). + +Example:: + + alert quic any any -> any any (msg:"match JA4 hash"; \ + ja4.hash; content:"q13d0310h3_55b375c5d22e_cd85d2d88918"; \ + sid:100001;) + +``ja4.hash`` is a 'sticky buffer'. + +``ja4.hash`` can be used as ``fast_pattern``. + diff --git a/etc/schema.json b/etc/schema.json index b06dbd4b29fe..c7287dc28c73 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -3012,6 +3012,9 @@ }, "additionalProperties": false }, + "ja4": { + "type": "string" + }, "sni": { "description": "Server Name Indication", "type": "string" @@ -5405,6 +5408,9 @@ } }, "additionalProperties": false + }, + "ja4": { + "type": "string" } }, "additionalProperties": false diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index 1e135510e9e3..7f1ba6b07975 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -20,6 +20,7 @@ lua_int8 = ["lua"] strict = [] debug = [] debug-validate = [] +ja4 = [] [dependencies] nom7 = { version="7.0", package="nom" } diff --git a/rust/Makefile.am b/rust/Makefile.am index 2857288fefa3..53cb69621a66 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -16,6 +16,10 @@ if HAVE_LUA RUST_FEATURES += lua $(LUA_INT8) endif +if HAVE_JA4 +RUST_FEATURES += ja4 +endif + if DEBUG RUST_FEATURES += debug endif diff --git a/rust/src/ja4.rs b/rust/src/ja4.rs new file mode 100644 index 000000000000..a009e5edd70b --- /dev/null +++ b/rust/src/ja4.rs @@ -0,0 +1,390 @@ +/* Copyright (C) 2023-2024 Open Information Security Foundation +* +* You can copy, redistribute or modify this Program under the terms of +* the GNU General Public License version 2 as published by the Free +* Software Foundation. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* version 2 along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +* 02110-1301, USA. + +// Author: Sascha Steinbiss + +*/ + +#[cfg(feature = "ja4")] +use digest::Digest; +use libc::c_uchar; +#[cfg(feature = "ja4")] +use sha2::Sha256; +use std::os::raw::c_char; +use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion}; + +#[derive(Debug, PartialEq)] +pub struct JA4 { + tls_version: Option, + ciphersuites: Vec, + extensions: Vec, + signature_algorithms: Vec, + domain: bool, + alpn: [char; 2], + quic: bool, + nof_exts: u16, + nof_ciphers: u16, +} + +impl Default for JA4 { + fn default() -> Self { + Self::new() + } +} + +// Stubs for when JA4 is disabled +#[cfg(not(feature = "ja4"))] +impl JA4 { + pub fn new() -> Self { + Self { + tls_version: None, + ciphersuites: Vec::with_capacity(20), + extensions: Vec::with_capacity(20), + signature_algorithms: Vec::with_capacity(20), + domain: false, + alpn: ['0', '0'], + quic: false, + nof_exts: 0, + nof_ciphers: 0, + } + } + pub fn set_quic(&mut self) {} + pub fn set_tls_version(&mut self, _version: TlsVersion) {} + pub fn set_alpn(&mut self, _alpn: &[u8]) {} + pub fn add_cipher_suite(&mut self, _cipher: TlsCipherSuiteID) {} + pub fn add_extension(&mut self, _ext: TlsExtensionType) {} + pub fn add_signature_algorithm(&mut self, _sigalgo: u16) {} + pub fn get_hash(&self) -> String { + String::new() + } +} + +#[cfg(feature = "ja4")] +impl JA4 { + #[inline] + fn is_grease(val: u16) -> bool { + match val { + 0x0a0a | 0x1a1a | 0x2a2a | 0x3a3a | 0x4a4a | 0x5a5a | 0x6a6a | 0x7a7a | 0x8a8a + | 0x9a9a | 0xaaaa | 0xbaba | 0xcaca | 0xdada | 0xeaea | 0xfafa => true, + _ => false, + } + } + + #[inline] + fn version_to_ja4code(val: Option) -> &'static str { + match val { + Some(TlsVersion::Tls13) => "13", + Some(TlsVersion::Tls12) => "12", + Some(TlsVersion::Tls11) => "11", + Some(TlsVersion::Tls10) => "10", + Some(TlsVersion::Ssl30) => "s3", + // the TLS parser does not support SSL 1.0 and 2.0 hence no + // support for "s1"/"s2" + _ => "00", + } + } + + pub fn new() -> Self { + Self { + tls_version: None, + ciphersuites: Vec::with_capacity(20), + extensions: Vec::with_capacity(20), + signature_algorithms: Vec::with_capacity(20), + domain: false, + alpn: ['0', '0'], + quic: false, + nof_exts: 0, + nof_ciphers: 0, + } + } + + pub fn set_quic(&mut self) { + self.quic = true; + } + + pub fn set_tls_version(&mut self, version: TlsVersion) { + if JA4::is_grease(u16::from(version)) { + return; + } + // Track maximum of seen TLS versions + match self.tls_version { + None => { + self.tls_version = Some(version); + } + Some(cur_version) => { + if u16::from(version) > u16::from(cur_version) { + self.tls_version = Some(version); + } + } + } + } + + pub fn set_alpn(&mut self, alpn: &[u8]) { + if alpn.len() > 1 { + if alpn.len() == 2 { + // GREASE values are 2 bytes, so this could be one -- check + let v: u16 = (alpn[0] as u16) << 8 | alpn[alpn.len() - 1] as u16; + if JA4::is_grease(v) { + return; + } + } + self.alpn[0] = char::from(alpn[0]); + self.alpn[1] = char::from(alpn[alpn.len() - 1]); + } + } + + pub fn add_cipher_suite(&mut self, cipher: TlsCipherSuiteID) { + if JA4::is_grease(u16::from(cipher)) { + return; + } + self.ciphersuites.push(cipher); + self.nof_ciphers += 1; + } + + pub fn add_extension(&mut self, ext: TlsExtensionType) { + if JA4::is_grease(u16::from(ext)) { + return; + } + if ext != TlsExtensionType::ApplicationLayerProtocolNegotiation + && ext != TlsExtensionType::ServerName + { + self.extensions.push(ext); + } else if ext == TlsExtensionType::ServerName { + self.domain = true; + } + self.nof_exts += 1; + } + + pub fn add_signature_algorithm(&mut self, sigalgo: u16) { + if JA4::is_grease(sigalgo) { + return; + } + self.signature_algorithms.push(sigalgo); + } + + pub fn get_hash(&self) -> String { + // Calculate JA4_a + let ja4_a = format!( + "{proto}{version}{sni}{nof_c:02}{nof_e:02}{al1}{al2}", + proto = if self.quic { "q" } else { "t" }, + version = JA4::version_to_ja4code(self.tls_version), + sni = if self.domain { "d" } else { "i" }, + nof_c = if self.nof_ciphers > 99 { + 99 + } else { + self.nof_ciphers + }, + nof_e = if self.nof_exts > 99 { + 99 + } else { + self.nof_exts + }, + al1 = self.alpn[0], + al2 = self.alpn[1] + ); + + // Calculate JA4_b + let mut sorted_ciphers = self.ciphersuites.to_vec(); + sorted_ciphers.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b))); + let sorted_cipherstrings: Vec = sorted_ciphers + .iter() + .map(|v| format!("{:04x}", u16::from(*v))) + .collect(); + let mut sha = Sha256::new(); + let ja4_b_raw = sorted_cipherstrings.join(","); + sha.update(&ja4_b_raw); + let mut ja4_b = format!("{:x}", sha.finalize_reset()); + ja4_b.truncate(12); + + // Calculate JA4_c + let mut sorted_exts = self.extensions.to_vec(); + sorted_exts.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b))); + let sorted_extstrings: Vec = sorted_exts + .iter() + .map(|v| format!("{:04x}", u16::from(*v))) + .collect(); + let ja4_c1_raw = sorted_extstrings.join(","); + let unsorted_sigalgostrings: Vec = self + .signature_algorithms + .iter() + .map(|v| format!("{:04x}", (*v))) + .collect(); + let ja4_c2_raw = unsorted_sigalgostrings.join(","); + let ja4_c_raw = format!("{}_{}", ja4_c1_raw, ja4_c2_raw); + sha.update(&ja4_c_raw); + let mut ja4_c = format!("{:x}", sha.finalize()); + ja4_c.truncate(12); + + return format!("{}_{}_{}", ja4_a, ja4_b, ja4_c); + } +} + +pub struct SCJA4(JA4); + +#[no_mangle] +pub extern "C" fn SCJA4New() -> *mut SCJA4 { + let j = Box::new(SCJA4(JA4::new())); + Box::into_raw(j) +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4SetTLSVersion(j: &mut SCJA4, version: u16) { + j.0.set_tls_version(TlsVersion(version)); +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4AddCipher(j: &mut SCJA4, cipher: u16) { + j.0.add_cipher_suite(TlsCipherSuiteID(cipher)); +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4AddExtension(j: &mut SCJA4, ext: u16) { + j.0.add_extension(TlsExtensionType(ext)); +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4AddSigAlgo(j: &mut SCJA4, sigalgo: u16) { + j.0.add_signature_algorithm(sigalgo); +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4SetALPN(j: &mut SCJA4, proto: *const c_char, len: u16) { + let b: &[u8] = std::slice::from_raw_parts(proto as *const c_uchar, len as usize); + j.0.set_alpn(b); +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4GetHash(j: &mut SCJA4, out: &mut [u8; 36]) { + let hash = j.0.get_hash(); + out[0..36].copy_from_slice(hash.as_bytes()); +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4Free(j: &mut SCJA4) { + let ja4: Box = Box::from_raw(j); + std::mem::drop(ja4); +} + +#[cfg(all(test, feature = "ja4"))] +mod tests { + use super::*; + + #[test] + fn test_is_grease() { + let mut alpn = "foobar".as_bytes(); + let mut len = alpn.len(); + let v: u16 = (alpn[0] as u16) << 8 | alpn[len - 1] as u16; + assert!(!JA4::is_grease(v)); + + alpn = &[0x0a, 0x0a]; + len = alpn.len(); + let v: u16 = (alpn[0] as u16) << 8 | alpn[len - 1] as u16; + assert!(JA4::is_grease(v)); + } + + #[test] + fn test_tlsversion_max() { + let mut j = JA4::new(); + assert_eq!(j.tls_version, None); + j.set_tls_version(TlsVersion::Ssl30); + assert_eq!(j.tls_version, Some(TlsVersion::Ssl30)); + j.set_tls_version(TlsVersion::Tls12); + assert_eq!(j.tls_version, Some(TlsVersion::Tls12)); + j.set_tls_version(TlsVersion::Tls10); + assert_eq!(j.tls_version, Some(TlsVersion::Tls12)); + } + + #[test] + fn test_get_hash_limit_numbers() { + let mut j = JA4::new(); + + for i in 1..200 { + j.add_cipher_suite(TlsCipherSuiteID(i)); + } + for i in 1..200 { + j.add_extension(TlsExtensionType(i)); + } + + let mut s = j.get_hash(); + s.truncate(10); + assert_eq!(s, "t00i999900"); + } + + #[test] + fn test_short_alpn() { + let mut j = JA4::new(); + + j.set_alpn("a".as_bytes()); + let mut s = j.get_hash(); + s.truncate(10); + assert_eq!(s, "t00i000000"); + + j.set_alpn("aa".as_bytes()); + let mut s = j.get_hash(); + s.truncate(10); + assert_eq!(s, "t00i0000aa"); + } + + #[test] + fn test_get_hash() { + let mut j = JA4::new(); + + // the empty JA4 hash + let s = j.get_hash(); + assert_eq!(s, "t00i000000_e3b0c44298fc_d2e2adf7177b"); + + // set TLS version + j.set_tls_version(TlsVersion::Tls12); + let s = j.get_hash(); + assert_eq!(s, "t12i000000_e3b0c44298fc_d2e2adf7177b"); + + // set QUIC + j.set_quic(); + let s = j.get_hash(); + assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b"); + + // set GREASE extension, should be ignored + j.add_extension(TlsExtensionType(0x0a0a)); + let s = j.get_hash(); + assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b"); + + // set SNI extension, should only increase count and change i->d + j.add_extension(TlsExtensionType(0x0000)); + let s = j.get_hash(); + assert_eq!(s, "q12d000100_e3b0c44298fc_d2e2adf7177b"); + + // set ALPN extension, should only increase count and set end of JA4_a + j.set_alpn(b"h3-16"); + j.add_extension(TlsExtensionType::ApplicationLayerProtocolNegotiation); + let s = j.get_hash(); + assert_eq!(s, "q12d0002h6_e3b0c44298fc_d2e2adf7177b"); + + // set some ciphers + j.add_cipher_suite(TlsCipherSuiteID(0x1111)); + j.add_cipher_suite(TlsCipherSuiteID(0x0a20)); + j.add_cipher_suite(TlsCipherSuiteID(0xbada)); + let s = j.get_hash(); + assert_eq!(s, "q12d0302h6_f500716053f9_d2e2adf7177b"); + + // set some extensions and signature algorithms + j.add_extension(TlsExtensionType(0xface)); + j.add_extension(TlsExtensionType(0x0121)); + j.add_extension(TlsExtensionType(0x1234)); + j.add_signature_algorithm(0x6666); + let s = j.get_hash(); + assert_eq!(s, "q12d0305h6_f500716053f9_2debc8880bae"); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 15e21c4057d1..9e58d4d805e8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -84,6 +84,8 @@ pub mod filetracker; pub mod kerberos; pub mod detect; +pub mod ja4; + #[cfg(feature = "lua")] pub mod lua; diff --git a/rust/src/quic/detect.rs b/rust/src/quic/detect.rs index 7e9019bef004..cd88120646ab 100644 --- a/rust/src/quic/detect.rs +++ b/rust/src/quic/detect.rs @@ -63,6 +63,21 @@ pub unsafe extern "C" fn rs_quic_tx_get_ja3( } } +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_ja4( + tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if let Some(ja4) = &tx.ja4 { + *buffer = ja4.as_ptr(); + *buffer_len = ja4.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 e1fb7d080727..fd890ac16404 100644 --- a/rust/src/quic/frames.rs +++ b/rust/src/quic/frames.rs @@ -16,6 +16,7 @@ */ use super::error::QuicError; +use crate::ja4::*; use crate::quic::parser::quic_var_uint; use nom7::bytes::complete::take; use nom7::combinator::{all_consuming, complete}; @@ -137,6 +138,7 @@ pub(crate) struct Crypto { // the lifetime of TlsExtension due to references to the slice used for parsing pub extv: Vec, pub ja3: String, + pub ja4: Option, } #[derive(Debug, PartialEq)] @@ -235,7 +237,7 @@ fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec) { // get interesting stuff out of parsed tls extensions fn quic_get_tls_extensions( - input: Option<&[u8]>, ja3: &mut String, client: bool, + input: Option<&[u8]>, ja3: &mut String, mut ja4: Option<&mut JA4>, client: bool, ) -> Vec { let mut extv = Vec::new(); if let Some(extr) = input { @@ -249,8 +251,21 @@ fn quic_get_tls_extensions( dash = true; } ja3.push_str(&u16::from(etype).to_string()); + if let Some(ref mut ja4) = ja4 { + ja4.add_extension(etype) + } let mut values = Vec::new(); match e { + TlsExtension::SupportedVersions(x) => { + for version in x { + let mut value = Vec::new(); + value.extend_from_slice(version.to_string().as_bytes()); + values.push(value); + if let Some(ref mut ja4) = ja4 { + ja4.set_tls_version(*version); + } + } + } TlsExtension::SNI(x) => { for sni in x { let mut value = Vec::new(); @@ -258,7 +273,22 @@ fn quic_get_tls_extensions( values.push(value); } } + TlsExtension::SignatureAlgorithms(x) => { + for sigalgo in x { + let mut value = Vec::new(); + value.extend_from_slice(sigalgo.to_string().as_bytes()); + values.push(value); + if let Some(ref mut ja4) = ja4 { + ja4.add_signature_algorithm(*sigalgo) + } + } + } TlsExtension::ALPN(x) => { + if !x.is_empty() { + if let Some(ref mut ja4) = ja4 { + ja4.set_alpn(x[0]); + } + } for alpn in x { let mut value = Vec::new(); value.extend_from_slice(alpn); @@ -284,6 +314,8 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option { let mut ja3 = String::with_capacity(256); ja3.push_str(&u16::from(ch.version).to_string()); ja3.push(','); + let mut ja4 = JA4::new(); + ja4.set_quic(); let mut dash = false; for c in &ch.ciphers { if dash { @@ -292,11 +324,17 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option { dash = true; } ja3.push_str(&u16::from(*c).to_string()); + ja4.add_cipher_suite(*c); } ja3.push(','); let ciphers = ch.ciphers; - let extv = quic_get_tls_extensions(ch.ext, &mut ja3, true); - return Some(Frame::Crypto(Crypto { ciphers, extv, ja3 })); + let extv = quic_get_tls_extensions(ch.ext, &mut ja3, Some(&mut ja4), true); + return Some(Frame::Crypto(Crypto { + ciphers, + extv, + ja3, + ja4: Some(ja4), + })); } ServerHello(sh) => { let mut ja3 = String::with_capacity(256); @@ -305,8 +343,13 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option { ja3.push_str(&u16::from(sh.cipher).to_string()); ja3.push(','); let ciphers = vec![sh.cipher]; - let extv = quic_get_tls_extensions(sh.ext, &mut ja3, false); - return Some(Frame::Crypto(Crypto { ciphers, extv, ja3 })); + let extv = quic_get_tls_extensions(sh.ext, &mut ja3, None, false); + return Some(Frame::Crypto(Crypto { + ciphers, + extv, + ja3, + ja4: None, + })); } _ => {} } @@ -504,8 +547,7 @@ impl Frame { let mut d = vec![0; crypto_max_size as usize]; 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); + 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) { diff --git a/rust/src/quic/logger.rs b/rust/src/quic/logger.rs index 8cb08830e4d0..2bb92f3be01d 100644 --- a/rust/src/quic/logger.rs +++ b/rust/src/quic/logger.rs @@ -122,6 +122,11 @@ fn log_quic(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> js.set_string("string", ja3)?; js.close()?; } + + if let Some(ref ja4) = &tx.ja4 { + js.set_string("ja4", ja4)?; + } + if !tx.extv.is_empty() { js.open_array("extensions")?; for e in &tx.extv { diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 8e3ea6f35a21..9d1460857984 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -22,7 +22,7 @@ use super::{ parser::{quic_pkt_num, QuicData, QuicHeader, QuicType}, }; use crate::applayer::{self, *}; -use crate::core::{AppProto, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP, Direction}; +use crate::core::{AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP}; use std::collections::VecDeque; use std::ffi::CString; use tls_parser::TlsExtensionType; @@ -48,6 +48,7 @@ pub struct QuicTransaction { pub ua: Option>, pub extv: Vec, pub ja3: Option, + pub ja4: Option, pub client: bool, tx_data: AppLayerTxData, } @@ -55,9 +56,13 @@ pub struct QuicTransaction { impl QuicTransaction { fn new( header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, - extv: Vec, ja3: Option, client: bool, + extv: Vec, ja3: Option, ja4: Option, client: bool, ) -> Self { - let direction = if client { Direction::ToServer } else { Direction::ToClient }; + let direction = if client { + Direction::ToServer + } else { + Direction::ToClient + }; let cyu = Cyu::generate(&header, &data.frames); QuicTransaction { tx_id: 0, @@ -67,13 +72,18 @@ impl QuicTransaction { ua, extv, ja3, + ja4, client, tx_data: AppLayerTxData::for_direction(direction), } } fn new_empty(client: bool, header: QuicHeader) -> Self { - let direction = if client { Direction::ToServer } else { Direction::ToClient }; + let direction = if client { + Direction::ToServer + } else { + Direction::ToClient + }; QuicTransaction { tx_id: 0, header, @@ -82,6 +92,7 @@ impl QuicTransaction { ua: None, extv: Vec::new(), ja3: None, + ja4: None, client, tx_data: AppLayerTxData::for_direction(direction), } @@ -132,9 +143,9 @@ impl QuicState { fn new_tx( &mut self, header: QuicHeader, data: QuicData, sni: Option>, ua: Option>, - extb: Vec, ja3: Option, client: bool, + extb: Vec, ja3: Option, ja4: Option, client: bool, ) { - let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, client); + 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; self.transactions.push_back(tx); @@ -212,6 +223,7 @@ impl QuicState { let mut sni: Option> = None; let mut ua: Option> = None; let mut ja3: Option = None; + let mut ja4: Option = None; let mut extv: Vec = Vec::new(); for frame in &data.frames { match frame { @@ -231,6 +243,14 @@ impl QuicState { } Frame::Crypto(c) => { ja3 = Some(c.ja3.clone()); + // we only do client fingerprints for now + if to_server { + // our hash is complete, let's only use strings from + // now on + if let Some(ref rja4) = c.ja4 { + ja4 = Some(rja4.get_hash()); + } + } for e in &c.extv { if e.etype == TlsExtensionType::ServerName && !e.values.is_empty() { sni = Some(e.values[0].to_vec()); @@ -246,7 +266,7 @@ impl QuicState { _ => {} } } - self.new_tx(header, data, sni, ua, extv, ja3, to_server); + self.new_tx(header, data, sni, ua, extv, ja3, ja4, to_server); } fn set_event_notx(&mut self, event: QuicEvent, header: QuicHeader, client: bool) { @@ -303,6 +323,7 @@ impl QuicState { None, Vec::new(), None, + None, to_server, ); continue; diff --git a/src/Makefile.am b/src/Makefile.am index b8c28dcf6372..f2056a622213 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -231,6 +231,7 @@ noinst_HEADERS = \ detect-ipv6hdr.h \ detect-isdataat.h \ detect-itype.h \ + detect-ja4-hash.h \ detect-krb5-cname.h \ detect-krb5-errcode.h \ detect-krb5-msgtype.h \ @@ -849,6 +850,7 @@ libsuricata_c_a_SOURCES = \ detect-ipv6hdr.c \ detect-isdataat.c \ detect-itype.c \ + detect-ja4-hash.c \ detect-krb5-cname.c \ detect-krb5-errcode.c \ detect-krb5-msgtype.c \ diff --git a/src/app-layer-ssl.c b/src/app-layer-ssl.c index cb094f3801ab..63e244ce81c4 100644 --- a/src/app-layer-ssl.c +++ b/src/app-layer-ssl.c @@ -43,6 +43,8 @@ #include "decode-events.h" #include "conf.h" +#include "feature.h" + #include "util-spm.h" #include "util-unittest.h" #include "util-debug.h" @@ -143,8 +145,9 @@ enum { ERR_EXTRACT_VALIDITY, }; -/* JA3 fingerprints are disabled by default */ +/* JA3 and JA4 fingerprints are disabled by default */ #define SSL_CONFIG_DEFAULT_JA3 0 +#define SSL_CONFIG_DEFAULT_JA4 0 enum SslConfigEncryptHandling { SSL_CNF_ENC_HANDLE_DEFAULT = 0, /**< disable raw content, continue tracking */ @@ -154,10 +157,12 @@ enum SslConfigEncryptHandling { typedef struct SslConfig_ { enum SslConfigEncryptHandling encrypt_mode; - /** dynamic setting for ja3: can be enabled on demand if not explicitly - * disabled. */ + /** dynamic setting for ja3 and ja4: can be enabled on demand if not + * explicitly disabled. */ SC_ATOMIC_DECLARE(int, enable_ja3); bool disable_ja3; /**< ja3 explicitly disabled. Don't enable on demand. */ + SC_ATOMIC_DECLARE(int, enable_ja4); + bool disable_ja4; /**< ja4 explicitly disabled. Don't enable on demand. */ } SslConfig; SslConfig ssl_config; @@ -691,6 +696,11 @@ static inline int TLSDecodeHSHelloVersion(SSLState *ssl_state, uint16_t version = (uint16_t)(*input << 8) | *(input + 1); ssl_state->curr_connp->version = version; + if (ssl_state->curr_connp->ja4 != NULL && + ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + SCJA4SetTLSVersion(ssl_state->curr_connp->ja4, version); + } + /* TLSv1.3 draft1 to draft21 use the version field as earlier TLS versions, instead of using the supported versions extension. */ if ((ssl_state->current_flags & SSL_AL_FLAG_STATE_SERVER_HELLO) && @@ -834,17 +844,23 @@ static inline int TLSDecodeHSHelloCipherSuites(SSLState *ssl_state, goto invalid_length; } - if (SC_ATOMIC_GET(ssl_config.enable_ja3)) { - JA3Buffer *ja3_cipher_suites = Ja3BufferInit(); - if (ja3_cipher_suites == NULL) - return -1; + if (SC_ATOMIC_GET(ssl_config.enable_ja3) || SC_ATOMIC_GET(ssl_config.enable_ja4)) { + JA3Buffer *ja3_cipher_suites = NULL; + + if (SC_ATOMIC_GET(ssl_config.enable_ja3)) { + ja3_cipher_suites = Ja3BufferInit(); + if (ja3_cipher_suites == NULL) + return -1; + } uint16_t processed_len = 0; /* coverity[tainted_data] */ while (processed_len < cipher_suites_length) { if (!(HAS_SPACE(2))) { - Ja3BufferFree(&ja3_cipher_suites); + if (SC_ATOMIC_GET(ssl_config.enable_ja3)) { + Ja3BufferFree(&ja3_cipher_suites); + } goto invalid_length; } @@ -852,19 +868,25 @@ static inline int TLSDecodeHSHelloCipherSuites(SSLState *ssl_state, input += 2; if (TLSDecodeValueIsGREASE(cipher_suite) != 1) { - int rc = Ja3BufferAddValue(&ja3_cipher_suites, cipher_suite); - if (rc != 0) { - return -1; + if (ssl_state->curr_connp->ja4 != NULL && + ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + SCJA4AddCipher(ssl_state->curr_connp->ja4, cipher_suite); + } + if (SC_ATOMIC_GET(ssl_config.enable_ja3)) { + int rc = Ja3BufferAddValue(&ja3_cipher_suites, cipher_suite); + if (rc != 0) { + return -1; + } } } - processed_len += 2; } - int rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, - &ja3_cipher_suites); - if (rc == -1) { - return -1; + if (SC_ATOMIC_GET(ssl_config.enable_ja3)) { + int rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, &ja3_cipher_suites); + if (rc == -1) { + return -1; + } } } else { @@ -1025,6 +1047,10 @@ static inline int TLSDecodeHSHelloExtensionSupportedVersions(SSLState *ssl_state uint16_t ver = (uint16_t)(input[i] << 8) | input[i + 1]; if (TLSVersionValid(ver)) { ssl_state->curr_connp->version = ver; + if (ssl_state->curr_connp->ja4 != NULL && + ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + SCJA4SetTLSVersion(ssl_state->curr_connp->ja4, ver); + } break; } i += 2; @@ -1171,6 +1197,106 @@ static inline int TLSDecodeHSHelloExtensionEllipticCurvePF(SSLState *ssl_state, return -1; } +static inline int TLSDecodeHSHelloExtensionSigAlgorithms( + SSLState *ssl_state, const uint8_t *const initial_input, const uint32_t input_len) +{ + const uint8_t *input = initial_input; + + /* Empty extension */ + if (input_len == 0) + return 0; + + if (!(HAS_SPACE(2))) + goto invalid_length; + + uint16_t sigalgo_len = (uint16_t)(*input << 8) | *(input + 1); + input += 2; + + /* Signature algorithms length should always be divisible by 2 */ + if ((sigalgo_len % 2) != 0) { + goto invalid_length; + } + + if (!(HAS_SPACE(sigalgo_len))) + goto invalid_length; + + if (ssl_state->curr_connp->ja4 != NULL && + ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + uint16_t sigalgo_processed_len = 0; + while (sigalgo_processed_len < sigalgo_len) { + uint16_t sigalgo = (uint16_t)(*input << 8) | *(input + 1); + input += 2; + sigalgo_processed_len += 2; + + SCJA4AddSigAlgo(ssl_state->curr_connp->ja4, sigalgo); + } + } else { + /* Skip signature algorithms */ + input += sigalgo_len; + } + + return (input - initial_input); + +invalid_length: + SCLogDebug("Signature algorithm list invalid length"); + SSLSetEvent(ssl_state, TLS_DECODER_EVENT_HANDSHAKE_INVALID_LENGTH); + + return -1; +} + +static inline int TLSDecodeHSHelloExtensionALPN( + SSLState *ssl_state, const uint8_t *const initial_input, const uint32_t input_len) +{ + const uint8_t *input = initial_input; + + /* Empty extension */ + if (input_len == 0) + return 0; + + if (!(HAS_SPACE(2))) + goto invalid_length; + + uint16_t alpn_len = (uint16_t)(*input << 8) | *(input + 1); + input += 2; + + if (!(HAS_SPACE(alpn_len))) + goto invalid_length; + + if (ssl_state->curr_connp->ja4 != NULL && + ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + /* We use 32 bits here to avoid potentially overflowing a value that + needs to be compared to an unsigned 16-bit value. */ + uint32_t alpn_processed_len = 0; + while (alpn_processed_len < alpn_len) { + uint8_t protolen = *input; + input += 1; + alpn_processed_len += 1; + + if (!(HAS_SPACE(protolen))) + goto invalid_length; + + /* we only want the first value for JA4 */ + if (alpn_processed_len == 1) { + SCJA4SetALPN(ssl_state->curr_connp->ja4, (const char *)input, protolen); + } + + alpn_processed_len += protolen; + input += protolen; + } + } else { + /* Skip ALPN protocols */ + input += alpn_len; + } + + return (input - initial_input); + +invalid_length: + SCLogDebug("ALPN list invalid length"); + SSLSetEvent(ssl_state, TLS_DECODER_EVENT_HANDSHAKE_INVALID_LENGTH); + + return -1; +} + static inline int TLSDecodeHSHelloExtensions(SSLState *ssl_state, const uint8_t * const initial_input, const uint32_t input_len) @@ -1272,6 +1398,28 @@ static inline int TLSDecodeHSHelloExtensions(SSLState *ssl_state, break; } + case SSL_EXTENSION_SIGNATURE_ALGORITHMS: { + /* coverity[tainted_data] */ + ret = TLSDecodeHSHelloExtensionSigAlgorithms(ssl_state, input, ext_len); + if (ret < 0) + goto end; + + input += ret; + + break; + } + + case SSL_EXTENSION_ALPN: { + /* coverity[tainted_data] */ + ret = TLSDecodeHSHelloExtensionALPN(ssl_state, input, ext_len); + if (ret < 0) + goto end; + + input += ret; + + break; + } + case SSL_EXTENSION_EARLY_DATA: { if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { @@ -1325,6 +1473,13 @@ static inline int TLSDecodeHSHelloExtensions(SSLState *ssl_state, } } + if (ssl_state->curr_connp->ja4 != NULL && + ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + if (TLSDecodeValueIsGREASE(ext_type) != 1) { + SCJA4AddExtension(ssl_state->curr_connp->ja4, ext_type); + } + } + processed_len += ext_len + 4; } @@ -1373,6 +1528,15 @@ static int TLSDecodeHandshakeHello(SSLState *ssl_state, int ret; uint32_t parsed = 0; + /* Ensure that we have a JA4 state defined by now if we have JA4 enabled, + we are in a client hello and we don't have such a state yet (to avoid + leaking memory in case this function is entered more than once). */ + if (SC_ATOMIC_GET(ssl_config.enable_ja4) && + ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO && + ssl_state->curr_connp->ja4 == NULL) { + ssl_state->curr_connp->ja4 = SCJA4New(); + } + ret = TLSDecodeHSHelloVersion(ssl_state, input, input_len); if (ret < 0) goto end; @@ -2696,6 +2860,8 @@ static void SSLStateFree(void *p) if (ssl_state->server_connp.session_id) SCFree(ssl_state->server_connp.session_id); + if (ssl_state->client_connp.ja4) + SCJA4Free(ssl_state->client_connp.ja4); if (ssl_state->client_connp.ja3_str) Ja3BufferFree(&ssl_state->client_connp.ja3_str); if (ssl_state->client_connp.ja3_hash) @@ -3048,9 +3214,10 @@ void RegisterSSLParsers(void) } SCLogDebug("ssl_config.encrypt_mode %u", ssl_config.encrypt_mode); +#ifdef HAVE_JA3 + const char *strval = NULL; /* Check if we should generate JA3 fingerprints */ int enable_ja3 = SSL_CONFIG_DEFAULT_JA3; - const char *strval = NULL; if (ConfGet("app-layer.protocols.tls.ja3-fingerprints", &strval) != 1) { enable_ja3 = SSL_CONFIG_DEFAULT_JA3; } else if (strcmp(strval, "auto") == 0) { @@ -3062,15 +3229,53 @@ void RegisterSSLParsers(void) enable_ja3 = true; } SC_ATOMIC_SET(ssl_config.enable_ja3, enable_ja3); + if (!ssl_config.disable_ja3 && !g_disable_hashing) { + /* The feature is available, i.e. _could_ be activated by a rule or + even is enabled in the configuration. */ + ProvidesFeature(FEATURE_JA3); + } +#endif /* HAVE_JA3 */ +#ifdef HAVE_JA4 +#ifndef HAVE_JA3 + const char *strval = NULL; +#endif /* HAVE_JA3 */ + /* Check if we should generate JA4 fingerprints */ + int enable_ja4 = SSL_CONFIG_DEFAULT_JA4; + if (ConfGet("app-layer.protocols.tls.ja4-fingerprints", &strval) != 1) { + enable_ja4 = SSL_CONFIG_DEFAULT_JA4; + } else if (strcmp(strval, "auto") == 0) { + enable_ja4 = SSL_CONFIG_DEFAULT_JA4; + } else if (ConfValIsFalse(strval)) { + enable_ja4 = 0; + ssl_config.disable_ja4 = true; + } else if (ConfValIsTrue(strval)) { + enable_ja4 = true; + } + SC_ATOMIC_SET(ssl_config.enable_ja4, enable_ja4); + if (!ssl_config.disable_ja4 && !g_disable_hashing) { + /* The feature is available, i.e. _could_ be activated by a rule or + even is enabled in the configuration. */ + ProvidesFeature(FEATURE_JA4); + } +#endif /* HAVE_JA4 */ if (g_disable_hashing) { if (SC_ATOMIC_GET(ssl_config.enable_ja3)) { SCLogWarning("MD5 calculation has been disabled, disabling JA3"); SC_ATOMIC_SET(ssl_config.enable_ja3, 0); } + if (SC_ATOMIC_GET(ssl_config.enable_ja4)) { + SCLogWarning("Hashing has been disabled, disabling JA4"); + SC_ATOMIC_SET(ssl_config.enable_ja4, 0); + } } else { if (RunmodeIsUnittests()) { +#ifdef HAVE_JA3 SC_ATOMIC_SET(ssl_config.enable_ja3, 1); +#endif /* HAVE_JA4 */ +#ifdef HAVE_JA4 + SC_ATOMIC_SET(ssl_config.enable_ja4, 1); +#endif /* HAVE_JA4 */ } } } else { @@ -3098,10 +3303,45 @@ void SSLEnableJA3(void) SC_ATOMIC_SET(ssl_config.enable_ja3, 1); } -bool SSLJA3IsEnabled(void) +/** + * \brief if not explicitly disabled in config, enable ja4 support + * + * Implemented using atomic to allow rule reloads to do this at + * runtime. + */ +void SSLEnableJA4(void) { - if (SC_ATOMIC_GET(ssl_config.enable_ja3)) { - return true; + if (g_disable_hashing || ssl_config.disable_ja4) { + return; } - return false; + if (SC_ATOMIC_GET(ssl_config.enable_ja4)) { + return; + } + SC_ATOMIC_SET(ssl_config.enable_ja4, 1); +} + +/** + * \brief return whether ja3 is effectively enabled + * + * This means that it either has been enabled explicitly or has been + * enabled by having loaded a rule while not being explicitly disabled. + * + * \retval true if enabled, false otherwise + */ +bool SSLJA3IsEnabled(void) +{ + return SC_ATOMIC_GET(ssl_config.enable_ja3); +} + +/** + * \brief return whether ja4 is effectively enabled + * + * This means that it either has been enabled explicitly or has been + * enabled by having loaded a rule while not being explicitly disabled. + * + * \retval true if enabled, false otherwise + */ +bool SSLJA4IsEnabled(void) +{ + return SC_ATOMIC_GET(ssl_config.enable_ja4); } diff --git a/src/app-layer-ssl.h b/src/app-layer-ssl.h index f2e42622308e..ab5768d9665f 100644 --- a/src/app-layer-ssl.h +++ b/src/app-layer-ssl.h @@ -141,6 +141,8 @@ enum { #define SSL_EXTENSION_SNI 0x0000 #define SSL_EXTENSION_ELLIPTIC_CURVES 0x000a #define SSL_EXTENSION_EC_POINT_FORMATS 0x000b +#define SSL_EXTENSION_SIGNATURE_ALGORITHMS 0x000d +#define SSL_EXTENSION_ALPN 0x0010 #define SSL_EXTENSION_SESSION_TICKET 0x0023 #define SSL_EXTENSION_EARLY_DATA 0x002a #define SSL_EXTENSION_SUPPORTED_VERSIONS 0x002b @@ -267,6 +269,8 @@ typedef struct SSLStateConnp_ { JA3Buffer *ja3_str; char *ja3_hash; + SCJA4 *ja4; + /* handshake tls fragmentation buffer. Handshake messages can be fragmented over multiple * TLS records. */ uint8_t *hs_buffer; @@ -307,5 +311,7 @@ void RegisterSSLParsers(void); void SSLVersionToString(uint16_t, char *); void SSLEnableJA3(void); bool SSLJA3IsEnabled(void); +void SSLEnableJA4(void); +bool SSLJA4IsEnabled(void); #endif /* __APP_LAYER_SSL_H__ */ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 595bffcc5737..714f37daea8b 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -238,6 +238,7 @@ #include "detect-quic-version.h" #include "detect-quic-cyu-hash.h" #include "detect-quic-cyu-string.h" +#include "detect-ja4-hash.h" #include "detect-bypass.h" #include "detect-ftpdata.h" @@ -703,6 +704,7 @@ void SigTableSetup(void) DetectQuicVersionRegister(); DetectQuicCyuHashRegister(); DetectQuicCyuStringRegister(); + DetectJa4HashRegister(); DetectBypassRegister(); DetectConfigRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index eff9c0ed2572..3e6897a146f8 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -346,6 +346,8 @@ enum DetectKeywordId { DETECT_AL_IKE_NONCE, DETECT_AL_IKE_KEY_EXCHANGE, + DETECT_AL_JA4_HASH, + /* make sure this stays last */ DETECT_TBLSIZE, }; diff --git a/src/detect-ja4-hash.c b/src/detect-ja4-hash.c new file mode 100644 index 000000000000..a8e7b4aa037a --- /dev/null +++ b/src/detect-ja4-hash.c @@ -0,0 +1,203 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Sascha Steinbiss + * + * Implements support for ja4.hash keyword. + */ + +#include "suricata-common.h" +#include "threads.h" +#include "decode.h" +#include "detect.h" + +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-mpm.h" +#include "detect-engine-prefilter.h" +#include "detect-content.h" +#include "detect-pcre.h" +#include "detect-ja4-hash.h" + +#include "flow.h" +#include "flow-util.h" +#include "flow-var.h" + +#include "conf.h" +#include "conf-yaml-loader.h" + +#include "util-debug.h" +#include "util-spm.h" +#include "util-print.h" +#include "util-ja3.h" + +#include "stream-tcp.h" + +#include "app-layer.h" +#include "app-layer-ssl.h" + +#include "util-unittest.h" +#include "util-unittest-helper.h" + +#ifndef HAVE_JA4 + +static int DetectJA4SetupNoSupport(DetectEngineCtx *a, Signature *b, const char *c) +{ + SCLogError("no JA4 support built in"); + return -1; +} + +void DetectJa4HashRegister(void) +{ + sigmatch_table[DETECT_AL_JA4_HASH].name = "ja4.hash"; + sigmatch_table[DETECT_AL_JA4_HASH].alias = "ja4_hash"; + sigmatch_table[DETECT_AL_JA4_HASH].desc = "sticky buffer to match the JA4 hash buffer"; + sigmatch_table[DETECT_AL_JA4_HASH].url = "/rules/ja4-keywords.html#ja4-hash"; + sigmatch_table[DETECT_AL_JA4_HASH].Setup = DetectJA4SetupNoSupport; + sigmatch_table[DETECT_AL_JA4_HASH].Free = NULL; + sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_NOOPT; + sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_INFO_STICKY_BUFFER; +} + +#else /* HAVE_JA4 */ + +static int DetectJa4HashSetup(DetectEngineCtx *, Signature *, const char *); +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Flow *f, const uint8_t flow_flags, void *txv, + const int list_id); +int Ja4IsDisabled(const char *type); +static InspectionBuffer *Ja4DetectGetHash(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv, + const int list_id); + +static int g_ja4_hash_buffer_id = 0; + +/** + * \brief Registration function for keyword: ja4.hash + */ +void DetectJa4HashRegister(void) +{ + sigmatch_table[DETECT_AL_JA4_HASH].name = "ja4.hash"; + sigmatch_table[DETECT_AL_JA4_HASH].alias = "ja4_hash"; + sigmatch_table[DETECT_AL_JA4_HASH].desc = "sticky buffer to match the JA4 hash buffer"; + sigmatch_table[DETECT_AL_JA4_HASH].url = "/rules/ja4-keywords.html#ja4-hash"; + sigmatch_table[DETECT_AL_JA4_HASH].Setup = DetectJa4HashSetup; + sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_NOOPT; + sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_INFO_STICKY_BUFFER; + + DetectAppLayerInspectEngineRegister("ja4.hash", ALPROTO_TLS, SIG_FLAG_TOSERVER, 0, + DetectEngineInspectBufferGeneric, GetData); + + DetectAppLayerMpmRegister( + "ja4.hash", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister, GetData, ALPROTO_TLS, 0); + + DetectAppLayerMpmRegister("ja4.hash", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister, + Ja4DetectGetHash, ALPROTO_QUIC, 1); + + DetectAppLayerInspectEngineRegister("ja4.hash", ALPROTO_QUIC, SIG_FLAG_TOSERVER, 1, + DetectEngineInspectBufferGeneric, Ja4DetectGetHash); + + DetectBufferTypeSetDescriptionByName("ja4.hash", "TLS JA4 hash"); + + g_ja4_hash_buffer_id = DetectBufferTypeGetByName("ja4.hash"); +} + +/** + * \brief this function setup the ja4.hash modifier keyword used in the rule + * + * \param de_ctx Pointer to the Detection Engine Context + * \param s Pointer to the Signature to which the current keyword belongs + * \param str Should hold an empty string always + * + * \retval 0 On success + * \retval -1 On failure + */ +static int DetectJa4HashSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + if (DetectBufferSetActiveList(de_ctx, s, g_ja4_hash_buffer_id) < 0) + return -1; + + if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_TLS && s->alproto != ALPROTO_QUIC) { + SCLogError("rule contains conflicting protocols."); + return -1; + } + + /* try to enable JA4 */ + SSLEnableJA4(); + + /* check if JA4 enabling had an effect */ + if (!RunmodeIsUnittests() && !SSLJA4IsEnabled()) { + if (!SigMatchSilentErrorEnabled(de_ctx, DETECT_AL_JA4_HASH)) { + SCLogError("JA4 support is not enabled"); + } + return -2; + } + s->init_data->init_flags |= SIG_FLAG_INIT_JA; + + return 0; +} + +#define SC_JA4_HEX_LEN 36 + +static InspectionBuffer *GetData(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) { + const SSLState *ssl_state = (SSLState *)f->alstate; + + if (ssl_state->client_connp.ja4 == NULL) { + return NULL; + } + + uint8_t data[SC_JA4_HEX_LEN]; + SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[SC_JA4_HEX_LEN])data); + + InspectionBufferSetup(det_ctx, list_id, buffer, data, 0); + InspectionBufferCopy(buffer, data, SC_JA4_HEX_LEN); + InspectionBufferApplyTransforms(buffer, transforms); + } + + return buffer; +} + +static InspectionBuffer *Ja4DetectGetHash(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_ja4(txv, &b, &b_len) != 1) + return NULL; + if (b == NULL || b_len == 0) + return NULL; + + InspectionBufferSetup(det_ctx, list_id, buffer, NULL, 0); + InspectionBufferCopy(buffer, (uint8_t *)b, SC_JA4_HEX_LEN); + InspectionBufferApplyTransforms(buffer, transforms); + } + return buffer; +} + +#endif /* HAVE_JA4 */ diff --git a/src/detect-ja4-hash.h b/src/detect-ja4-hash.h new file mode 100644 index 000000000000..f3a5782e36fd --- /dev/null +++ b/src/detect-ja4-hash.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Sascha Steinbiss + */ + +#ifndef __DETECT_JA4_HASH_H__ +#define __DETECT_JA4_HASH_H__ + +/* Prototypes */ +void DetectJa4HashRegister(void); + +#endif /* __DETECT_JA4_HASH_H__ */ diff --git a/src/detect-parse.c b/src/detect-parse.c index e1c073efd6a3..e433bf1fa5e8 100644 --- a/src/detect-parse.c +++ b/src/detect-parse.c @@ -2090,9 +2090,9 @@ static int SigValidate(DetectEngineCtx *de_ctx, Signature *s) DetectLuaPostSetup(s); #endif - if (s->init_data->init_flags & SIG_FLAG_INIT_JA3 && s->alproto != ALPROTO_UNKNOWN && + if ((s->init_data->init_flags & SIG_FLAG_INIT_JA) && s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_TLS && s->alproto != ALPROTO_QUIC) { - SCLogError("Cannot have ja3 with protocol %s.", AppProtoToString(s->alproto)); + SCLogError("Cannot have ja3/ja4 with protocol %s.", AppProtoToString(s->alproto)); SCReturnInt(0); } if ((s->flags & SIG_FLAG_FILESTORE) || s->file_flags != 0 || diff --git a/src/detect-tls-ja3-hash.c b/src/detect-tls-ja3-hash.c index 0cfe18d66e65..dee5b53690ef 100644 --- a/src/detect-tls-ja3-hash.c +++ b/src/detect-tls-ja3-hash.c @@ -56,6 +56,27 @@ #include "util-unittest.h" #include "util-unittest-helper.h" +#ifndef HAVE_JA3 + +static int DetectJA3SetupNoSupport(DetectEngineCtx *a, Signature *b, const char *c) +{ + SCLogError("no JA3 support built in"); + return -1; +} + +void DetectTlsJa3HashRegister(void) +{ + sigmatch_table[DETECT_AL_TLS_JA3_HASH].name = "ja3.hash"; + sigmatch_table[DETECT_AL_TLS_JA3_HASH].alias = "ja3_hash"; + sigmatch_table[DETECT_AL_TLS_JA3_HASH].desc = "sticky buffer to match the JA3 hash buffer"; + sigmatch_table[DETECT_AL_TLS_JA3_HASH].url = "/rules/ja3-keywords.html#ja3-hash"; + sigmatch_table[DETECT_AL_TLS_JA3_HASH].Setup = DetectJA3SetupNoSupport; + sigmatch_table[DETECT_AL_TLS_JA3_HASH].flags |= SIGMATCH_NOOPT; + sigmatch_table[DETECT_AL_TLS_JA3_HASH].flags |= SIGMATCH_INFO_STICKY_BUFFER; +} + +#else /* HAVE_JA3 */ + static int DetectTlsJa3HashSetup(DetectEngineCtx *, Signature *, const char *); static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, const DetectEngineTransforms *transforms, @@ -134,7 +155,7 @@ static int DetectTlsJa3HashSetup(DetectEngineCtx *de_ctx, Signature *s, const ch } return -2; } - s->init_data->init_flags |= SIG_FLAG_INIT_JA3; + s->init_data->init_flags |= SIG_FLAG_INIT_JA; return 0; } @@ -225,3 +246,5 @@ static void DetectTlsJa3HashSetupCallback(const DetectEngineCtx *de_ctx, } } } + +#endif /* HAVE_JA3 */ diff --git a/src/detect-tls-ja3-string.c b/src/detect-tls-ja3-string.c index 6c2fbc6ad975..36a7a89c04ad 100644 --- a/src/detect-tls-ja3-string.c +++ b/src/detect-tls-ja3-string.c @@ -56,6 +56,27 @@ #include "util-unittest.h" #include "util-unittest-helper.h" +#ifndef HAVE_JA3 + +static int DetectJA3SetupNoSupport(DetectEngineCtx *a, Signature *b, const char *c) +{ + SCLogError("no JA3 support built in"); + return -1; +} + +void DetectTlsJa3StringRegister(void) +{ + sigmatch_table[DETECT_AL_TLS_JA3_STRING].name = "ja3.string"; + sigmatch_table[DETECT_AL_TLS_JA3_STRING].alias = "ja3_string"; + sigmatch_table[DETECT_AL_TLS_JA3_STRING].desc = "sticky buffer to match the JA3 string buffer"; + sigmatch_table[DETECT_AL_TLS_JA3_STRING].url = "/rules/ja3-keywords.html#ja3-string"; + sigmatch_table[DETECT_AL_TLS_JA3_STRING].Setup = DetectJA3SetupNoSupport; + sigmatch_table[DETECT_AL_TLS_JA3_STRING].flags |= SIGMATCH_NOOPT; + sigmatch_table[DETECT_AL_TLS_JA3_STRING].flags |= SIGMATCH_INFO_STICKY_BUFFER; +} + +#else /* HAVE_JA3 */ + static int DetectTlsJa3StringSetup(DetectEngineCtx *, Signature *, const char *); static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, const DetectEngineTransforms *transforms, @@ -123,7 +144,7 @@ static int DetectTlsJa3StringSetup(DetectEngineCtx *de_ctx, Signature *s, const } return -2; } - s->init_data->init_flags |= SIG_FLAG_INIT_JA3; + s->init_data->init_flags |= SIG_FLAG_INIT_JA; return 0; } @@ -150,3 +171,5 @@ static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, return buffer; } + +#endif /* HAVE_JA3 */ diff --git a/src/detect-tls-ja3s-hash.c b/src/detect-tls-ja3s-hash.c index a1a334a4f16b..9ae6c4f12bc6 100644 --- a/src/detect-tls-ja3s-hash.c +++ b/src/detect-tls-ja3s-hash.c @@ -56,6 +56,26 @@ #include "util-unittest.h" #include "util-unittest-helper.h" +#ifndef HAVE_JA3 + +static int DetectJA3SetupNoSupport(DetectEngineCtx *a, Signature *b, const char *c) +{ + SCLogError("no JA3 support built in"); + return -1; +} + +void DetectTlsJa3SHashRegister(void) +{ + sigmatch_table[DETECT_AL_TLS_JA3S_HASH].name = "ja3s.hash"; + sigmatch_table[DETECT_AL_TLS_JA3S_HASH].desc = "sticky buffer to match the JA3S hash buffer"; + sigmatch_table[DETECT_AL_TLS_JA3S_HASH].url = "/rules/ja3-keywords.html#ja3s-hash"; + sigmatch_table[DETECT_AL_TLS_JA3S_HASH].Setup = DetectJA3SetupNoSupport; + sigmatch_table[DETECT_AL_TLS_JA3S_HASH].flags |= SIGMATCH_NOOPT; + sigmatch_table[DETECT_AL_TLS_JA3S_HASH].flags |= SIGMATCH_INFO_STICKY_BUFFER; +} + +#else /* HAVE_JA3 */ + static int DetectTlsJa3SHashSetup(DetectEngineCtx *, Signature *, const char *); static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, const DetectEngineTransforms *transforms, @@ -132,7 +152,7 @@ static int DetectTlsJa3SHashSetup(DetectEngineCtx *de_ctx, Signature *s, const c } return -2; } - s->init_data->init_flags |= SIG_FLAG_INIT_JA3; + s->init_data->init_flags |= SIG_FLAG_INIT_JA; return 0; } @@ -223,3 +243,5 @@ static void DetectTlsJa3SHashSetupCallback(const DetectEngineCtx *de_ctx, } } } + +#endif /* HAVE_JA3 */ diff --git a/src/detect-tls-ja3s-string.c b/src/detect-tls-ja3s-string.c index 32117df68442..11874aba565a 100644 --- a/src/detect-tls-ja3s-string.c +++ b/src/detect-tls-ja3s-string.c @@ -56,6 +56,27 @@ #include "util-unittest.h" #include "util-unittest-helper.h" +#ifndef HAVE_JA3 + +static int DetectJA3SetupNoSupport(DetectEngineCtx *a, Signature *b, const char *c) +{ + SCLogError("no JA3 support built in"); + return -1; +} + +void DetectTlsJa3SStringRegister(void) +{ + sigmatch_table[DETECT_AL_TLS_JA3S_STRING].name = "ja3s.string"; + sigmatch_table[DETECT_AL_TLS_JA3S_STRING].desc = + "sticky buffer to match the JA3S string buffer"; + sigmatch_table[DETECT_AL_TLS_JA3S_STRING].url = "/rules/ja3-keywords.html#ja3s-string"; + sigmatch_table[DETECT_AL_TLS_JA3S_STRING].Setup = DetectJA3SetupNoSupport; + sigmatch_table[DETECT_AL_TLS_JA3S_STRING].flags |= SIGMATCH_NOOPT; + sigmatch_table[DETECT_AL_TLS_JA3S_STRING].flags |= SIGMATCH_INFO_STICKY_BUFFER; +} + +#else /* HAVE_JA3 */ + static int DetectTlsJa3SStringSetup(DetectEngineCtx *, Signature *, const char *); static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, const DetectEngineTransforms *transforms, @@ -123,7 +144,7 @@ static int DetectTlsJa3SStringSetup(DetectEngineCtx *de_ctx, Signature *s, const } return -2; } - s->init_data->init_flags |= SIG_FLAG_INIT_JA3; + s->init_data->init_flags |= SIG_FLAG_INIT_JA; return 0; } @@ -150,3 +171,5 @@ static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, return buffer; } + +#endif /* HAVE_JA3 */ diff --git a/src/detect.h b/src/detect.h index 76c6d2b66f03..8ba17a04399c 100644 --- a/src/detect.h +++ b/src/detect.h @@ -289,7 +289,8 @@ typedef struct DetectPort_ { #define SIG_FLAG_INIT_PRIO_EXPLICIT \ BIT_U32(8) /**< priority is explicitly set by the priority keyword */ #define SIG_FLAG_INIT_FILEDATA BIT_U32(9) /**< signature has filedata keyword */ -#define SIG_FLAG_INIT_JA3 BIT_U32(10) /**< signature has ja3 keyword */ +#define SIG_FLAG_INIT_JA BIT_U32(10) /**< signature has ja3/ja4 keyword */ +#define SIG_FLAG_INIT_OVERFLOW BIT_U32(11) /**< signature has overflown buffers */ /* signature mask flags */ /** \note: additions should be added to the rule analyzer as well */ diff --git a/src/feature.h b/src/feature.h index 6549c5bbeab5..a1420f4a73ab 100644 --- a/src/feature.h +++ b/src/feature.h @@ -26,6 +26,8 @@ /* Provided feature names */ #define FEATURE_OUTPUT_FILESTORE "output::file-store" +#define FEATURE_JA3 "ja3" +#define FEATURE_JA4 "ja4" void ProvidesFeature(const char *); bool RequiresFeature(const char *); diff --git a/src/output-json-tls.c b/src/output-json-tls.c index 7460a32f2574..51948e9786c7 100644 --- a/src/output-json-tls.c +++ b/src/output-json-tls.c @@ -76,6 +76,7 @@ SC_ATOMIC_EXTERN(unsigned int, cert_id); #define LOG_TLS_FIELD_CLIENT (1 << 13) /**< client fields (issuer, subject, etc) */ #define LOG_TLS_FIELD_CLIENT_CERT (1 << 14) #define LOG_TLS_FIELD_CLIENT_CHAIN (1 << 15) +#define LOG_TLS_FIELD_JA4 (1 << 16) typedef struct { const char *name; @@ -90,7 +91,7 @@ TlsFields tls_fields[] = { { "version", LOG_TLS_FIELD_VERSION }, { "chain", LOG_TLS_FIELD_CHAIN }, { "session_resumed", LOG_TLS_FIELD_SESSION_RESUMED }, { "ja3", LOG_TLS_FIELD_JA3 }, { "ja3s", LOG_TLS_FIELD_JA3S }, { "client", LOG_TLS_FIELD_CLIENT }, { "client_certificate", LOG_TLS_FIELD_CLIENT_CERT }, - { "client_chain", LOG_TLS_FIELD_CLIENT_CHAIN }, { NULL, -1 } }; + { "client_chain", LOG_TLS_FIELD_CLIENT_CHAIN }, { "ja4", LOG_TLS_FIELD_JA4 }, { NULL, -1 } }; typedef struct OutputTlsCtx_ { uint32_t flags; /** Store mode */ @@ -210,6 +211,16 @@ static void JsonTlsLogJa3(JsonBuilder *js, SSLState *ssl_state) } } +static void JsonTlsLogSCJA4(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->client_connp.ja4 != NULL) { + uint8_t buffer[36]; + /* JA4 hash has 36 characters */ + SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[36])buffer); + jb_set_string_from_bytes(js, "ja4", buffer, 36); + } +} + static void JsonTlsLogJa3SHash(JsonBuilder *js, SSLState *ssl_state) { if (ssl_state->server_connp.ja3_hash != NULL) { @@ -381,6 +392,10 @@ static void JsonTlsLogJSONCustom(OutputTlsCtx *tls_ctx, JsonBuilder *js, if (tls_ctx->fields & LOG_TLS_FIELD_JA3S) JsonTlsLogJa3S(js, ssl_state); + /* tls ja4 */ + if (tls_ctx->fields & LOG_TLS_FIELD_JA4) + JsonTlsLogSCJA4(js, ssl_state); + if (tls_ctx->fields & LOG_TLS_FIELD_CLIENT) { const bool log_cert = (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CERT) != 0; const bool log_chain = (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CHAIN) != 0; @@ -421,6 +436,9 @@ static bool JsonTlsLogJSONExtendedAux(void *vtx, JsonBuilder *tjs) /* tls ja3s */ JsonTlsLogJa3S(tjs, state); + /* tls ja4 */ + JsonTlsLogSCJA4(tjs, state); + if (HasClientCert(&state->client_connp)) { jb_open_object(tjs, "client"); JsonTlsLogClientCert(tjs, &state->client_connp, false, false); diff --git a/src/suricata.c b/src/suricata.c index 126d02f900f0..e82d25f7ab36 100644 --- a/src/suricata.c +++ b/src/suricata.c @@ -741,6 +741,12 @@ static void PrintBuildInfo(void) #ifdef HAVE_LUA strlcat(features, "HAVE_LUA ", sizeof(features)); #endif +#ifdef HAVE_JA3 + strlcat(features, "HAVE_JA3 ", sizeof(features)); +#endif +#ifdef HAVE_JA4 + strlcat(features, "HAVE_JA4 ", sizeof(features)); +#endif #ifdef HAVE_LUAJIT strlcat(features, "HAVE_LUAJIT ", sizeof(features)); #endif @@ -2687,6 +2693,10 @@ int PostConfLoadedSetup(SCInstance *suri) SetMasterExceptionPolicy(); + /* Must occur prior to output mod registration + and app layer setup. */ + FeatureTrackingRegister(); + AppLayerSetup(); /* Suricata will use this umask if provided. By default it will use the @@ -2744,7 +2754,6 @@ int PostConfLoadedSetup(SCInstance *suri) SCReturnInt(TM_ECODE_FAILED); } - FeatureTrackingRegister(); /* must occur prior to output mod registration */ RegisterAllModules(); #ifdef HAVE_PLUGINS SCPluginsLoad(suri->capture_plugin_name, suri->capture_plugin_args); diff --git a/src/util-ja3.c b/src/util-ja3.c index b361b3e74e39..1cbfbd5fc01e 100644 --- a/src/util-ja3.c +++ b/src/util-ja3.c @@ -64,6 +64,8 @@ void Ja3BufferFree(JA3Buffer **buffer) *buffer = NULL; } +#ifdef HAVE_JA3 + /** * \internal * \brief Resize buffer if it is full. @@ -300,3 +302,34 @@ InspectionBuffer *Ja3DetectGetString(DetectEngineThreadCtx *det_ctx, } return buffer; } + +#else /* HAVE_JA3 */ + +/* Stubs for when JA3 is disabled */ + +int Ja3BufferAppendBuffer(JA3Buffer **buffer1, JA3Buffer **buffer2) +{ + return 0; +} + +int Ja3BufferAddValue(JA3Buffer **buffer, uint32_t value) +{ + return 0; +} + +char *Ja3GenerateHash(JA3Buffer *buffer) +{ + char *ja3_hash = SCCalloc(SC_MD5_HEX_LEN + 1, 1); + if (ja3_hash == NULL) { + SCLogError("Error allocating memory for JA3 hash"); + return NULL; + } + return ja3_hash; +} + +int Ja3IsDisabled(const char *type) +{ + return true; +} + +#endif /* HAVE_JA3 */ diff --git a/src/util-ja3.h b/src/util-ja3.h index 5a0f8c508e6d..5a4fa0a12c0f 100644 --- a/src/util-ja3.h +++ b/src/util-ja3.h @@ -41,6 +41,7 @@ int Ja3BufferAddValue(JA3Buffer **, uint32_t); char *Ja3GenerateHash(JA3Buffer *); int Ja3IsDisabled(const char *); +#ifdef HAVE_JA3 InspectionBuffer *Ja3DetectGetHash(DetectEngineThreadCtx *det_ctx, const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv, const int list_id); @@ -48,6 +49,5 @@ InspectionBuffer *Ja3DetectGetHash(DetectEngineThreadCtx *det_ctx, InspectionBuffer *Ja3DetectGetString(DetectEngineThreadCtx *det_ctx, const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv, const int list_id); - +#endif /* HAVE_JA3 */ #endif /* __UTIL_JA3_H__ */ - diff --git a/suricata.yaml.in b/suricata.yaml.in index 97a7e1318edd..bc7b681d96c2 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -252,7 +252,7 @@ outputs: # session id #session-resumption: no # custom controls which TLS fields that are included in eve-log - #custom: [subject, issuer, session_resumed, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s] + #custom: [subject, issuer, session_resumed, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s, ja4] - files: force-magic: no # force logging magic on all logged files # force logging of checksums, available hash functions are md5, @@ -887,9 +887,10 @@ app-layer: detection-ports: dp: 443 - # Generate JA3 fingerprint from client hello. If not specified it + # Generate JA3/JA4 fingerprints from client hello. If not specified it # will be disabled by default, but enabled if rules require it. #ja3-fingerprints: auto + #ja4-fingerprints: auto # What to do when the encrypted communications start: # - default: keep tracking TLS session, check for protocol anomalies,