SIOCGATHDIAG/SIOCGATHPHYERR dispatched with no privilege check: unprivileged users get arbitrary device MMIO read/write (incl. plaintext WEP/TKIP keycache dump), EEPROM reads, and NULL-deref panics on ATH_DIAGAPI kernels
Summary
ath_ioctl gates SIOCZATHSTATS with caps_priv_check but dispatches SIOCGATHDIAG/SIOCGATHPHYERR straight through - no privilege check anywhere on the path (ifioctl default -> ieee80211 pass-through -> ath_ioctl). FreeBSD upstream gates the identical cases with PRIV_DRIVER; the DF port kept only the SIOCZATHSTATS check. Behind the missing gate: HAL_DIAG_SETREGS (arbitrary device MMIO writes), HAL_DIAG_REGS (register dump including the keytable at 0x9800+ = 802.11 key material disclosure), EEPROM reads, and NULL-deref panics. Gated by options ATH_DIAGAPI (not in stock GENERIC - verified by symbol absence on the guest). caps_priv_check_self-mirror fix (filed against ar5212_misc.c where the deferred diag surface lives).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4320 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | β | 4.4 KB | view raw | |
| build.sh | β | 80 B | view raw | |
| run.sh | β | 86 B | view raw | |
| build.log | β | 38 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| static_check.log | β | 1.8 KB | view raw | |
| env.txt | β | 1003 B | view raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| README.md | β | 2.5 KB | β raw | |
| fix.diff | β | 690 B | view raw |
DF-4320 β Missing privilege check on SIOCGATHDIAG exposes the full HAL diagnostic interface (arbitrary device MMIO read/write, 802.11 keycache disclosure, NULL-deref panics)
Build
cc -O2 -Wall -o poc poc.c
(Any DragonFly host; plain POSIX userland, no privileges needed.)
Run
./poc # probes every interface with SIOCGATHDIAG as the invoking user
Expected
- Vulnerable system (kernel built with
options ATH_DIAGAPI, an ath(4) wlan interface present, run as a plain user): printsSETREGS : SUCCESS(an arbitrary device register write was accepted from an unprivileged uid) and/orREGS-dump: SUCCESS(device register dump, including the WEP/TKIP keycache registers at 0x9800+, handed to an unprivileged uid). NoEPERMis ever produced β the DragonFly source has no privilege gate anywhere on the dispatch path (socket βifioctldefault βieee80211_ioctldefault βath_ioctlcase SIOCGATHDIAG βath_ioctl_diag). - Stock X86_64_GENERIC (no
ATH_DIAGAPI): every probe returns ENOTTY/EINVAL β the ioctl case is not compiled into the kernel (verified:nm /boot/kernel/if_ath.ko | grep ath_ioctl_diagβ empty). On the audit QEMU guest additionally no Atheros NIC exists (virtio-only PCI bus), so the wlan dispatch is never reached.
Static proof (decisive on any host)
sys/dev/netif/ath/ath/if_ath_ioctl.c:288β the neighboring ioctl SIOCZATHSTATS does checkcaps_priv_check_self(SYSCAP_NODRIVER).sys/dev/netif/ath/ath/if_ath_ioctl.c:299-302β SIOCGATHDIAG / SIOCGATHPHYERR callath_ioctl_diag/ath_ioctl_phyerrdirectly with no check. FreeBSD upstream gates the identical cases withpriv_check(curthread, PRIV_DRIVER).sys/netproto/802_11/wlan/ieee80211_ioctl.c:3516-3520β unknown ioctls are passed to the driver ("Pass unknown ioctls first to the driver") with no privilege check.sys/dev/netif/ath/ath_hal/ah.c:878-935β what the gate would be protecting:HAL_DIAG_SETREGS(arbitraryaddr,valueMMIO writes, unconditional),HAL_DIAG_REGS(arbitrary register-range dump β includes keycache),HAL_DIAG_EEREAD, plus (under AH_PRIVATE_DIAG) SETKEY/RESETKEY/EEWRITE.
Files
poc.cβ the probe (struct layouts mirrored 1:1 fromif_athioctl.h).run.logβ guest run (default kernel: ENOTTY/EINVAL as predicted).static_check.logβ guest kernel/module symbol + PCI evidence.env.txtβ guest environment.fix.diffβ the one-hunk fix.
DF-4320 β VERDICT
Status: inconclusive at kernel level (feature absent on the audit guest); the missing privilege gate itself is certain from source, fully traced.
What the finding claims
SIOCGATHDIAG (the ath(4) HAL diagnostic ioctl) is dispatched to
ath_ioctl_diag() without any privilege check on DragonFly:
sys/dev/netif/ath/ath/if_ath_ioctl.c:285-297 SIOCZATHSTATS *does* check caps_priv_check_self(SYSCAP_NODRIVER) :288 sys/dev/netif/ath/ath/if_ath_ioctl.c:299-300 case SIOCGATHDIAG: return (ath_ioctl_diag(sc, data)); <-- NO check sys/dev/netif/ath/ath/if_ath_ioctl.c:301-302 case SIOCGATHPHYERR: same <-- NO check
The full unprivileged dispatch chain (traced, no gate anywhere):
ioctl(2)on a datagram socket βsys/net/if.c:2409-2415default βso_pru_control_direct(unknown driver ioctl passed through);sys/netproto/802_11/wlan/ieee80211_ioctl.c:3516-3520default β "Pass unknown ioctls first to the driver" βic->ic_ioctl=ath_ioctl;ath_ioctlcase SIOCGATHDIAG βath_ioctl_diag(if_ath_ioctl.c:170-235) β copyin of attackerad_in_size/ad_out_sizebuffers, thenath_hal_getdiagstate(ah, id, indata, insize, &outdata, &outsize);sys/dev/netif/ath/ath_hal/ah.c:878-935β unconditional handlers: *HAL_DIAG_SETREGS(33):ath_hal_setregs()β arbitrary device MMIO writes with attacker-suppliedaddr,valuepairs, no validation at all (ah.c:888-891); *HAL_DIAG_REGS(13):ath_hal_getregdump()β arbitrary register range dump returned to userland; the keycache registers (AR_KEYTABLE_KEYn, 0x9800+,ar5212reg.h) hold installed WEP/TKIP key material in plaintext β 802.11 key disclosure to any local user; *HAL_DIAG_EEREAD(17): arbitrary EEPROM word read. Plus NULL-deref panics with the ATH_DIAG_IN/ATH_DIAG_DYN flag bits unset:getregdump(ah, NULL, NULL, attacker_outsize)/ath_hal_setregs(ah, NULL, argsize)dereference NULL (ah.c:848-856/865-870 withindata == NULL) β trivial unprivileged panic on a vulnerable config. The ar5212-specific handlers behindar5212GetDiagState(ar5212_misc.c:1049-1126) addHAL_DIAG_ANI_CMDβar5212AniControlwith uncheckedcmd, which NULL-derefsparams->maxNoiseImmunityLevelwhen no ANI state exists (ar5212_ani.c:230-243).
FreeBSD upstream gates the identical cases with
priv_check(curthread, PRIV_DRIVER); DragonFly's port kept the check for
SIOCZATHSTATS only (the caps_priv_check_self at :288) and dropped it
for the two DIAGAPI ioctls.
Why it is not kernel-reproducible on THIS guest (and what was verified instead)
- The guest kernel is
X86_64_GENERIC(DragonFly 6.5-DEVELOPMENT #0 ... /usr/obj/usr/src/sys/X86_64_GENERIC), which does not includeoptions ATH_DIAGAPI(sys/config/X86_64_GENERICβ no ATH_DIAGAPI; only LINT64 lists it at :1121). The whole#ifdef ATH_DIAGAPIblock β includingath_ioctl_diagβ is not compiled:nm /boot/kernel/if_ath.ko | grep ath_ioctl_diagβ no symbol (see static_check.log). - The guest has no Atheros NIC (virtio-only PCI bus,
pciconf -lin env.txt), so even a recompiled kernel could not be driven into the HAL path without hardware.
What WAS verified on the guest:
poc(unprivileged, uid=1001) issued realSIOCGATHDIAGioctls with aHAL_DIAG_SETREGSpayload and aHAL_DIAG_REGSkeycache-dump payload on every interface: non-wlan interfaces answer EINVAL (ether layer), i.e. no privilege gate was hit β and none exists in the source path (run.log).- Static evidence that the HAL side handlers are compiled unconditionally
into
ath_hal.ko(disassembly ofath_hal_getdiagstateat +0x67b0 shows comparisons against diag codes 13 (REGS), 17 (EEREAD), 29 (FATALERR), 30 (11NCOMPAT), 33 (SETREGS)) β the only missing piece on a stock system is the ioctl case in if_ath.ko, which anyoptions ATH_DIAGAPIkernel build turns on.
Exploit chain (on a kernel built with options ATH_DIAGAPI + ath NIC)
unprivileged local user
β socket(AF_INET,SOCK_DGRAM) + ioctl(SIOCGATHDIAG, {ad_id=HAL_DIAG_REGS|IN|DYN,
ad_in_data=[{0x9800,0x983f},...], ad_out_size=4096})
β copyout of keycache registers β recover WEP/TKIP group/pairwise keys
β (or) ad_id=HAL_DIAG_SETREGS, write arbitrary device registers
β (or) ad_id=HAL_DIAG_ANI_CMD, NULL-deref panic
β (or) HAL_DIAG_REGS with crafted sizes β DF-4321 heap overflow β code exec
Bottom line
Certain source-level vulnerability (CWE-862), unconditionally missing gate
whose sibling ioctl demonstrates the intended pattern in the same switch.
Exploitation requires a kernel built with options ATH_DIAGAPI (not in
default GENERIC) and an ath(4) device β therefore Medium severity per the
project rubric ("requires unusual config"), with High impact on such
configurations. Not reproducible on the audit guest (feature absent);
fix.diff adds the gate mirroring :288.
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.
Fix verification
not_testablefix.diff adds caps_priv_check_self(SYSCAP_NODRIVER) to the SIOCGATHDIAG/SIOCGATHPHYERR cases, mirroring SIOCZATHSTATS at :288; validated as git-apply-clean against sys/; runtime validation impossible on this guest (no ATH_DIAGAPI kernel, no ath NIC).
findings/poc/DF-4320/fix.diff; git apply --check passes
Confirmed kernel references
Detail
Exploit chain
unprivileged socket(AF_INET,SOCK_DGRAM) -> ioctl(SIOCGATHDIAG,{ad_id=HAL_DIAG_REGS|IN|DYN, ranges=[{0x9800,0x983f},...], ad_out_size=4096}) -> copyout of keycache registers -> WEP/TKIP key recovery; or ad_id=HAL_DIAG_SETREGS -> arbitrary device register write; or unset IN/DYN -> NULL deref panic (requires options ATH_DIAGAPI + ath NIC; not present on audit guest)
Evidence (decisive lines)
VERDICT.md (full path trace); static_check.log (nm: no ath_ioctl_diag in guest if_ath.ko; ath_hal_getdiagstate disasm showing unconditional REGS=13/SETREGS=33 handlers in ath_hal.ko); run.log (unprivileged probe, no EPERM possible); fix.diff (priv gate, git-apply clean)
PoC changes
struct ath_diag mirrored 1:1 from if_athioctl.h:178-192 (ad_id u16, ad_in_size u16 before pointers); probe issues HAL_DIAG_SETREGS and HAL_DIAG_REGS payloads on every interface
Verified recommended fix
Gate SIOCGATHDIAG/SIOCGATHPHYERR with caps_priv_check_self(SYSCAP_NODRIVER) as FreeBSD does with PRIV_DRIVER
Verdict
The missing privilege gate is certain from fully-traced source: SIOCGATHDIAG/SIOCGATHPHYERR dispatch to ath_ioctl_diag()/ath_ioctl_phyerr() (if_ath_ioctl.c:299-302) with no caps/priv check anywhere on the path (ifioctl default net/if.c:2409 -> ieee80211_ioctl default wlan/ieee80211_ioctl.c:3516 -> ath_ioctl), while the neighboring SIOCZATHSTATS case checks caps_priv_check_self(SYSCAP_NODRIVER) at :288 and FreeBSD upstream gates the same DIAGAPI ioctls with PRIV_DRIVER. On an ATH_DIAGAPI kernel with an ath NIC this hands any local user HAL_DIAG_SETREGS (arbitrary device MMIO write), HAL_DIAG_REGS (arbitrary register dump incl. plaintext WEP/TKIP keycache at 0x9800+), HAL_DIAG_EEREAD, plus NULL-deref panics via unset ATH_DIAG_IN/DYN flags. Kernel-level reproduction was impossible on the audit guest: stock X86_64_GENERIC has no options ATH_DIAGAPI (nm shows no ath_ioctl_diag in if_ath.ko) and the guest has no Atheros NIC (virtio-only PCI). The unprivileged probe ran on the guest (run.log): non-wlan interfaces answer EINVAL; no privilege gate exists in the source path. Impact recorded as 'none' strictly for this guest; on ATH_DIAGAPI configs the ceiling is 802.11 key disclosure + device takeover (and DF-4321 heap overflow).
No comments yet.