Skip to content

JA3 and JA4 in threat hunting: fingerprinting malware C2 traffic

· 22 min read
Copyright: MIT
JA3 to JA4 wordmark with an orange accent on a dark background

A piece of malware on a compromised host has to call home. It opens a TLS connection to its operator’s server, and from that moment everything inside the tunnel is encrypted. The defender watching the wire sees a destination IP, a port, a certificate, and a stream of ciphertext. The operator knows this, so the IP is fresh, the domain was registered last week, and the certificate is a throwaway from Let’s Encrypt. Every indicator of compromise you would normally pivot on is disposable. What is not disposable, and what the operator usually forgets about, is the shape of the handshake itself.

That shape is the question this post is about. The bytes a client puts in its TLS ClientHello, before any encryption begins, are a fingerprint of the software stack that sent them. A Cobalt Strike beacon negotiates TLS the way the Windows networking library and the Cobalt Strike profile tell it to, and that negotiation looks different from Chrome, different from curl, and crucially the same across every beacon built from the same profile. Hash those bytes and you have a signal that survives IP rotation, domain churn, and certificate swaps. That is the promise of JA3, and the more durable promise of JA4. The catch is that the same hashing trick that lets a defender cluster malware also lets a browser vendor accidentally break it, and an evader deliberately defeat it.

This is a defensive reference. The sections below trace how JA3 and JA3S compute their hashes, why combining client and server fingerprints catches C2 that single indicators miss, how JARM turned passive listening into active scanning of the whole internet, what Cobalt Strike’s defaults leak, the moment in early 2023 when Chrome’s ClientHello randomization quietly killed JA3 for browser traffic, how JA4 was rebuilt to survive that, and where the limits sit when the adversary controls their own TLS stack. The thread running through all of it is a single tension: a fingerprint is only useful while the thing being fingerprinted does not know it is being measured.

What JA3 actually hashes

JA3 was published as open source in 2017 by John Althouse, Jeff Atkinson and Josh Atkins at Salesforce. The construction is deliberately simple. It reads five fields out of the TLS ClientHello, in this fixed order: the TLS version, the list of accepted cipher suites, the list of extensions, the list of supported elliptic curves, and the elliptic curve point formats. Each field’s values are taken as their decimal numbers, the values within a field are joined with hyphens, the fields are joined with commas, and the whole string is run through MD5. The output is a 32-character hex digest.

The pre-hash string for a connection looks like this:

769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0

That 769 is decimal for TLS 1.0 in the version field; the long middle run is cipher suites; 0-10-11 is the extension list; 23-24-25 are the curves; the trailing 0 is the point format. MD5 that string and you get ada70206e40642a3e4461f35503241d5. The hash is the part you share, store, and match against. MD5’s collision weakness does not matter here because nobody is attacking the hash; it is just a fixed-width label for a variable-length string.

The reason this works as a signal is that a TLS client does not choose these fields at random. They come from the TLS library the application is linked against, the version of that library, and any preferences the application sets on top. A program built on Windows SChannel offers a different cipher list in a different order than one built on OpenSSL, which differs again from Go’s crypto/tls, which differs from NSS in Firefox. Two unrelated programs that happen to share a TLS stack and use it the same way will share a JA3. That is both the strength and the original sin of the method, and we will come back to it.

JA3: five ClientHello fields, joined and hashed Version 769 Ciphers 47-53-5-10-... Extensions 0-10-11 Curves 23-24-25 PtFmt 0 769,47-53-5-10-...,0-10-11,23-24-25,0 MD5 ada70206e40642a3e4461f35503241d5 order matters: the extension list is hashed as sent, not sorted *The JA3 pipeline. The decimal values of five ClientHello fields are concatenated in fixed order and MD5 hashed. The extension list is taken in the order the client sent it, which is the detail that randomization later exploited.*

JA3S, and why the pair beats either half

JA3 fingerprints the client. JA3S does the same for the server’s response, reading the TLS version, the single cipher the server chose, and the server’s extension list out of the ServerHello, then concatenating and MD5-hashing the same way. A JA3S pre-hash string is shorter because the server commits to one cipher rather than offering a menu:

769,47,65281-0-11-35-5-16

