# DF-3323 — VERDICT

**Status: reproduced** (baseline, stock INVARIANTS kernel #0 Thu Jul 2 06:02:54
UTC 2026) · impact: remote RF-adjacent authentication bypass (no key material
required) → authorized + associated station; cleartext data delivery when
DROPUNENC is off · fix: validated (kernel rebuilt with `fix.diff`).

## Root cause (path:line)

- `sys/netproto/802_11/wlan/ieee80211_hostap.c:851-880` — `hostap_input()`
  MGT path: decryption happens **iff** `wh->i_fc[1] & IEEE80211_FC1_PROTECTED`;
  after `ieee80211_crypto_decap()` the bit is cleared (`:879`), so downstream
  code cannot tell an encrypted frame from a cleartext one.
- `sys/netproto/802_11/wlan/ieee80211_hostap.c:1122-1150` —
  `hostap_auth_shared()` `case IEEE80211_AUTH_SHARED_RESPONSE`: only
  `memcmp(ni->ni_challenge, &challenge[2], challenge[1])` (`:1138`); on match
  `ieee80211_node_authorize(ni)` (`:1149`). No Protected-bit requirement
  anywhere on the seq-3 path.
- `sys/netproto/802_11/wlan/ieee80211_output.c:2380-2417` — `add_auth()`:
  the AP's seq-2 CHALLENGE frame carries the 128-byte challenge IE and is
  **not** marked for encryption (only `arg == IEEE80211_AUTH_SHARED_RESPONSE`
  sets `IEEE80211_BPF_CRYPTO`, and the AP never sends seq 3). Frame 2 is
  legitimately clearttext (802.11-2016 12.3.3), frame 3 must be protected —
  the kernel never checks.

## Attack chain (executed on the guest via the fw4 software radio)

1. `sysctl kern.fw4_attack=1` — inject AUTH algo=SHARED seq=1 (30 bytes,
   legitimately cleartext). AP (wlanmode ap, `authmode shared wepkey …
   wepmode on`) creates the node, allocates + randomizes the challenge,
   transmits seq-2 with the 128-byte challenge IE **protected=0**; the
   harness's `raw_xmit` sniffer captures it byte-for-byte ("off the air").
2. Control `fw4_attack=3` — respond seq-3 with one corrupted challenge byte:
   AP answers seq-4 **status=15** (IEEE80211_STATUS_CHALLENGE), node stays
   AUTHORIZED=0. (Proves the exchange is otherwise enforced.)
3. Attack `fw4_attack=2` — respond seq-3 with the captured challenge,
   Protected bit CLEAR: AP answers **seq-4 status=0** and the node reports
   `AUTHORIZED=1 <<<< BYPASSED`.
4. `fw4_attack=4` — ASSOC_REQ: `associd 0xc001`, `list sta` FLAG `A`
   (authorized+assoc) — under `wepmode on`, the strictest WEP config.
5. Part B (`wepmode mixed`, DROPUNENC off — also the default state of
   `authmode shared wepkey …` without explicit `wepmode on`): steps 1-4
   repeat, then ONE cleartext TODS data frame is injected
   (`fw4_attack=5`): `netstat -I wlan0` Ipkts 1→2, Ibytes 20→42 — the
   frame traversed `hostap_input`'s privacy path (F_PRIVACY set, Protected
   bit clear → `key = NULL` at :711), was decapped, and delivered to
   `if_input`.

No WEP key was ever known or used by the attacker.

## Why not higher severity

No memory corruption. Requires an RF-adjacent attacker and a WEP
shared-key-configured AP (legacy, non-default config). Under the
recommended `wepmode on` (DROPUNENC set) cleartext *data* is still dropped
(non-PAE), but 802.11 authentication, association, AID/power-save state and
the EAPOL (PAE) pass-through are gained without the key.

## Fix validation (kernel #1, both fixes applied)

`fix.diff` (in `hostap_input`, before the Protected-bit is stripped):
discard `subtype==AUTH` frames that arrive unprotected and parse as
`algo == IEEE80211_AUTH_ALG_SHARED && seq == IEEE80211_AUTH_SHARED_RESPONSE`
(802.11-2016 12.3.3 requires frame 3 to be protected). Requires
`m_pkthdr.len >= 30 && m_len >= 30` so it never reads a fragmented header
(if shorter, `hostap_recv_mgmt`'s own VERIFY_LENGTH rejects the frame
anyway).

- baseline: attack=2 → AUTHORIZED=1 + AP TX seq4 status=0
- patched : attack=2 → frame dropped (`is_rx_bad_auth++`), NO AP TX, node
  AUTHORIZED=0; attack=1 still elicits the seq-2 challenge (legit
  cleartext frames 1/2 unaffected).
- regression: open-authmode vap, open auth (attack=6) → authorized →
  assoc succeeds (see DF-3326 fixcheck in findings/poc/DF-3326/fix.log).

`fix_baseline_reproduced=1`, `fix_patched_reproduced=0` (the bug behavior
is GONE on the patched kernel) → fix_status=fixed.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
