ja4: implement for TLS and QUIC

Ticket: OISF#6379
pull/10856/head
Sascha Steinbiss 2 years ago committed by Victor Julien
parent 9d0db71ebf
commit 120313f4da

@ -2217,6 +2217,17 @@
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")
@ -2671,6 +2682,7 @@ SURICATA_BUILD_CONF="Suricata Configuration:
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}

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

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

@ -17,7 +17,7 @@ Suricata Rules
dns-keywords
tls-keywords
ssh-keywords
ja3-keywords
ja-keywords
modbus-keyword
dcerpc-keywords
dhcp-keywords

@ -1,9 +1,16 @@
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
--------
@ -71,3 +78,19 @@ Example::
``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``.

@ -3060,6 +3060,9 @@
},
"additionalProperties": false
},
"ja4": {
"type": "string"
},
"sni": {
"description": "Server Name Indication",
"type": "string"
@ -5594,6 +5597,9 @@
}
},
"additionalProperties": false
},
"ja4": {
"type": "string"
}
},
"additionalProperties": false

@ -23,6 +23,7 @@ strict = []
debug = []
debug-validate = []
ja3 = []
ja4 = []
[dependencies]
nom7 = { version="7.0", package="nom" }

@ -22,6 +22,10 @@ if HAVE_JA3
RUST_FEATURES += ja3
endif
if HAVE_JA4
RUST_FEATURES += ja4
endif
if DEBUG
RUST_FEATURES += debug
endif

@ -0,0 +1,383 @@
/* 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;
use std::{cmp::min, 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");
}
}

@ -84,6 +84,8 @@ pub mod filetracker;
pub mod kerberos;
pub mod detect;
pub mod ja4;
#[cfg(feature = "lua")]
pub mod lua;

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

@ -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<QuicTlsExtension>,
pub ja3: Option<String>,
pub ja4: Option<JA4>,
}
#[derive(Debug, PartialEq)]
@ -235,7 +237,7 @@ fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec<TlsExtension>) {
// 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 {
@ -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<Frame> {
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,10 +324,11 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
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,
@ -304,6 +337,11 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
} else {
None
},
ja4: if cfg!(feature = "ja4") {
Some(ja4)
} else {
None
},
}));
}
ServerHello(sh) => {
@ -313,7 +351,7 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
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,
@ -322,6 +360,7 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
} else {
None
},
ja4: None,
}));
}
_ => {}
@ -520,8 +559,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) {

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

@ -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<Vec<u8>>,
pub extv: Vec<QuicTlsExtension>,
pub ja3: Option<String>,
pub ja4: Option<String>,
pub client: bool,
tx_data: AppLayerTxData,
}
@ -55,9 +56,13 @@ pub struct QuicTransaction {
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,
@ -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<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);
@ -212,6 +223,7 @@ impl QuicState {
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 {
@ -233,6 +245,14 @@ impl QuicState {
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());
@ -248,7 +268,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) {
@ -305,6 +325,7 @@ impl QuicState {
None,
Vec::new(),
None,
None,
to_server,
);
continue;

@ -232,6 +232,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 \
@ -551,6 +552,7 @@ noinst_HEADERS = \
util-ioctl.h \
util-ip.h \
util-ja3.h \
util-ja4.h \
util-landlock.h \
util-logopenfile.h \
util-log-redis.h \
@ -843,6 +845,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 \

@ -145,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 */
@ -156,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;
@ -693,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) &&
@ -836,17 +844,25 @@ 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;
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;
}
@ -854,19 +870,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 (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 {
@ -1027,6 +1049,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;
@ -1173,6 +1199,113 @@ invalid_length:
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)
@ -1274,6 +1407,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 += ext_len;
break;
}
case SSL_EXTENSION_EARLY_DATA:
{
if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
@ -1327,6 +1482,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;
}
@ -1375,6 +1537,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;
@ -2698,6 +2869,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)
@ -2970,12 +3143,37 @@ static void CheckJA3Enabled(void)
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
*/
@ -3078,17 +3276,27 @@ void RegisterSSLParsers(void)
#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 {
@ -3115,10 +3323,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);
}

@ -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;
JA4 *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 /* SURICATA_APP_LAYER_SSL_H */

@ -240,6 +240,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"
@ -707,6 +708,7 @@ void SigTableSetup(void)
DetectQuicVersionRegister();
DetectQuicCyuHashRegister();
DetectQuicCyuStringRegister();
DetectJa4HashRegister();
DetectBypassRegister();
DetectConfigRegister();

@ -348,6 +348,8 @@ enum DetectKeywordId {
DETECT_AL_IKE_NONCE,
DETECT_AL_IKE_KEY_EXCHANGE,
DETECT_AL_JA4_HASH,
/* make sure this stays last */
DETECT_TBLSIZE,
};

@ -0,0 +1,173 @@
/* 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
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");
#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;
}

@ -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 <sascha@steinbiss.name>
*/
#ifndef __DETECT_JA4_HASH_H__
#define __DETECT_JA4_HASH_H__
/* Prototypes */
void DetectJa4HashRegister(void);
#endif /* __DETECT_JA4_HASH_H__ */

@ -2091,9 +2091,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 ||

@ -148,7 +148,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;
}

@ -137,7 +137,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;
}

@ -146,7 +146,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;
}

@ -137,7 +137,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;
}

@ -290,7 +290,7 @@ 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 */
/* signature mask flags */
/** \note: additions should be added to the rule analyzer as well */

@ -27,6 +27,7 @@
/* 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 *);

@ -46,6 +46,7 @@
#include "util-logopenfile.h"
#include "util-ja3.h"
#include "util-ja4.h"
#include "output-json.h"
#include "output-json-tls.h"
@ -76,6 +77,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 +92,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 +212,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[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) {
@ -381,6 +393,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 +437,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);

@ -747,6 +747,9 @@ static void PrintBuildInfo(void)
#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

@ -0,0 +1,29 @@
/* 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 */

@ -257,7 +257,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,
@ -895,9 +895,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,

Loading…
Cancel
Save