On its own, JA3S is weak. Servers respond based on what the client offered, so a given server produces different JA3S values for different clients, and many benign servers share a JA3S. The power is in the pairing. A specific piece of malware talks to its C2 server using a fixed client stack, and that C2 server, running a specific build of the operator’s tooling on a specific OS, answers in a fixed way. The two halves together form a tuple that is far more specific than either alone. Salesforce’s own framing was blunt about the payoff: combine JA3 and JA3S and “we are then able to identify this malicious communication regardless of destination IP, Domain, or Certificate Details.”

That sentence is the whole reason the technique matters for threat hunting. An operator can burn an IP and stand up a new one in minutes. Rotating the TLS stack on both ends of the conversation is a real engineering change, not a config edit, and most operators never bother. So the defender who has captured the JA3/JA3S pair for a malware family from a sandbox detonation can hunt for that pair across months of network logs and catch the same family on infrastructure that did not exist when the signature was built. The fingerprint outlives the indicators.

This is the mechanism behind abuse.ch’s SSLBL, which has run a JA3 blocklist since 2019. The fingerprints there were extracted from over 25 million PCAPs generated by detonating malware samples, and the project ships a Suricata ruleset so a sensor can alert on a matching ClientHello in real time. The published fingerprints carry an honest warning that they “have not been tested against known good traffic yet and may cause a significant amount of FPs.” That caveat is the recurring problem with single-fingerprint detection, and it sets up everything that follows about specificity and collisions.

JARM: turning the fingerprint inside out

JA3S is passive. You learn a server’s fingerprint only when something connects to it and you happen to be watching that exact handshake. In late 2020 Salesforce released JARM, again with John Althouse as lead author, which flips the model. JARM is active. Instead of waiting to observe a server, it reaches out and probes one.

JARM sends ten specially crafted TLS ClientHello packets to the target. Each of the ten varies the TLS versions, the cipher suites, and the extension orderings on purpose, including deliberately malformed and unusual combinations no normal client would send. The server’s ten responses, the version and cipher it picks each time plus how it handles the odd inputs, are captured and compressed into a 62-character fingerprint. The first 30 characters encode the cipher and TLS version the server chose for each of the ten probes, with 000 marking a refusal to respond, and the last 32 are a truncated SHA-256 of the cumulative server extensions.

Because the probes are aggressive and varied, JARM extracts far more about a server’s TLS stack than a single passive observation. And because it is active, you can scan the entire IPv4 internet on port 443 and fingerprint every TLS server that answers, then filter for the ones whose JARM matches a known-bad tool. Salesforce’s launch claim was that a single internet-wide scan “found most, if not all, Cobalt Strike C2’s listening on the Internet on port 443.” For Trickbot infrastructure they reported that 80 percent of the live C2 IPs they checked produced the same JARM.

JA3S (passive) JARM (active) client server you only watch one handshake JA3S = f(1 ServerHello) scanner server 10 crafted probes scan all of IPv4, match known-bad JARM JARM = f(10 ServerHellos) *JA3S learns a server's fingerprint only from handshakes you happen to observe. JARM sends ten deliberately varied probes and can be run against the whole internet, which is how researchers enumerated Cobalt Strike C2 servers in bulk.*

The trade-off is that JARM is noisy and detectable. You are sending unusual TLS traffic to a server the operator controls, so a careful adversary can log the probes and recognise a scan in progress. It also fingerprints the TLS stack, not the specific tool, so anything sharing that stack collides. JARM tells you a server answers like Cobalt Strike’s default; it does not by itself prove the server is malicious, because the default Cobalt Strike team server runs on a Java TLS stack that other Java services share.

What Cobalt Strike leaks by default

Cobalt Strike is the worked example for all of this because it is both the most widely abused commercial C2 framework and the most thoroughly documented by defenders. Out of the box it leaks a set of stable fingerprints that have become standard hunting signatures.

