DF-1326 / verdict.json
{ "finding_id": "DF-1326", "status": "reproduced", "reproduced": 1, "impact": "uid0_privesc", "confidence": "certain", "verdict": "FULL unprivileged-to-root escalation through the REAL production mpr ioctl code of the shipped kernel. Pass 3 replaced the pass-2 verbatim-replica with a dispatch rig that resolves the actual compiled-in static handler mpr_ioctl_devsw (0xffffffff80598ab0 in kernel #0, prologue byte-verified per dispatch) from the running kernel and forwards ioctls to it against a fabricated softc, over a /dev node with the exact make_dev() ownership of /dev/mprN (0640 root:operator, mpr_user.c:205). The unprivileged trigger (uid=2/gid=5 operator, O_RDONLY open) issued MPTIOCTL_PASS_THRU whose RequestSize drove the unchecked copyin at mpr_user.c:800 into the 12-byte stack tmphdr; disassembly-verified offsets (tmphdr at rbp-0x3c, epilogue pops through the smashed bytes, saved RIP at payload+0x44) give exact control of the return address. 6/6 runs (incl. one from a fresh vm.sh reset with-src snapshot, kernel #0 md5 4625046e) landed a kernel shellcode stub from a user RWX page (no SMEP; CPL0-exec-of-user-page proven live via ud2 trap at 0x10000000) that walked gs:0 -> gd_curthread -> td_proc -> p_ucred and zeroed the uid/gid fields, then swapgs+iretq to a userspace continuation: getuid()=0, root shell (uid=0(root) gid=0(wheel)), guest healthy throughout. Sizes both the post-hoc check rejects (132>128) and accepts (96<=128, evaded mode) escalate identically; controlled non-canonical RIP (trap 9 AT mpr_user_pass_thru+0x6fe: ret) and the 4096 spray (trap 9 with frame pointer 0x4546474841424344) provide the deterministic corruption proofs. Root staged ONLY the driver-instance stand-in because QEMU cannot emulate the LSI SAS2008 PCI HBA mpr_attach() needs -- on hardware with an mpr(4) controller the entire chain is attacker-only. fix.diff validated end-to-end on the real rebuilt kernel (single-fix nativekernel #1, md5 76ac5c67): all four modes now return cleanly (132/4096 -> EINVAL before any copyin; 96 -> ENOMEM after a 12-byte copyin), zero traps, guest healthy, patched binary shows the range check preceding the copyin site.", "exploit_chain": "operator-group member (uid=2) -> open /dev/mprN O_RDONLY (0640 root:operator; group bit r--, ioctls not gated on open mode) -> ioctl(MPTIOCTL_PASS_THRU=0xc0384904, DataSize=0/DataDirection=NONE passes the :776-788 gate) -> copyin(PtrRequest, &12-byte tmphdr, attacker u32 RequestSize) at :800 BEFORE the reqframesz check at :804 -> RequestSize>=76 overwrites saved rbx/r12-r15/rbp/RIP (RIP at payload+0x44; epilogue pops purely from attacker bytes; cm slot at rbp-0x48 survives NULL so the error path returns cleanly to the smash) -> saved RIP = user RWX page (no SMEP; verified by CPL0 ud2 trap at 0x10000000) -> kernel shellcode: mov rax,[gs:0]; curthread=[rax+8]; proc=[rdi+168]; ucred=[rdi+16]; zero cr_uid/cr_ruid/cr_svuid/cr_groups[0]/cr_rgid/cr_svgid -> swapgs; push ss/rsp%16==8/rflags=0x202/cs/rip; iretq -> userspace continuation with getuid()=0 -> exec /bin/sh: uid=0(root) gid=0(wheel). Rig proved every link with the real compiled handler; only the /dev node + softc stand in for the absent SAS2008 HBA.", "evidence": "run.log (canonical fresh-snapshot probe: DF1326_CONTINUATION getuid=0, uid=0(root) gid=0(wheel), guest up 34s; then the full baseline-vs-patched mode matrix), rig_stress_evaded.log (5x probe + evaded all uid=0), rig_rip_control_trap.txt + rig_smash_panic.txt (Fatal trap 9 AT mpr_user_pass_thru+0x6fe: ret; frame pointer 0x4546474841424344), rig_stage0_userpage_exec.txt (CPL0 executed ud2 at userspace 0x10000000 = no SMEP), df1326rig.c + df1326rig_trigger.c (rig + trigger + shellcode emitter), build.log (-fno-stack-protector build; fix apply; nativekernel+install completions), rig_fixed_runs.log + rig_fixed_disasm_order.txt (patched kernel: EINVAL/ENOMEM clean returns, cmp-before-copyin ordering), VERDICT.md pass-3 addendum", "kernel_refs": [ "sys/dev/raid/mpr/mpr_user.c:742", "sys/dev/raid/mpr/mpr_user.c:800", "sys/dev/raid/mpr/mpr_user.c:804", "sys/dev/raid/mpr/mpr_user.c:828", "sys/dev/raid/mpr/mpr_user.c:886", "sys/dev/raid/mpr/mpr_user.c:205", "sys/dev/raid/mpr/mpr_user.c:1145", "sys/dev/raid/mpr/mpr_ioctl.h:193", "sys/dev/raid/mpr/mpi/mpi2.h:855" ], "poc_changes": "Pass-3 rig replaces the pass-2 verbatim replica: df1326rig.c KLD dispatches the REAL static mpr_ioctl_devsw resolved via nm from the running kernel (KASLR-free), fabricates only the softc fields the compiled code touches (offsets from disassembly) and the 0640 root:operator node. df1326rig_trigger.c builds the payload with the disassembly-proven RIP slot 0x44 and emits the kernel shellcode at runtime (offsets read from rig sysctls computed against the guests real headers; cs/ss/rsp read live). Two bugs found and fixed during bring-up: emit_mov_rax_imm64 originally emitted the push-rax byte INSIDE the imm64 (stream shift -> triple fault), and the continuation initially landed rsp%16==0 instead of the ABIs 8 (SIGBUS in printf SSE). fresh_run.sh is the canonical fresh-snapshot sequence.", "attempts": 9, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 7800, "guest_dirty": 0, "build_cmd": "cd /root/rig && make SYSDIR=/usr/src/sys MACHINE=x86_64 MACHINE_ARCH=x86_64 && cc -O -o trigger df1326rig_trigger.c && kldload ./df1326rig.ko && sysctl -w kern.df1326rig_target=0x$(nm /boot/kernel/kernel | awk / t mpr_ioctl_devsw$/{print $1})", "run_cmd": "su operator -c /root/rig/trigger probe (also: evaded | rip | smash | dump)", "code_hash": "0ace7706dd948dd5aaebeaa319501455c8d9f7ae428b89c9f258535a0b24cc1a", "notes": "status upgraded from pass-2 inconclusive to reproduced because the escalation now runs entirely through the REAL compiled production handler (only root-staged stand-in: the driver instance, since QEMU has no LSI SAS2008 device; /dev/mpr* absent, mpr is compiled into the kernel and attaches the moment such hardware exists). The kernel has no stack canary (-fno-stack-protector, zero __stack_chk symbols), no KASLR/SMEP/SMAP (qemu64 CPU); meltdown_mitigation=1 does not block the chain (kernel-mode view maps user pages; syscall entry re-establishes CR3/GS absolutely, so the rogue swapgs+iretq exit is self-healing on the next entry). Sizes that PASS the post-hoc check (76..128) smash exactly like oversized ones; the ioctl never returns in probe/evaded modes on the vulnerable kernel. Old pass-1/pass-2 artifacts retained in this pack; guest returned to the clean with-src snapshot after verification.", "recommended_fix": "Bounds-check RequestSize (>= sizeof(tmphdr), <= reqframesz) BEFORE the copyin and copy only sizeof(tmphdr) for the routing peek; re-read the full request from userspace into the sized HW buffer at the two bcopy sites (fix.diff, validated on a rebuilt kernel).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 1, "fix_verdict": "Baseline (shipped kernel #0, real handler): probe/evaded -> uid=0 root shell (never returns); rip/smash -> Fatal trap 9 at mpr_user_pass_thru+0x6fe: ret with attacker RIP/frame-pointer. Patched (fix.diff, single-fix nativekernel #1, real handler at 0xffffffff80598ae0, prologue verified): probe/rip/smash -> EINVAL before any copyin (rc=-1 errno=22, 3x repeat), evaded -> ENOMEM via the normal command-allocation path after a 12-byte copyin (rc=-1 errno=12); all ioctls RETURN cleanly, zero kernel traps, guest healthy. objdump of the installed patched binary shows cmp $0xb / cmp reqframesz reaching EINVAL before the copyin site.", "fix_evidence": "rig_fixed_runs.log (all four modes, patched kernel, guest health + dmesg trap count 0), rig_fixed_disasm_order.txt (cmp-before-copyin in the installed binary), rig_env_patched.txt (kernel #1 boot + kldstat + no /dev/mpr*), build.log (fix apply 3 hunks, nativekernel completed 22:48:42, installkernel INSTALL_RC=0), run.log (baseline column)" } |