-
Notifications
You must be signed in to change notification settings - Fork 1
fragattacks
Mathy Vanhoef, Fragment and Forge: Breaking Wi-Fi Through Frame Aggregation and Fragmentation, USENIX Security 2021. Paper · Project site · Advisory index
Twelve CVEs. Three are design flaws in 802.11 itself and affect every device shipped since 1997; the other nine are implementation bugs. They share one outcome: an adversary in radio range can get frames of their choosing accepted into an encrypted WPA/WPA2/WPA3 session without holding the key.
Sloth implements eight of them, across seven detectors (-26140 and
-26143 share FRAG_PLAINTEXT — the fragmented and unfragmented
variants of the same accepting-plaintext bug). This page explains
which, why the remaining four are harder or impossible to observe
passively, and — the part worth
reading before you trust the alert — what each detector's gate
actually proves.
| CVE | what the receiver does wrong | passively observable? |
|---|---|---|
| 2020-24586 | does not clear the fragment cache on (re)connect | yes — shipped, slice 2 |
| 2020-24587 | reassembles fragments encrypted under different keys | yes — shipped, slice 4 |
| 2020-24588 | accepts non-SPP A-MSDU frames | yes, sideways — shipped. Not as usually described; see below |
| 2020-26139 | AP forwards an EAPOL frame from a station that has not completed authentication to another client | yes — shipped. Detected by its addressing footprint, not by authentication state; see below |
| 2020-26140 | accepts plaintext data frames in a protected network | yes — shipped |
| 2020-26141 | does not verify the TKIP MIC of fragmented frames | no (MIC is under the key) |
| 2020-26142 | processes fragmented frames as full frames | no (a receiver-side decision) |
| 2020-26143 | accepts fragmented plaintext data frames | yes — shipped |
| 2020-26144 | accepts plaintext A-MSDU starting with an EAPOL RFC1042 header | yes, plaintext only — shipped, slice 4 |
| 2020-26145 | accepts plaintext broadcast fragments as full frames | yes — shipped |
| 2020-26146 | reassembles encrypted fragments with non-consecutive PNs | yes — shipped |
| 2020-26147 | reassembles mixed encrypted/plaintext fragments | yes — shipped, slice 2 |
Several of these are only ever visible as the attacker's frames on the air. Nothing sloth can see tells it whether the victim's driver actually accepted them — that is a decision inside another machine. The alerts below therefore say "this was transmitted", not "this worked".
A data frame with the Protected bit clear, carrying something other than EAPOL, on a session where the sender's key install has already been witnessed.
A fragmented group-addressed data frame with Protected clear. In an RSN, broadcast frames are never fragmented — reassembling one is the bug — so the fragment is already a violation before anything reassembles it. Sloth fires on the fragment rather than waiting for a completion that only the victim can perform.
The fragment cache is supposed to be cleared on every (re)association.
Sloth tracks reassembly sessions keyed on (BSSID, SA, DA, TID): a
fragment with the More Fragments bit set and fragment number 0 opens a
session, and any later fragment for the same key is its continuation.
If a continuation completes a session that started before a (re)association response sloth witnessed for either endpoint, the completion straddles a boundary at which the buffer should have been empty. There is no benign reading of that — it is the CVE's own failure mode, not a threshold being crossed.
This one is deliberately not gated on the RSN-witnessed state
FRAG_PLAINTEXT and FRAG_BCAST use. The fragment cache exists before
decryption — an implementation's failure to clear it does not care
whether the fragments in it are plaintext or ciphertext — so the
detector fires on either.
Association timing uses a strict "after": a session that opens the same second an association lands is not reported, because sloth's one-second clock resolution cannot show which came first.
The same session table catches a second, unrelated bug: a reassembly whose fragments do not agree on the Protected bit. An all-encrypted sequence is normal. An all-plaintext one is normal on an open network. A sequence that starts encrypted and completes plaintext, or the reverse, is neither — each fragment is individually unremarkable, and the violation only exists once they are combined into one MSDU.
Two encrypted fragments of one reassembly whose CCMP packet numbers are not consecutive.
§12.5.3.4.4 requires the fragments of one MSDU to carry consecutive PNs; the vulnerability is receivers that fail to check. So a gap means the receiver is stitching together fragments from different bursts, which is the attack.
This page previously argued the detector was unshippable. Two objections were raised and both turned out to be wrong, which is worth recording because they are the objections anyone would raise again:
"The PN is one counter shared by every frame sent under one key on one TID, so two fragments only get consecutive PNs if nothing else was transmitted between them."
Transmission is serialised per TID, and a conforming transmitter does not interleave other MPDUs into a fragment burst — which is precisely why the receiver-side check the CVE is about is possible at all. The session is keyed on the TID, and the PN comparison is additionally scoped to one sequence number, so the first fragment of the next MSDU is never measured against the last fragment of the previous one.
"A retry gets a fresh PN even though its fragment number is unchanged."
It does not. A retransmitted MPDU is retransmitted verbatim, with the same PN — the frame is already encrypted under a nonce derived from that PN, and re-encrypting under a new one would produce different ciphertext and defeat the receiver's own replay handling. A retry therefore carries the same PN and the same fragment number.
What makes it shippable is comparing deltas, not expecting +1. The
rule is that the PN advances by the same step as the fragment number:
| situation | ΔPN | ΔFN | verdict |
|---|---|---|---|
| ordinary consecutive fragments | 1 | 1 | quiet |
| a fragment sloth did not hear | 2 | 2 | quiet |
| a retransmitted fragment | 0 | 0 | not compared |
| fragments from different bursts | ≠ ΔFN | fires |
The missed-fragment row is the one that decides whether this is usable
on a hopping radio, and a bare +1 check fails it. The retry row falls
out of the same arithmetic rather than needing a special case, which is
why there is not one.
Both fragments must carry a readable PN: an unprotected fragment has
none, and a WEP or original-TKIP IV is not a 48-bit PN at all. A
Protected mismatch between fragments is FRAG_MIXED's finding, not this
one.
An EAPOL frame the AP forwarded between two stations.
This one was also previously deferred, on the grounds that "names another station as its destination" needed the paper's frame trace to pin down where in the frame that destination lives. That framing made the problem harder than it is by looking for the attack's mechanism instead of its footprint.
On an infrastructure BSS, EAPOL travels only between a station and the authenticator. It is not a peer-to-peer protocol — there is no legitimate EAPOL exchange between two clients. So the footprint needs no handshake state and no authentication tracking at all:
A data frame carrying EtherType 0x888E where neither the source nor
the destination is the BSSID.
If neither address is the AP, the AP relayed the frame on behalf of a sender it should not have, which is the bug. That is structural, and it is the reason this rule is the odd one out in the family: every other plaintext detector here needs a witnessed key install, because "we did not see the association" is indistinguishable from "there was none". This frame is wrong on its own addressing, so it fires on the first frame of a capture.
Two exclusions, each carrying its weight:
- ToDS or FromDS must be set. An IBSS has no AP: addr3 is the BSSID and both stations are peers, so every EAPOL frame in an ad-hoc network would match and mean nothing.
- Unicast only. A group address can never equal the BSSID, so including broadcast would fire on a shape the CVE does not describe.
What this does not claim is that the sender was unauthenticated — sloth cannot know that, and the earlier note was right that inferring it from a hopping radio's association history would be a guess. It claims something narrower and stronger: this frame took a path EAPOL has no business taking.
The obvious implementation is "if the beacon for this BSSID advertises
RSN, any unprotected data frame is a finding." Sloth does not do that.
The gate is per (BSSID, station), and it is ordered: sloth must
have seen that station transmit a Protected frame before the
plaintext one.
Three reasons, and the third is the real one.
It survives a hopping radio. --hop means most BSSes are heard for
a fraction of a second at a time and the beacon may never land in the
window. A detector that needs the beacon first is silent exactly when
the radio is doing its job.
It is per-station, so one client cannot indict another. Association and the 4-way handshake happen in the clear. On a busy BSS there is almost always some station mid-association, and a per-BSS flag would turn its perfectly normal traffic into a CRIT against the whole network.
It is what the CVE says. These are "accepting plaintext after key install" bugs. The beacon tells you what the AP offers; it says nothing about whether this station completed a handshake. A station's own encrypted traffic is direct evidence that it did. Using the weaker signal would produce an alert that is right about the network and wrong about the frame.
The ordering requirement follows from the same argument and is tested directly: plaintext seen before the key install is a client that had not finished associating yet, and counting it retroactively would make every normal association look like an attack.
| exempt | because |
|---|---|
| EAPOL (EtherType 0x888E) | rekeying is legitimately unprotected. Without this, every rekey on the network is a CRIT |
| Null / QoS-Null subtypes | no frame body at all; they are how a station signals power-save state, and they are always unprotected. Without this, every idle client fires |
| continuation fragments (FN > 0) | carry no LLC header, so an EAPOL continuation cannot be told from a data one. Counting them anyway is a guess dressed as a detection |
| non-SNAP LLC | an EtherType read out of bare LLC is invented, and an invented one that is not 0x888E makes every IPX frame a CRIT |
| four-address (WDS) frames | cross two BSSes and carry no single BSSID. Attributing them to a guess is worse than not seeing them |
The unicast rule needs the EtherType and therefore only counts frames that say what they carry. The broadcast rule does not: fragmentation itself is the violation and broadcast EAPOL does not exist, so it needs no exemption at all.
The most cited of the family, and the detector usually proposed for it cannot work passively.
The attack flips the A-MSDU Present bit on an encrypted MPDU so the
receiver reparses the payload as aggregated subframes with
attacker-chosen destinations. The natural signals — the first subframe's
DA against Address 3, the subframe length against the MPDU, the
subframe's LLC prefix — are all inside the ciphertext. The QoS
Control field carrying the bit is in the plaintext MAC header, so the
bit is visible; the body is not, and sloth does not crack
(MISSION.md §2).
What is left is the bit alone, which fires on any hardware that aggregates — most of it.
So sloth detects the replay instead. The attacker does not forge a frame; they capture one the victim already sent and retransmit it with one bit changed. That makes the observable a duplicate MPDU whose A-MSDU bit differs.
The #75 triage proposed keying this on the sequence number. The CCMP packet number is strictly better and it is what shipped.
| sequence number | CCMP PN | |
|---|---|---|
| width | 12 bits | 48 bits |
| wraps | every 4096 frames — under a second at any real rate | never under one key |
| reuse | routine | a protocol violation (§12.5.3.4.4) |
A seqnum-keyed detector needs a comparison window short enough to be evaded and long enough to false-positive. The PN has no such tension: a repeat is already an anomaly before the flipped bit is considered, so the rule is "same transmitter, same PN, different A-MSDU bit" with nothing else propping it up.
It also has no meaning on unprotected frames, which is exactly right — -24588 is an attack on encrypted MPDUs, and a plaintext A-MSDU is CVE-2020-26144, a different row in the table above.
- A plain retransmission carries the same PN and the same bit. Excluded by construction, not by a threshold — which matters, because retries are constant on a congested link.
- Two radios using the same PN is normal: they have different keys. The key includes the transmitter.
- A rekey legitimately restarts PNs from zero, so the comparison window is 10 seconds. The attacker replays promptly — the victim's replay window and fragment cache are what the attack rides, and both are short-lived.
This is the only FragAttacks rule here with no key-install gate. It does not need one: the PN is itself proof the frame is protected.
It is transmitted in the clear immediately after the MAC header, and the byte order traps people:
PN0 PN1 rsvd KeyID PN2 PN3 PN4 PN5
Low octet first, then the high four after the KeyID octet. Reading
those eight bytes as a big-endian integer produces a plausible number
that is wrong, and nothing downstream would notice. Bit 5 of the KeyID
octet is the Extended IV bit; without it the frame is WEP or original
TKIP, which have no 48-bit PN at all, so the same eight bytes mean
something else entirely and dot11_ccmp_pn() returns -1 rather than
inventing evidence.
FRAG_AMSDU above detects the encrypted half of the aggregation
design flaw by replay, because the subframe headers a direct check
would need are inside the ciphertext. CVE-2020-26144 is the same flaw's
other half, and it needs no such workaround: the frame is plaintext,
so the A-MSDU subframe header — DA(6), SA(6), Length(2), then an
ordinary LLC/SNAP — sits in the clear right where any other subframe's
would.
The check is one fact: real EAPOL is never aggregated. It is always its
own MPDU, never a subframe of one. So a plaintext A-MSDU whose first
subframe's LLC/SNAP claims EtherType 0x888E (EAPOL) is not "unusual
aggregation" — it is a receiver being handed a subframe dressed as an
authenticator message, which is the confusion this CVE describes. There
is no threshold and no rate to tune: the claim itself is the finding.
Same key-install gate as FRAG_PLAINTEXT. This is plaintext
traffic being judged, so the same false-positive story applies for the
same reason: without evidence this station's key was already installed,
an unprotected A-MSDU is an open network or a station mid-association,
not a finding. The gate is per (BSSID, station) and ordered, exactly
as above.
Only the first subframe is read. A-MSDU subframes are each preceded by a Length field, but trusting that field to walk to the second subframe means trusting the exact value this attack manipulates elsewhere in the family — reading past the first would be leaning on the evidence to validate itself. The first subframe is also the one a spoofed EAPOL claim would occupy, so nothing downstream of it matters for this check.
What is not reused from FRAG_PLAINTEXT's EtherType reader. That
function (first_frag_ethertype()) explicitly declines any A-MSDU
frame — an aggregated body has no single LLC header, only a run of
subframes, so treating it as one would misread the first subframe's
length or DA/SA as if they were payload. This detector's reader
(first_amsdu_subframe_ethertype()) is the A-MSDU-aware sibling that
function's own comment points to.
This was the one item slice 1's triage flagged as needing state that did
not exist: eapol_log.c's M1–M4 parser tracked a handshake but had no
way to tell a rekey apart from the initial one, so a fragment
session had nothing to compare a later fragment of the same reassembly
against. It now does.
A per-(BSSID, STA) generation counter, bumped when an M3 carries
an ANonce that differs from the one last installed. M3 is the
message that matters — it is the AP telling the station Install=1, the
moment the key changes — not M4, which only confirms the station
complied and may never be seen on a hopping radio.
The ANonce comparison, not a message count, is what makes this usable. An AP resends M3 verbatim — same ANonce — when M4 is lost, which happens constantly on a lossy or hopping capture, and every one of those retries would otherwise look like a fresh rekey. A genuine rekey (periodic PTK refresh, or a fresh association) draws a new ANonce every time, which is exactly the property that tells the two apart.
A fragment session records that counter when it opens (fragment 0). A continuation fragment that completes the reassembly after the current generation has moved past the recorded one was assembled from fragments encrypted under two different keys — the mixed-key bug.
Two gates, both load-bearing:
-
Both fragments must be encrypted. A session that starts encrypted
and completes plaintext (or the reverse) is
FRAG_MIXED's finding, not this one — counting it here too would send the operator to two CVEs for one event. - The recorded generation must be nonzero. Zero means eapol_log.c never witnessed an install for this pair at all — not "generation zero". If the first M3 sloth has ever seen for a pair lands while a session opened with no prior evidence is still in flight, the current generation (1) exceeds the recorded one (0), but that is sloth acquiring its first evidence of any key, not a rekey spanning the session. Reporting a rekey without evidence a key existed at all would be a guess dressed as a detection, the same rule every plaintext detector in this file follows for its own gate.
[c] FragAttacks (slice 5, src/views/fragattack.c) is the operator
surface for the nine counters above: one row per BSSID, sorted by
most-recent finding, with a per-CVE breakdown for the selected row.
It reads no packets and adds no SQLite table — src/alert_pcap.c
already carries the triggering frames for each fired alert, which is a
better fixture seed than a truncated evidence blob in a database row
would have been. See docs/views/fragattack.md.
[f] went to #73's Research view first, so [c] is the key — the
last free letter in the global switch at the time slice 5 landed.
Every detector in this family needs to know which address field is the BSSID, and it moves with the DS bits (§9.3.2.1, Table 9-26):
| ToDS | FromDS | addr1 | addr2 | addr3 | addr4 | |
|---|---|---|---|---|---|---|
| 0 | 0 | DA | SA | BSSID | — | IBSS |
| 0 | 1 | DA | BSSID | SA | — | AP → STA |
| 1 | 0 | BSSID | SA | DA | — | STA → AP |
| 1 | 1 | RA | TA | DA | SA | WDS / mesh |
Reading addr3 as the BSSID unconditionally is the shape that looks right
because it holds for a beacon. On a downlink frame it attributes the
traffic to the sending station instead of its AP, which silently
merges every station on the network into one bogus BSS. dot11_data_addrs()
in src/dot11_data.c owns this table and is tested against all four rows.
Hand-built uint8_t frames per IEEE 802.11-2020 §9.2, no captures — see
agents/AGENTS.md § Discipline. The cases that decide whether this is
usable in the field are the negative ones: an open network, a client
mid-association, a rekey, an idle client sending Null frames, a QoS
frame whose LLC sits two bytes further along. A capture from a lab rig
running fragattacks.py would exercise none of them.
That gap is real and worth stating: these tests prove the detector
against the specification, not against what the tool actually puts on
the air. The needs-pcap-fixture label on #75 means exactly that —
wanted as follow-up validation, not blocking.
- captive-portal — the other detector whose false-positive story is the whole design
-
docs/views/alerts.md— the rule table rows - Issue #75 for the slicing and the premises that needed correcting
Mirrored from docs/wiki/ on main by .github/scripts/wiki_sync.sh. Edit there, not here — hand edits to this wiki are overwritten on the next push.
Read this first — the complete reference
- what-sloth-does
- how-wifi-works
- monitor-mode
- where-exploits-happen
- wifi-sigint-techniques
- cli-reference
- wifi-state-of-the-art
Start here
Engines
WiFi SIGINT
- wifi-sigint
- non-ip-sensors
- mac-randomisation
- evil-twin-reproducer
- btm-abuse
- action-frames
- research-corpus
- captive-portal
- fragattacks
- tool-fingerprints
- enterprise-rogue
- ipv6-ndp
- smb-snoop
- kerberos-snoop
- ldap-snoop
- bgp-snoop
- ssh-snoop
- rdp-snoop
- snmp-snoop
- mqtt-snoop
UI and infrastructure
- ip-palette
- platform-vtable
- version-checkin
- manifest-format
- pcap-export
- jsonl-schema
- data-socket-exposure
- sqlite-schema
- ring-buffers
Factory infrastructure
Reference
Source material
Maintenance