The JARM value for a default Cobalt Strike team server is 07d14d16d21d21d00042d41d00041de5fb3038104f457d92ba02e9311512c2. That exact string changed on 20 April 2021, when the underlying Java runtime disabled TLS 1.0 and 1.1 in favour of 1.2 and above; servers patched before that date carried a different JARM ending in ...a458a375eef0c576d23a7bab9a9fb1. The shift is a useful lesson on its own. A JARM signature is not a property of Cobalt Strike, it is a property of the Java TLS stack Cobalt Strike happens to run on, so a routine JRE update moved the fingerprint for every default server in the world overnight without anyone touching the malware.

There is also the default certificate. Cobalt Strike’s bundled self-signed HTTPS certificate ships with the serial number 146473198 and a SHA-256 of 87F2085C32B6A2CC709B365F55873E207A9CAA10BFFECF2FD16D3CF9D94D390C. Hunting for that serial or that hash across certificate-transparency feeds and internet scan data has been one of the most productive ways to find live C2, and researchers have repeatedly found hundreds of servers exposing it at any given moment. Tellingly, the set of servers found by the default certificate and the set found by the default JARM overlap by less than half. Different operators forget to change different things, so the most complete picture comes from running several independent fingerprints and taking the union.

On the beacon side, known default JA3 client values such as 72a589da586844d7f0818ce684948eea and matching JA3S server values have circulated in defender reports for years. The reason they exist is that a default beacon uses the Windows socket library to set up TLS in a fixed way, and the default team server answers in a fixed way, so the JA3/JA3S pair is stable across deployments that never customised it. None of these signatures are a law of nature. Every one of them can be changed by an operator who reads the same defender blogs, which is exactly what the better-resourced ones do.

Default Cobalt Strike: independent fingerprints beacon JA3 72a589da...948eea server JARM 07d14d16...1512c2 default cert serial 146473198 union catches more than any single signature cert-found and JARM-found server sets overlap less than 50% each one is a config the operator can change independently *Three orthogonal default signatures. Because operators customise different things, no single fingerprint finds every server; hunting runs them in parallel and unions the results.*

If you are tracing how C2 stays reachable once the fingerprints are known, the infrastructure side is its own subject. See domain generation algorithms for how beacons find a server without a hardcoded domain, and fast-flux networks for how the IP behind a name rotates faster than a blocklist can keep up.

The day Chrome broke JA3

JA3’s quiet assumption is that the order of fields in a ClientHello is stable for a given client. The cipher list, the extension list, the curve list, all of it gets hashed in the order the client transmitted, so two connections from the same browser version produce the same hash only if the browser sends the same order every time. For years it did. Then Google changed its mind.

Starting around 20 January 2023, Chrome began permuting the order of the extensions in its TLS ClientHello. The change was associated with Chrome 110 but appeared to take effect in builds 108 and 109 as well, and it shuffles the extension list per connection. The motivation was ossification, the same reason GREASE exists. When a popular client always sends extensions in one order, middleboxes and servers start hard-coding that order and break whenever it changes, which makes the order impossible to ever change. Randomizing it on every handshake forces everything downstream to parse extensions in any order, keeping the protocol flexible. Firefox and the Chromium-derived browsers followed, so within a release cycle the dominant browsers on the internet were all shuffling.

For JA3 this was fatal in the most arithmetic way possible. A ClientHello with 16 extensions has 16 factorial possible orderings, which is 20,922,789,888,000. A single Chrome version now produces some appreciable fraction of those 21 trillion JA3 hashes, a new one on practically every connection. A blocklist of bad JA3 hashes still works, because malware that does not randomize keeps its stable hash. But using JA3 to positively identify Chrome, or to allowlist “known good” browser traffic, stopped working overnight. The signal for the most common client on the planet dissolved into noise.

This is the cleanest example of the central tension. JA3 was never an adversarial target when Google made this change; the goal was protocol hygiene, not evasion. But the side effect handed every evader a free pass, because anything that mimics Chrome’s randomized ClientHello inherits the same un-fingerprintable property. The same mechanism that protects a real browser’s flexibility protects a malicious client hiding behind a real browser’s TLS profile. A defender cannot ask browsers to stop randomizing without re-ossifying the protocol. The fingerprint had to be rebuilt instead.

JA4: sorting away the randomization

JA4 is that rebuild. John Althouse, by then at his own company FoxIO, published it on 26 September 2023 as the TLS client piece of a larger suite called JA4+. The core insight is almost embarrassingly direct: if the client randomizes the order, do not hash the order. Sort first.

