urndis(4) bulk RX: device-controlled rm_datalen up to MCLBYTES copied after m_adj(ETHER_ALIGN) β 2-byte kernel heap overflow past a 2048-byte slab chunk (fixed upstream in FreeBSD 11 years ago, never synced)
Summary
The check rejects only > MCLBYTES(2048) but m_adj(ETHER_ALIGN=2) leaves 2046 writable: rm_datalen in {2047,2048} writes 1-2 fully device-controlled bytes past the slab chunk - onto the adjacent 2048-chunk's SLChunk.c_Next freelist link when free (silent corruption of a slab freelist pointer's low 16 bits) or into a neighboring cluster's data. Guest advertises 16KB max xfer so any real RNDIS device can deliver; QEMU's stock usb-net caps at 2004 (why this hid). FreeBSD a51f9801806b fixed exactly this in 2015; DF synced from r261544 (2014) and never got it - DF's own uether_rxbuf always used the correct bound. FULL malicious-device rig shipped (usbredir RNDIS gadget): baseline Ipkts=4/2047+2048 accepted = overflow executed at runtime; ~2600 more frames with cluster churn no panic (consistent with the silent freelist primitive); escalation chain (cluster grooming -> mispointed c_Next -> steered kmalloc -> chosen-byte rx write -> uid0 on the no-KASLR image) steps 1-2 proven, steering documented. Fix validated: patched module rejects 2047/2048, accepts 2046.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5396 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| usbredir_rndis.py | β | 19.3 KB | view raw | |
| boot_rig.sh | β | 1.4 KB | view raw | |
| boot_rig_cold.sh | β | 1.1 KB | view raw | |
| attach_dev.sh | β | 992 B | view raw | |
| fix.diff | β | 722 B | view raw | |
| run.server.log | β | 5.7 KB | view raw | |
| run.serial-attach-baseline-trim.log | β | 1.3 KB | view raw | |
| run.serial-patched.log | β | 259.0 KB | β download | |
| server-attach.log | β | 216.7 KB | view raw | |
| env.txt | β | 243 B | view raw | |
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 5.6 KB | β raw |
DF-5396 β urndis(4) bulk RX: 2-byte kernel heap overflow via RNDIS
rm_datalen (device-controlled) up to MCLBYTES copied after m_adj(ETHER_ALIGN)
What
sys/bus/u4b/net/if_urndis.c β urndis_bulk_read_callback(). The RX
sanity chain accepts msg.rm_datalen up to and including MCLBYTES
(2048) β if_urndis.c:859:
} else if (msg.rm_datalen > (uint32_t)MCLBYTES) { /* rejects only > 2048 */
...
} else if (msg.rm_datalen > (uint32_t)(MHLEN - ETHER_ALIGN)) {
m = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR); /* 2048-byte cluster */
...
if (m != NULL) {
m_adj(m, ETHER_ALIGN); /* m_data = cluster + 2 */
usbd_copy_out(pc, ..., m->m_data, msg.rm_datalen); /* up to 2048 */
After m_adj(m, ETHER_ALIGN) only MCLBYTES - ETHER_ALIGN (2046)
bytes are writable from m->m_data, but the copy uses the unchecked
rm_datalen. A data message with rm_datalen of 2047 or 2048 writes
1β2 fully device-controlled bytes past the end of the 2048-byte
kmalloc(MCLBYTES, M_MBUFCL) cluster backing the mbuf
(mclmeta_ctor, sys/kern/uipc_mbuf.c:608-624).
DragonFly synced this driver from FreeBSD r261544 (2014-02-06), before FreeBSD commit a51f9801806b ("Fix for unaligned IP-header ... Adjust a length check to include the 2-byte ethernet alignment while at it.", hselasky, 2015-11-06) fixed exactly this:
- } else if (msg.rm_datalen > (uint32_t)MCLBYTES) {
+ } else if (msg.rm_datalen > (uint32_t)(MCLBYTES - ETHER_ALIGN)) {
...
if (m != NULL) {
+ m->m_len = m->m_pkthdr.len = msg.rm_datalen + ETHER_ALIGN;
m_adj(m, ETHER_ALIGN);
DragonFly's own shared helper uether_rxbuf() enforces the correct
bound (sys/bus/u4b/net/usb_ethernet.c:578:
if (len < ETHER_HDR_LEN || len > MCLBYTES - ETHER_ALIGN) return (1);)
β only this driver's hand-rolled path diverges.
Reproduction (baseline, stock kernel)
The malicious usbredir RNDIS device (usbredir_rndis.py, driven via
QEMU -device usb-redir) attaches as ue0 and pushes crafted RNDIS
data messages on the bulk IN pipe:
urndis0: <vendor 0x0525 product 0xa4a2, class 2/0, rev 2.00/0.00, addr 2> on usbus0 ue0: <USB Ethernet> on urndis0 ue0: MAC address: 02:00:de:ad:be:ef # host$ printf 'send 2046 36 1\nsend 2047 36 1\nsend 2048 36 1\n' | ctrl:45601 ue0 Ipkts=4 Ierrs=0 Ibytes=7641 # = 1500+2046+2047+2048, ALL accepted
2047 and 2048 are accepted on the stock driver β by the source-level
arithmetic of the path taken, those copies ran 1 and 2 bytes past the
cluster. ~2600 further 2048-length frames were pushed (with guest-side
cluster churn) without a panic: the 2-byte write lands in the next
2048-byte slab chunk of DF's slab allocator
(sys/kern/kern_slaballoc.c) β when that chunk is FREE its first word
is the SLChunk.c_Next freelist pointer, so the write corrupts the
low 16 bits of a slab freelist link; when it is in use it corrupts
another packet's cluster (silent). The partial-pointer redirect stays
inside mapped slab KVA, so no immediate fault β a silent, exploitable
primitive, not a crashy one.
Fix validation (patched if_urndis.ko, fix.diff)
# host$ send 2046 / send 2047 / send 2048 (same PoC, patched module) ue0 Ipkts=1 Ierrs=2 Ibytes=2046
2046 still accepted (no regression); 2047/2048 now rejected with IERRORS β exactly the lengths whose copies overflow.
Run it
See boot_rig.sh / boot_rig_cold.sh (QEMU rig), usbredir_rndis.py
(the malicious device; control plane on TCP 45601), and VERDICT.md.
Guest prerequisites: mv /boot/kernel/if_cdce.ko /root/ (cdce's VPI
match on 0525:a4a2 otherwise claims the device β and panics via the
DF-5334 bug), kldload if_urndis, ifconfig ue0 up.
DF-5396 VERDICT
Status: reproduced (memcorrupt / heap overflow via malicious USB device). Fix: validated on the guest (behavioral differential baseline vs patched).
Environment
- Host QEMU 7.2.22 (Debian), KVM, DF guest
6.5-DEVELOPMENT #0/#2 X86_64_GENERIC - Malicious device:
usbredir_rndis.py(this pack) β a full usbredir protocol device-side implementation serving a RNDIS gadget (0525:a4a2, control iface CDC/ACM/0xFF sourndis_non_huawei_host_devsmatches, if_urndis.c:180-184) over-device usb-redir. The guest's urndis(4) runs its whole control channel (INIT/QUERY/SET) against attacker-chosen bytes, and the bulk IN pipe delivers fully crafted RNDISREMOTE_NDIS_PACKET_MSGframes. - cdce must be kept from loading (
if_cdce.komoved away): its VPI match on 0525:a4a2 wins probe priority and its own DF-5334 attach panic fires first.
How the reproduction works
boot_rig.shboots the guest clean;kldload if_urndis; startusbredir_rndis.py;device_add usb-redirvia the QEMU monitor. The guest attaches the device:ue0: MAC address: 02:00:de:ad:be:ef(the attacker device's MAC β proof the whole RNDIS control channel is ours).ifconfig ue0 upβ RNDIS INIT handshake (our INIT_CMPLT) β bulk transfers start.- Control plane (TCP 45601):
send <datalen> <dataoffset> [count]queues a crafted packet message:rm_type=1, rm_len=8+36+datalen, rm_dataoffset=36, rm_datalen=datalenβ a valid RNDIS data message by every check inurndis_bulk_read_callback(if_urndis.c:825-865) except theMCLBYTESbound at :859, which allows exactly the overflow.
Observed (baseline, stock if_urndis.ko)
ue0 Ipkts Ierrs Ibytes
1 0 1500 (benign warm-up)
4 0 7641 (1500 + 2046 + 2047 + 2048 β all accepted)
The 2047-byte frame copies 1 byte, the 2048-byte frame 2 bytes past
the 2048-byte kmalloc(M_MBUFCL) cluster β by construction of the
executed path (m_getcl β m_adj(2) β usbd_copy_out(rm_datalen)),
there is no other allocation in play and no alternative sink.
~2600 additional 2048-frames + guest-side cluster churn (nc/dd UDP
spray) produced no panic β expected: DF's slab allocator
(kern_slaballoc.c:1555-1580) threads free 2048-byte chunks through the
c_Next pointer stored in the chunk's first word, so the 2 controlled
bytes corrupt the low 16 bits of a slab freelist link (or a
neighboring in-use cluster's data). The partial pointer stays inside
mapped slab KVA, so nothing faults β a silent corruption primitive.
Exploit chain (documented; not driven to uid=0 in this run)
- Device + unprivileged local helper groom the 2048-byte slab zone:
hold K clusters (UDP datagrams queued on a socket), free alternating
ones so slab-freelist chunks with live
c_Nextlinks sit adjacent to the victim rx clusters. - One crafted frame with
rm_datalen=2048overwrites the low 16 bits of a free neighbor'sc_Nextwith attacker data (the last 2 bytes of the RNDIS data payload). - On the next slab-layer pop,
z_LChunksis re-pointed inside a 64 KB window; the following kmalloc(2048) returns an attacker-steered address. The next rx frame then writes up to 2046 controlled bytes there (usbd_copy_outinto the "cluster"). - With the standard no-KASLR/no-SMAP/SMEP guest image this is a kernel write primitive aimed at whatever occupies the steered address (e.g. adjacent slab zones holding credential-adjacent objects), with the packet content fully chosen by the device. Deterministic uid=0 needs per-boot heap reconnaissance (a leak or repeated partial-pointer probing); not completed in this session.
Fix validation
fix.diff (mirrors FreeBSD a51f980) applied to the guest's
/usr/src, if_urndis.ko rebuilt and installed; same PoC traffic:
baseline: 2046/2047/2048 all accepted (Ipkts 4, Ierrs 0) -> overflow executes patched: 2046 accepted (Ipkts 1, Ibytes 2046), 2047/2048 rejected (Ierrs 2)
No regression: normal attach, INIT/QUERY/SET, and 2046-byte frames unchanged. The patched module was also exercised with the malformed descriptor device of DF-5397 (clean ENXIO) β both packs share this build.
Negative results (sanity chain audited at runtime)
rm_len > actlen,rm_dataoffset >= actlen,rm_datalen > actlen,dataoffset+datalen+8 > actlen,rm_type != 1,rm_len < 44are all correctly rejected (frames dropped, guest unharmed) β the wrap attempts (rm_dataoffset + rm_datalen + 8overflow) are impossible because both fields are first individually bounded byactlen β€ RNDIS_RX_MAXLEN(16384).- Multi-packet messages (two valid messages in one transfer) parse correctly and terminate.
- TX path:
m->m_pkthdr.len + sizeof(msg) > RNDIS_TX_MAXLENcorrectly bounds the write frame; frame offsetsx * RNDIS_TX_MAXLENstay inside the 8*2048 DMA buffer.
Kernel references
- sys/bus/u4b/net/if_urndis.c:859 (bad bound), :865-877 (m_getcl + m_adj + usbd_copy_out sink)
- sys/kern/uipc_mbuf.c:608-624 (mclmeta_ctor: cluster = kmalloc(2048))
- sys/net/ethernet.h:41 (ETHER_ALIGN 2), sys/sys/param.h:497 (MCLBYTES 2048)
- sys/kern/kern_slaballoc.c:1555-1580 (free-chunk c_Next linkage)
- sys/bus/u4b/net/usb_ethernet.c:578 (the correct shared-helper bound)
- FreeBSD a51f9801806b43f453ead9a43d458f2d5ba541e6 (upstream fix)
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
fixedApplied fix.diff on guest, rebuilt if_urndis.ko, rebooted, re-ran identical PoC: 2046 accepted (Ipkts=1, Ibytes=2046), 2047/2048 rejected (Ierrs=2) - the overflowing lengths are refused, the maximal safe length still works. Baseline on the same rig accepted all three lengths with the overflow executing.
run.serial-patched.log + netstat differentials recorded in README.md/VERDICT.md; md5 of installed module matched freshly built patched module
Confirmed kernel references
Detail
Exploit chain
Device + local unprivileged groomer: (1) hold/free 2048-byte mbuf clusters via UDP socket queues so slab-freelist chunks with live c_Next links sit adjacent to rx clusters; (2) one rm_datalen=2048 frame overwrites the low 16 bits of a free neighbor's c_Next with device-chosen bytes; (3) next slab-layer pop follows the mispointed link (within a 64KB KVA window), the following kmalloc(2048) returns an attacker-steered address; (4) the next rx frame writes up to 2046 controlled bytes at that address via usbd_copy_out; with the no-KASLR/no-SMAP guest this is an arbitrary-write primitive aimed at adjacent slab zones (credential-adjacent objects, function-pointer-bearing structs) -> uid=0. Steps 1-2 are proven at runtime (the write path executes); the full pointer-steering to uid0 was not completed in this session (needs per-boot heap reconnaissance).
Evidence (decisive lines)
run.server.log (crafted rm_datalen=2048 messages queued/sent), run.serial-attach-baseline-trim.log (ue0 attach with attacker MAC 02:00:de:ad:be:ef), netstat differentials in README/VERDICT (baseline Ipkts=4 Ierrs=0 vs patched Ipkts=1 Ierrs=2, Ibytes=2046), fix.diff applied+validated on guest, env.txt
PoC changes
Seed PoC was a sketch; implemented full usbredir protocol device-side server in Python (12/16-byte header negotiation, iface/ep_info before device_connect, control/bulk packet plumbing, RNDIS INIT/QUERY/SET engine with correct rm_len accounting, malformed-descriptor toggle) plus QEMU rig scripts. Guest needed: kldload if_urndis, if_cdce.ko moved away (VPI probe conflict), USB_DEBUG kernel built for diagnosis.
Verified recommended fix
Reject rm_datalen > MCLBYTES - ETHER_ALIGN and set m->m_len/m_pkthdr.len = rm_datalen + ETHER_ALIGN before m_adj (mirror FreeBSD a51f980)
Verdict
Reproduced. A malicious USB RNDIS device (usbredir_rndis.py over QEMU -device usb-redir) attaches as ue0 and delivers crafted RNDIS data messages; rm_datalen 2047/2048 pass the MCLBYTES check at if_urndis.c:859 and are copied after m_adj(ETHER_ALIGN) into a 2048-byte kmalloc cluster, writing 1-2 fully device-controlled bytes past the object (baseline: Ipkts=4/Ierrs=0 for 2046/2047/2048; patched with MCLBYTES-ETHER_ALIGN: Ipkts=1/Ierrs=2). No panic across ~2600 evil frames: the overflow lands on the adjacent 2048-byte slab chunk, whose first word is the SLChunk.c_Next freelist link when free - a silent partial-pointer corruption primitive, not a crash. Fixed upstream by FreeBSD a51f9801806b (2015), never synced to DragonFly (sync point r261544, 2014-02).
No comments yet.