Skip to content
Network Security

Detecting Cobalt Strike by its TLS Handshake

10 min read

Every TLS connection starts with a ClientHello message. The client announces which TLS version it supports, which cipher suites it offers, which extensions it uses, and which elliptic curves it prefers. This combination is remarkably stable across runs of the same software. Cobalt Strike's Beacon, Metasploit's Meterpreter, and Emotet all have distinctive TLS fingerprints that survive IP rotation, domain fronting, and certificate changes.

Inner Warden captures these fingerprints in real time using pure Rust, no libpcap, no external dependencies. This post explains how JA3 and JA4 fingerprinting works, how we implemented it, and which malware families you can detect today.

What is JA3

JA3 was created by John Althouse, Jeff Atkinson, and Josh Atkins at Salesforce in 2017. The idea is simple: extract five fields from the TLS ClientHello, concatenate them in a specific order separated by commas, and hash the result with MD5.

JA3 formula
MD5(
  TLSVersion,
  Ciphers,
  Extensions,
  EllipticCurves,
  EllipticCurvePointFormats
)

Example raw string:
771,4866-4867-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,0-11-10-35-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2

JA3 hash: e35d3904ce04caf3c038c24a8983e13e

The five fields capture the client's TLS capabilities in sufficient detail to distinguish most software implementations. A Chrome browser on Windows produces a different JA3 than Firefox, which differs from curl, which differs from Cobalt Strike.

How JA4 improves on JA3

JA4, created by John Althouse at FoxIO in 2023, addresses several limitations of JA3. Where JA3 is a single opaque hash, JA4 produces a human-readable fingerprint with three sections:

  • JA4_a - protocol type + TLS version + SNI presence + cipher count + extension count + ALPN first value. This is readable without lookup tables.
  • JA4_b - truncated SHA-256 of sorted cipher suites. Sorting removes ordering-based evasion.
  • JA4_c - truncated SHA-256 of sorted extensions + signature algorithms. Captures the full extension profile.

Because JA4 sorts cipher suites and extensions before hashing, an attacker cannot evade detection by simply reordering them. JA3 was vulnerable to this. Inner Warden computes both JA3 and JA4 for every TLS connection, giving you backward compatibility with existing threat intel feeds while gaining the robustness of JA4.

Pure Rust, no libpcap

Most TLS fingerprinting tools depend on libpcap for packet capture (Zeek, Suricata, ja3-rs). Inner Warden uses AF_PACKET raw sockets on Linux to capture packets directly from the network interface. No C library dependency. No FFI. No pcap file format.

Packet capture flow
AF_PACKET socket (SOCK_RAW)
  → Ethernet frame
    → IP header parse (v4/v6)
      → TCP header parse
        → TLS record layer (content_type == 22)
          → Handshake (msg_type == 1 = ClientHello)
            → Extract: version, ciphers, extensions, curves, point_formats
              → Filter GREASE values
                → Compute JA3 string → MD5 → lookup
                → Compute JA4 sections → SHA-256 truncate → lookup

The JA3 spec requires MD5 for the final hash. We implement MD5 in pure Rust rather than pulling in an external crypto crate for a single hash function. The implementation is 128 lines, well-tested, and avoids adding a dependency that would pull in dozens of transitive crates for one function call.

GREASE value filtering

RFC 8701 introduced GREASE (Generate Random Extensions And Sustain Extensibility): random values injected into ClientHello fields to prevent server ossification. Chrome and Firefox add random GREASE values to cipher suites and extensions on every connection. If you do not filter them, the same browser produces a different JA3 hash every time.

GREASE values (must be stripped)
0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a,
0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a,
0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa

Pattern: (value & 0x0f0f) == 0x0a0a

Inner Warden filters GREASE values before computing both JA3 and JA4 hashes. The check is a single bitmask operation per value, adding negligible overhead.

Known malicious fingerprints

Inner Warden ships with a curated list of known malicious JA3 hashes. When a TLS connection matches one of these fingerprints, it is flagged as a C2 callback and an incident is created with the malware family name:

Cobalt Strike Beacon72a589da586844d7f0818ce684948eea
Metasploit Meterpreter5d65ea3ab1d764ee22f9d68b2b5efea8
Emotet4d7a28d6f2263ed61de88ca66eb011e3
Trickbot6734f37431670b3ab4292b8f60f29984
IcedID / BokBotc12f54a3f91dc7bafd92b1b4e5b1d41c
Qakbot / Qbot2c535ae0ecbe460f0b94e3b7f1d60579
AsyncRATe35d3904ce04caf3c038c24a8983e13e
SUNBURST / SolarWindsb37e25c3d9b5c40e1cfe2e0a4e7a4395

This list is updated with each release. Hashes are sourced from abuse.ch, Trellix, and community submissions.

Comparison with Zeek and Suricata

Zeek and Suricata both support JA3 fingerprinting, but with different trade-offs:

FeatureInner WardenZeekSuricata
JA3YesYesYes
JA4YesPluginNo
libpcap dependencyNoYesYes
Memory footprint~8 MB~200 MB~150 MB
Auto-responseBlock IP, alert, correlateLog onlyAlert + drop
Firmware correlationYes (cross-layer)NoNo

Inner Warden's advantage is integration. A JA3 match does not just create a log entry. It feeds into the correlation engine, checks the behavioral DNA database, triggers IP blocking, and sends operator alerts. The TLS fingerprint becomes one signal in a larger detection chain.

Adding custom JA3 hashes

If you have threat intelligence with JA3 hashes specific to your environment (internal red team tools, known APT fingerprints), add them to your configuration:

config.toml
[sensor.tls_fingerprint]
enabled = true

# Custom malicious JA3 hashes
[[sensor.tls_fingerprint.malicious_ja3]]
hash = "a0e9f5d64349fb13191bc781f81f42e1"
name = "InternalRedTeam-Implant"

[[sensor.tls_fingerprint.malicious_ja3]]
hash = "b4f0e9c245ab37df1098cc76ef4b2a18"
name = "APT-CustomLoader"

Custom hashes are loaded at startup and checked alongside the built-in list. Matches produce the same incident pipeline as built-in detections: correlation, alerting, and optional auto-blocking.

What to do next

  • eBPF kernel security - how Inner Warden's 30 eBPF programs monitor kernel events in real time alongside TLS fingerprinting.
  • Cross-layer correlation - how a TLS fingerprint match combines with kernel events and honeypot data to build a complete attack picture.
  • Behavioral DNA - fingerprinting attackers by behavior instead of IP address, correlating campaigns across sessions.