A JA4 fingerprint has the form a_b_c, three sections joined by underscores, and that structure is itself part of the design. The a section is human-readable metadata: the protocol (t for TCP, q for QUIC), the TLS version (13 for 1.3), whether SNI is present (d for a domain destination, i for a bare IP), a two-digit count of cipher suites, a two-digit count of extensions, and the first ALPN value. GREASE values are excluded from the counts. So t13d1516h2 reads as TCP, TLS 1.3, SNI present, 15 ciphers, 16 extensions, ALPN of HTTP/2.

The b section is a 12-character truncated SHA-256 of the cipher list sorted into hex order. The c section is a 12-character truncated SHA-256 of the extension list, also sorted into hex order, followed by the signature algorithms in their original unsorted order. Two extensions get deliberately removed before the c hash: SNI (0000) and ALPN (0010). Pulling those out keeps the fingerprint stable across different destinations and different negotiated protocols, since their presence is already recorded in the readable a section. A complete example from the spec is t13d1516h2_8daaf6152771_e5627efa2ab1.

Sorting is the whole trick. Chrome can permute its extensions all it likes; once JA4 sorts them by hex value before hashing, the permutation collapses back to a single canonical order and the fingerprint is stable again. The randomization that produced 21 trillion JA3 hashes produces exactly one JA4 c value. JA4 reads the same fields JA3 did, plus ALPN and signature algorithms that JA3 ignored, but it never trusts the transmitted order.

JA4 = a_b_c, with sorting in b and c t13d1516h2 8daaf6152771 e5627efa2ab1 a: readable metadata proto, TLS ver, SNI, counts, ALPN b: sorted cipher hash c: sorted extension hash + sig algorithms why randomization fails against JA4 sent: 0a,2b,00,10,05 ... sent: 05,10,2b,0a,00 ... (permuted per connection) sort 0005,000a,0010,002b... one canonical order SNI (0000) and ALPN (0010) are removed before the c hash for stability *JA4's a_b_c layout. The readable a section supports partial matching, and the sorted b and c hashes flatten Chrome's per-connection permutation back to one value, which is the property JA3 lost.*

The readable a section enables something JA3 never could: partial matching. Because the fingerprint is delimited, a hunter can match on just a plus c while ignoring b, or on a alone, depending on what they are chasing. That makes JA4 a query surface, not just a single equality check. A threat actor’s tooling might keep a constant c while the cipher selection drifts, and you can still cluster it.

JA4+ is the wider suite, and the cross-link to the JA3-to-JA4 pillar covers the full lineage. Alongside JA4 for the TLS client there is JA4S for the server response, JA4H for the HTTP request, JA4X for how an X.509 certificate was generated, JA4L for latency and physical distance, JA4SSH for SSH session patterns, and JA4T for the TCP layer. Licensing splits the suite: JA4 itself is BSD 3-Clause and free to use anywhere, while JA4S, JA4H, JA4X, JA4L and JA4SSH sit under the FoxIO License 1.1, which is permissive for internal and research use but requires a separate license to build into a product you sell. For a defender running these in their own SOC, the FoxIO terms are not an obstacle.

How the blue-team use differs from anti-bot use

The same TLS fingerprints show up in two adjacent worlds, and it is worth being precise about how the goals diverge, because the technique is identical but the economics are not. Anti-bot vendors fingerprint the TLS handshake to tell a real browser from an HTTP client pretending to be one. A scraper using Python’s requests library has a wildly different JA4 from Chrome, and that mismatch, especially when the User-Agent claims Chrome but the ClientHello says OpenSSL, is a strong bot signal. The treatment is covered in how Cloudflare uses TLS and HTTP/2 fingerprints in bot scoring and why your Python requests is fingerprintable. There the defender’s job is to keep a high-volume, low-stakes verdict cheap and fast across billions of requests, and a false positive is a blocked shopper, an annoyance.

