P packet out-of-bounds stack write via NUM_REGS/struct mismatch and missing lower bound
Summary
P (set one register) packet validates register index only with regno<NUM_REGS where NUM_REGS hardcoded 22 but struct x86_64regs only 152 bytes = 19 eight-byte slots. Indices 19 20 21 write 8 bytes each past end of registers stack local. hexToInt() produces signed int with no sign rejection so parsed value 0xffffffff yields regno=-1 which passes regno<22 and writes 8 bytes at ®isters-8. P handler hex2mem(ptr (vm_offset_t)®isters+regno*8 8). Result kernel-stack OOB write of attacker-controlled bytes at attacker-controlled offsets. Corrupts adjacent locals saved rbp return address. Attacker: GDB debug console holder (serial sio 0x80 flag dcons/FireWire) while kernel in GDB stub. [VERIFIED pass2: reproduced against the REAL stock stub in the shipped kernel over a serial debug console - g reply proves sizeof registers 152 vs NUM_REGS 22, P13-P15 land exact attacker bytes at +152 +160 +168, Pffffffff passes as regno -1 writing at registers-8, Pffffffe0 reaches registers-0x100, saved-register slot write at +152 kills the VM on resume twice; RIP control itself out of reach on this gcc -O2 layout (retaddr at +208) and kernel image 127TiB above kstack; console-holder reach only, no unpriv network path; bounds fix-validated]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2361 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc2361.py | β | 8.8 KB | view raw | |
| poc2361rip.py | β | 3.0 KB | view raw | |
| rig236.py | β | 5.1 KB | view raw | |
| rsp.py | β | 6.1 KB | view raw | |
| rig-attach.diff | β | 972 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| run.log | β | 34.8 KB | view raw | |
| run.raw.log | β | 124.1 KB | view raw | |
| run.fixed.log | β | 34.9 KB | view raw | |
| rip.log | β | 18.6 KB | view raw | |
| rip.raw.log | β | 17.9 KB | view raw | |
| env.txt | β | 409 B | view raw | |
| probe.py | β | 1.4 KB | view raw | |
| kmemv.c | β | 1.3 KB | view raw | |
| exp_p_oob.py | β | 6.8 KB | view raw |
Fix verification
fixedfix.diff (regno >= 0 && regno < sizeof(registers)/8) applied in-guest, kernel rebuilt and installed: P13/P14/P15, Pffffffff and Pffffffe0 all rejected with P01 and no memory change (run.fixed.log), in-bounds P12 still returns OK with exact bytes, protocol healthy, clean detach. Baseline and fixed runs on otherwise identical kernels.
run.fixed.log [S2]-[S5] blocks; fix.diff in this pack applies clean to the read-only sys/ tree (git apply --check)
Confirmed kernel references
Detail
Exploit chain
console access -> sysctl debug.enter_debugger=gdb (or ddb gdb / panic with RB_GDB) -> stub T05 handshake -> P packet with regno 19/20/21 or negative regno -> 8 attacker-chosen bytes at ®isters+152..168 or ®isters+negative_offset (up to 16 GiB below the kernel stack, 8-byte aligned) -> saved-register corruption -> D -> kernel does not resume, VM dies (controlled-RIP not achievable on this build: return address at +208 is out of positive-regno reach)
Evidence (decisive lines)
run.log lines [S0]-[S6] (g=304 hex chars proving sizeof 152; P12 in-bounds control; P13/P14/P15 exact OOB bytes at +152/+160/+168 with struct untouched; Pffffffff OK proving regno=-1 accepted; Pffffffe0 exact marker at A-0x100); rip.log + rip.raw.log (P13=4141414141414141 + D -> VM death, frame autopsy showing saved regs and retaddr at +208); run.fixed.log (all OOB P packets rejected with P01, in-bounds still OK after fix.diff)
PoC changes
Rewrote the seed exp_p_oob.py into a single-session rig (rig236.py + poc2361.py): fixed the T05 field endianness (mem2hex emits little-endian register bytes; int(hex,16) mis-decoded rsp and read unmapped memory), added the P25-rejected control (hex 19 = 25 dec, proving the upper bound works), re-based the in-bounds control to slot 18 dec (P12; the seed used hex 18 = 24 dec which is rejected), documented A-8/A-64/A-128 as per-packet transient spill slots (the marker writes land but the handler re-spills ptr/locals there every packet), and added poc2361rip.py for the destructive saved-register corruption. rig-attach.diff (tty_cons.c only) attaches gdb_tab because hint.sio.0.flags=0x90 demonstrably does not on this ACPI guest.
Verified recommended fix
P handler: require regno >= 0 && regno < (int)(sizeof(registers)/8) (19 slots), replacing the unbounded regno < NUM_REGS(22) check
Verdict
Reproduced on the live stock kernel: the P handler guard regno < NUM_REGS(22) vs the 152-byte (19-slot) struct x86_64regs plus the missing regno >= 0 lower bound give an attacker on the kernel debug console an arbitrary-offset fully controlled 8-byte kernel-stack write: regno 19/20/21 write at ®isters+152/+160/+168 (0-24 bytes past the object, struct untouched, exact attacker bytes verified), hexToInt of ffffffff yields -1 which passes regno < 22 (stub replies OK; write issued at ®isters-8), and regno -32 lands an exact marker 256 bytes below the object. Destructive proof: 0x4141414141414141 into the saved-register slot at +152 then detach (D) - the kernel never resumes and the VM dies (twice reproduced); the identical session without the corruption resumes cleanly. On this gcc -O2 build the return address sits at ®isters+208, outside regno 19-21 reach, and the kernel image lies ~127 TiB above the kstack region, so the negative direction covers the 16 GiB of kernel-map memory below the stack rather than the image. Reach requires the debug console (sio flags 0x80/0x90, dcons, or DDB access): operator/physical, no unprivileged or network path, hence Medium/dos with a genuine memory-corruption primitive inside that boundary.
No comments yet.