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")
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}
* "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:
* "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):
::
"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
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.
dns-keywords
tls-keywords
ssh-keywords
- ja3-keywords
+ ja-keywords
modbus-keyword
dcerpc-keywords
dhcp-keywords
-JA3 Keywords
-============
+JA3/JA4 Keywords
+================
-Suricata comes with a JA3 integration (https://github.com/salesforce/ja3). JA3 is used to fingerprint TLS clients.
+Suricata comes with JA3 (https://github.com/salesforce/ja3) and
+JA4 (https://github.com/FoxIO-LLC/ja4) integration.
+JA3 and JA4 are used to fingerprint TLS and QUIC clients.
-JA3 must be enabled in the Suricata config file (set 'app-layer.protocols.tls.ja3-fingerprints' to 'yes').
+Support must be enabled in the Suricata config file (set
+``app-layer.protocols.tls.ja{3,4}-fingerprints`` to ``yes``). If it is not
+explicitly disabled (``no``) , it will be enabled if a loaded rule requires it.
+Note that JA3/JA4 support can also be disabled at compile time; it is possible to
+use the ``requires: feature ja{3,4};`` keyword to skip rules if no JA3/JA4 support is
+present.
ja3.hash
--------
``ja3s.string`` is a 'sticky buffer'.
``ja3s.string`` can be used as ``fast_pattern``.
+
+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``.
+
"renewal_time": {
"type": "integer"
},
- "requested_ip":{
+ "requested_ip": {
"type": "string"
},
"subnet_mask": {
"type": {
"type": "string"
},
- "vendor_class_identifier":{
+ "vendor_class_identifier": {
"type": "string"
},
"dns_servers": {
},
"additionalProperties": false
},
+ "ja4": {
+ "type": "string"
+ },
"sni": {
"type": "string"
},
}
},
"additionalProperties": false
+ },
+ "ja4": {
+ "type": "string"
}
},
"additionalProperties": false
debug = []
debug-validate = []
ja3 = []
+ja4 = []
[dependencies]
nom7 = { version="7.0", package="nom" }
RUST_FEATURES += ja3
endif
+if HAVE_JA4
+RUST_FEATURES += ja4
+endif
+
if DEBUG
RUST_FEATURES += debug
endif
--- /dev/null
+/* 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 <sascha@steinbiss.name>
+
+*/
+
+#[cfg(feature = "ja4")]
+use digest::Digest;
+use libc::c_uchar;
+#[cfg(feature = "ja4")]
+use sha2::Sha256;
+#[cfg(feature = "ja4")]
+use std::cmp::min;
+use std::os::raw::c_char;
+use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion};
+
+#[derive(Debug, PartialEq)]
+pub struct JA4 {
+ tls_version: Option<TlsVersion>,
+ ciphersuites: Vec<TlsCipherSuiteID>,
+ extensions: Vec<TlsExtensionType>,
+ signature_algorithms: Vec<u16>,
+ domain: bool,
+ alpn: [char; 2],
+ quic: bool,
+ // Some extensions contribute to the total count component of the
+ // fingerprint, yet are not to be included in the SHA256 hash component.
+ // Let's track the count separately.
+ nof_exts: 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,
+ // Vec::new() does not allocate memory until filled, which we
+ // will not do here.
+ ciphersuites: Vec::new(),
+ extensions: Vec::new(),
+ signature_algorithms: Vec::new(),
+ domain: false,
+ alpn: ['0', '0'],
+ quic: false,
+ nof_exts: 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<TlsVersion>) -> &'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,
+ }
+ }
+
+ 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);
+ }
+
+ 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 = min(99, self.ciphersuites.len()),
+ nof_e = min(99, 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<String> = 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<String> = sorted_exts
+ .iter()
+ .map(|v| format!("{:04x}", u16::from(*v)))
+ .collect();
+ let ja4_c1_raw = sorted_extstrings.join(",");
+ let unsorted_sigalgostrings: Vec<String> = 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);
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn SCJA4New() -> *mut JA4 {
+ let j = Box::new(JA4::new());
+ Box::into_raw(j)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4SetTLSVersion(j: &mut JA4, version: u16) {
+ j.set_tls_version(TlsVersion(version));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4AddCipher(j: &mut JA4, cipher: u16) {
+ j.add_cipher_suite(TlsCipherSuiteID(cipher));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4AddExtension(j: &mut JA4, ext: u16) {
+ j.add_extension(TlsExtensionType(ext));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4AddSigAlgo(j: &mut JA4, sigalgo: u16) {
+ j.add_signature_algorithm(sigalgo);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4SetALPN(j: &mut JA4, proto: *const c_char, len: u16) {
+ let b: &[u8] = std::slice::from_raw_parts(proto as *const c_uchar, len as usize);
+ j.set_alpn(b);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4GetHash(j: &mut JA4, out: &mut [u8; 36]) {
+ let hash = j.get_hash();
+ out[0..36].copy_from_slice(hash.as_bytes());
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4Free(j: &mut JA4) {
+ let ja4: Box<JA4> = 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() {
+ // Test whether the limitation of the extension and ciphersuite
+ // count to 99 is reflected correctly.
+ 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");
+ }
+}
pub mod kerberos;
pub mod detect;
+pub mod ja4;
+
#[cfg(feature = "lua")]
pub mod lua;
}
}
+#[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,
*/
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};
// the lifetime of TlsExtension due to references to the slice used for parsing
pub extv: Vec<QuicTlsExtension>,
pub ja3: Option<String>,
+ pub ja4: Option<JA4>,
}
#[derive(Debug, PartialEq)]
// 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<QuicTlsExtension> {
let mut extv = Vec::new();
if let Some(extr) = input {
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();
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);
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 {
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);
+ let extv = quic_get_tls_extensions(ch.ext, &mut ja3, Some(&mut ja4), true);
return Some(Frame::Crypto(Crypto {
ciphers,
extv,
} else {
None
},
+ ja4: if cfg!(feature = "ja4") {
+ Some(ja4)
+ } else {
+ None
+ },
}));
}
ServerHello(sh) => {
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);
+ let extv = quic_get_tls_extensions(sh.ext, &mut ja3, None, false);
return Some(Frame::Crypto(Crypto {
ciphers,
extv,
} else {
None
},
+ ja4: None,
}));
}
_ => {}
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) {
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 {
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;
pub ua: Option<Vec<u8>>,
pub extv: Vec<QuicTlsExtension>,
pub ja3: Option<String>,
+ pub ja4: 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>, ja3: Option<String>, client: bool,
+ extv: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<String>, 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,
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,
ua: None,
extv: Vec::new(),
ja3: None,
+ ja4: None,
client,
tx_data: AppLayerTxData::for_direction(direction),
}
fn new_tx(
&mut self, header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>,
- extb: Vec<QuicTlsExtension>, ja3: Option<String>, client: bool,
+ extb: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<String>, 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);
let mut sni: Option<Vec<u8>> = None;
let mut ua: Option<Vec<u8>> = None;
let mut ja3: Option<String> = None;
+ let mut ja4: Option<String> = None;
let mut extv: Vec<QuicTlsExtension> = Vec::new();
for frame in &data.frames {
match frame {
if let Some(ja3str) = &c.ja3 {
ja3 = Some(ja3str.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());
_ => {}
}
}
- 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) {
None,
Vec::new(),
None,
+ None,
to_server,
);
continue;
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 \
util-ioctl.h \
util-ip.h \
util-ja3.h \
+ util-ja4.h \
util-landlock.h \
util-logopenfile.h \
util-log-redis.h \
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 \
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 */
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;
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) &&
goto invalid_length;
}
- if (SC_ATOMIC_GET(ssl_config.enable_ja3)) {
- JA3Buffer *ja3_cipher_suites = Ja3BufferInit();
- if (ja3_cipher_suites == NULL)
- return -1;
+ const bool enable_ja3 = SC_ATOMIC_GET(ssl_config.enable_ja3);
+
+ if (enable_ja3 || SC_ATOMIC_GET(ssl_config.enable_ja4)) {
+ JA3Buffer *ja3_cipher_suites = NULL;
+
+ if (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 (enable_ja3) {
+ Ja3BufferFree(&ja3_cipher_suites);
+ }
goto invalid_length;
}
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 (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 (enable_ja3) {
+ int rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, &ja3_cipher_suites);
+ if (rc == -1) {
+ return -1;
+ }
}
} else {
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;
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;
+
+ /* Check if reading another protolen bytes would exceed the
+ overall ALPN length; if so, skip and continue */
+ if (alpn_processed_len + protolen > ((uint32_t)alpn_len)) {
+ input += alpn_len - alpn_processed_len;
+ break;
+ }
+
+ /* Only record 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)
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 += ext_len;
+
+ break;
+ }
+
case SSL_EXTENSION_EARLY_DATA:
{
if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
}
}
+ 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;
}
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;
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)
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. */
+ even is enabled in the configuration. */
ProvidesFeature(FEATURE_JA3);
}
}
#endif /* HAVE_JA3 */
+#ifdef HAVE_JA4
+static void CheckJA4Enabled(void)
+{
+ const char *strval = NULL;
+ /* 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 */
+
/**
* \brief Function to register the SSL protocol parser and other functions
*/
#ifdef HAVE_JA3
CheckJA3Enabled();
#endif /* HAVE_JA3 */
+#ifdef HAVE_JA4
+ CheckJA4Enabled();
+#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_JA3 */
+#ifdef HAVE_JA4
+ SC_ATOMIC_SET(ssl_config.enable_ja4, 1);
+#endif /* HAVE_JA4 */
}
}
} else {
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);
}
#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
JA3Buffer *ja3_str;
char *ja3_hash;
+ JA4 *ja4;
+
/* handshake tls fragmentation buffer. Handshake messages can be fragmented over multiple
* TLS records. */
uint8_t *hs_buffer;
void SSLVersionToString(uint16_t, char *);
void SSLEnableJA3(void);
bool SSLJA3IsEnabled(void);
+void SSLEnableJA4(void);
+bool SSLJA4IsEnabled(void);
#endif /* __APP_LAYER_SSL_H__ */
#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"
DetectQuicVersionRegister();
DetectQuicCyuHashRegister();
DetectQuicCyuStringRegister();
+ DetectJa4HashRegister();
DetectBypassRegister();
DetectConfigRegister();
DETECT_AL_IKE_NONCE,
DETECT_AL_IKE_KEY_EXCHANGE,
+ DETECT_AL_JA4_HASH,
+
/* make sure this stays last */
DETECT_TBLSIZE,
};
--- /dev/null
+/* 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 <sascha@steinbiss.name>
+ *
+ * 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-ja4-hash.h"
+
+#include "util-ja4.h"
+
+#include "app-layer-ssl.h"
+
+#ifndef HAVE_JA4
+static int DetectJA4SetupNoSupport(DetectEngineCtx *a, Signature *b, const char *c)
+{
+ SCLogError("no JA4 support built in");
+ return -1;
+}
+#endif /* 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";
+#ifdef HAVE_JA4
+ sigmatch_table[DETECT_AL_JA4_HASH].Setup = DetectJa4HashSetup;
+#else /* HAVE_JA4 */
+ sigmatch_table[DETECT_AL_JA4_HASH].Setup = DetectJA4SetupNoSupport;
+#endif /* HAVE_JA4 */
+ sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_NOOPT;
+ sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_INFO_STICKY_BUFFER;
+
+#ifdef HAVE_JA4
+ DetectAppLayerInspectEngineRegister2("ja4.hash", ALPROTO_TLS, SIG_FLAG_TOSERVER, 0,
+ DetectEngineInspectBufferGeneric, GetData);
+
+ DetectAppLayerMpmRegister2(
+ "ja4.hash", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister, GetData, ALPROTO_TLS, 0);
+
+ DetectAppLayerMpmRegister2("ja4.hash", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister,
+ Ja4DetectGetHash, ALPROTO_QUIC, 1);
+
+ DetectAppLayerInspectEngineRegister2("ja4.hash", ALPROTO_QUIC, SIG_FLAG_TOSERVER, 1,
+ DetectEngineInspectBufferGeneric, Ja4DetectGetHash);
+
+ DetectBufferTypeSetDescriptionByName("ja4.hash", "TLS JA4 hash");
+
+ g_ja4_hash_buffer_id = DetectBufferTypeGetByName("ja4.hash");
+#endif /* HAVE_JA4 */
+}
+
+/**
+ * \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;
+}
+
+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[JA4_HEX_LEN];
+ SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])data);
+
+ InspectionBufferSetup(det_ctx, list_id, buffer, data, 0);
+ InspectionBufferCopy(buffer, data, 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, JA4_HEX_LEN);
+ InspectionBufferApplyTransforms(buffer, transforms);
+ }
+ return buffer;
+}
--- /dev/null
+/* 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 <sascha@steinbiss.name>
+ */
+
+#ifndef __DETECT_JA4_HASH_H__
+#define __DETECT_JA4_HASH_H__
+
+/* Prototypes */
+void DetectJa4HashRegister(void);
+
+#endif /* __DETECT_JA4_HASH_H__ */
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 ||
}
return -2;
}
- s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+ s->init_data->init_flags |= SIG_FLAG_INIT_JA;
return 0;
}
}
return -2;
}
- s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+ s->init_data->init_flags |= SIG_FLAG_INIT_JA;
return 0;
}
}
return -2;
}
- s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+ s->init_data->init_flags |= SIG_FLAG_INIT_JA;
return 0;
}
}
return -2;
}
- s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+ s->init_data->init_flags |= SIG_FLAG_INIT_JA;
return 0;
}
#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 */
/* 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 *);
#include "util-logopenfile.h"
#include "util-ja3.h"
+#include "util-ja4.h"
#include "output-json.h"
#include "output-json-tls.h"
#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;
{ "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 */
}
}
+static void JsonTlsLogSCJA4(JsonBuilder *js, SSLState *ssl_state)
+{
+ if (ssl_state->client_connp.ja4 != NULL) {
+ uint8_t buffer[JA4_HEX_LEN];
+ /* JA4 hash has 36 characters */
+ SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])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) {
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;
/* 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);
#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
--- /dev/null
+/* Copyright (C) 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.
+ */
+
+/**
+ * \file
+ *
+ * \author Sascha Steinbiss <sascha@steinbiss.name>
+ */
+
+#ifndef SURICATA_UTIL_JA4_H
+#define SURICATA_UTIL_JA4_H
+
+#define JA4_HEX_LEN 36
+
+#endif /* SURICATA_UTIL_JA4_H */
# 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,
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,