Threat hunting inverts the stakes. The volume is lower and the verdict is higher-consequence. A JA3 hit on a malware blocklist is not the end of an investigation, it is the start of one, a pivot point that tells an analyst which flows to pull apart. A false positive here wastes an analyst’s afternoon rather than a customer’s checkout, and a false negative means a beacon phoned home undetected. The fingerprint is one signal in a correlation, weighted alongside beacon timing, JA3S or JARM on the server, certificate anomalies, and destination reputation, rather than a gate that allows or denies a single request in milliseconds.

The adversary models differ too. An anti-bot vendor faces a scraper that wants to look like a browser and will reach for a TLS-impersonation library to do it. A threat hunter faces malware that, in the default case, did not try to hide its handshake at all, and in the sophisticated case is actively shaping it to blend into normal enterprise traffic. The first adversary is impersonating a browser; the second is often impersonating nothing, just using whatever TLS stack the OS handed it, which is precisely why default fingerprints are so productive. The serious operator closes that gap deliberately, and that is where the limits start.

Where the fingerprint runs out

The honest position is that TLS fingerprinting is a tax on lazy tradecraft, not a wall. Everything above works best against operators who used defaults. The moment an adversary controls their own TLS stack, the signal degrades, and there are several distinct ways they make it degrade.

The simplest is to mimic a real browser’s handshake exactly. Libraries that let a non-browser client send a byte-identical Chrome or Firefox ClientHello have existed for years; the uTLS approach and the broader curl-impersonate and uTLS detection problem are the canonical references. A beacon built on a stack like this produces the JA3 or JA4 of a current Chrome build, which means it lands in the most crowded, most-allowlisted bucket on the network. You cannot block that fingerprint without blocking every real Chrome user behind it.

The second is to inherit Chrome’s randomization as camouflage. Anything that genuinely shuffles its extension order the way Chrome does is un-pin-able by JA3 for the same reason real Chrome is, and a sorted JA4 will land it in the Chrome bucket rather than flagging it. The protocol-hygiene change that broke JA3 for defenders also handed a hiding place to anyone willing to imitate it.

The third is JARM evasion specifically. Because JARM probes are predictable, a server can be configured to answer them in a way that produces a benign or randomized JARM, and public JARM-randomizer tooling exists to do exactly that. A server that detects the ten-probe pattern can simply present a different TLS profile to scanners than to its real beacons.

There is also a deeper erosion coming from the protocol itself. Encrypted Client Hello, covered in Encrypted Client Hello (ECH), encrypts the inner ClientHello including the SNI, so a passive observer loses the destination name and some of the fields JA-style fingerprints lean on. ECH is not yet universal, and JA4 was designed to read what remains visible in the outer handshake, but the direction is clear: over time, more of the handshake moves behind encryption, and the visible surface a fingerprint can hash gets smaller. None of this makes the technique useless. It makes it one layer in a stack that also watches beacon periodicity, JA4H on the HTTP request, server-side JA4S and JARM, and the certificate, so that defeating any single fingerprint does not defeat the detection. A determined adversary can beat any one of these. Beating all of them at once, on every connection, for the lifetime of an operation, is the expensive part, and most never pay for it.

What endures

The arc from JA3 to JA4 is a small, clean case study in how a defensive signal ages. JA3 worked because TLS clients are creatures of habit and the handshake is sent in the clear before encryption starts. It stopped working as an allowlist the day the most common client on earth started shuffling its bytes for reasons that had nothing to do with malware. JA4 fixed the specific break by refusing to trust the order, and in doing so it bought back the signal for browser traffic while keeping it for the malware that never randomized in the first place. The fix was not cleverer cryptography. It was sorting a list before hashing it.

The part that endures is the asymmetry the whole approach exploits. Standing up new infrastructure is cheap, and rotating an IP or a domain or a certificate is something an operator does without thinking. Rebuilding the TLS stack on both ends of the C2 channel, on every connection, so that the handshake never collides with a known signature and never stands out as too unusual, is a real and continuing engineering cost. Most malware operators do not pay it, which is why a JA3 lifted from a sandbox in 2019 still catches the same family on infrastructure that did not exist when the hash was computed. The fingerprint is durable in exact proportion to how little the adversary cares about it, and the operators who learn to care are also the ones loud enough to leave other tells. That is not a permanent advantage. It is a recurring one, and on the current evidence the cycle favours the side doing the hashing.


Sources & further reading

Further reading