⬢ DragonFlyBSD Kernel Audit
DF-2675 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2675",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "uid0-CHAIN ATTEMPT (exploit-dev mode): NOT COMPLETED; finding remains REPRODUCED with Critical impact. The full escalation chain was engineered end-to-end and verified byte-exact offline (see pack/CHAIN_ENG.md + solve10.py + genplan4.py flow trace): victim = the 64KB cluster buffer of /lib/libc.so.8 @0x100000 (window = libc[0x100000..0x100800)); landing = fflush() entry +0x4a4; trigger = setuid-root /usr/bin/chpass whose exit-time stdio flush executes the stub with euid=0; payload = three crafted LFN filenames (mount_msdos -L zh_CN.GB18030; 26/32 bytes per dirent record attacker-controlled, restricted to the guest-calibrated xlat16 pair map incl. measured GB18030 quirk mappings) whose records, landed through the aliased page, form: xor edi,edi; rdx=rsp; push 0 (argv[1]); rax=59; hop-chain; mov edi,&\"/bin/sh\" (chpass rodata 0x403c18, encoded via the 8.3 StartCluster=0x18BF and FileSize=0x403C fields); bridge add [rsp+1],al; push S; jmp; push rsp/pop rsi (8.3 cluster 0x5E54); execve syscall (8.3 size 0x050F) -> root shell. A C3 RET guard preserves the corrupted isatty. The chain blocked at RUNTIME PLACEMENT: the aliased write reaches libcs VM page only when the victim buffer is qentered AFTER the 17-page directory buffer (last pmap_qenter wins, vfs_bio.c:3211 vs the neighbors own qenter), but hammer2 allocates exactly one cached buffer per (vnode, 64KB block) (hammer2_vnops.c:933-942, lbase = off & ~0xFFFF), so a fresh-after-O victim for the fixed code block needs ~nbuf allocations of pool-churn per attempt. Demonstrated on the guest while grooming (3 boots): the churn recycles live msdosfs root/FAT/dir buffers whose re-reads create new 17-page monsters -- 2x panic at msdosfs_readdir+0x477 (teardown fault on the aliased KVA), 1x panic in hammer2_base_delete->hammer2_flush_core (a 17th PTE overwrote a live metadata buffers page-0; hammer2 consumed foreign bytes as chain metadata), and root-buffer deget cache corruption wedging the box; kern.nbuf=2048 (set via loader.conf, vfs.nbuf confirmed) made recycling cheap but destabilized the guest within one run. Net: unprivileged grooming alone reproducibly corrupts live kernel filesystem metadata to panic; the uid=0 code-page route is engineering-complete but the O-then-V adjacency could not be stabilized on this single-tenant guest before the bugs own side effects took the box down.",
  "exploit_chain": "ENGINEERED (offline-verified, runtime-blocked): crafted FAT16 image (64KB clusters @ xoff 2048; forged free-cluster list 6335/24148) -> root mounts with -L zh_CN.GB18030 (attacker-crafted-media gating, same as the finding) -> unprivileged grooming: per attempt [churn][readdir D_k = 17-page O on cpu o][64KB read of libc@0x100000 = V on cpu o+1][oracle readdir !=1984 entries] -> on adjacency, 3 file creates write the LFN/8.3 records through the aliased page into libcs window page -> unprivileged exec of /usr/bin/chpass -> exit-flush lands on the stub at fflush+0 -> setuid-held euid 0 + execve(\"/bin/sh\") -> uid=0 shell. BLOCKER (path-accurate): requires V.qenter AFTER O.qenter; hammer2s single cached buffer per (vnode,64KB-block) (hammer2_vnops.c:938) means V refresh needs ~vfs.nbuf allocations of churn; the churn itself re-triggers the same bug against live fs metadata (3 panics + wedge observed) before adjacency statistics complete. RUNTIME-DEMONSTRATED this session instead: unpriv grooming -> msdosfs_readdir teardown panics x2, hammer2_base_insert/delete-class flush panic, root-buffer cache corruption (deget) -- on top of the original verified readdir leak + aliased-write + panic.",
  "evidence": [
    "pack/CHAIN_ENG.md - complete chain engineering + the honest blocker analysis",
    "pack/solve10.py - the pair-constrained x86 stub solver (run: 'solve: True'; 344 placed bytes)",
    "pack/genplan4.py + pack/chain_plan.json - the byte-exact flow trace (1188 pad/xor edi -> ... -> 1692 execve) and the 3 payload filenames",
    "pack/mkchain5.py - the FAT16 image forger (400 dirs, free@6335,24148)",
    "pack/chain15.c, pack/chain16.c - the grooming/trigger runners (readdir oracle, cpu pairings, junk churn, chpass exec)",
    "pack/pairmap_gb.json, pack/avail_emp.json, pack/bulkcal3.c, pack/pairprobe.sh - the guest-calibrated GB18030 xlat16 pair map + quirk measurements",
    "pack/panic_chainwalk.txt - the three panic signatures captured this session from unprivileged grooming",
    "original pack: panic.txt, run.log, fix.diff, fix_run.log (leak + write + panic + validated fix)"
  ],
  "kernel_refs": [
    "sys/kern/vfs_bio.c:3211",
    "sys/kern/vfs_bio.c:3044",
    "sys/kern/vfs_bio.c:638",
    "sys/vfs/msdosfs/msdosfs_vnops.c:513",
    "sys/vfs/msdosfs/msdosfs_lookup.c:639",
    "sys/vfs/msdosfs/msdosfs_conv.c:527",
    "sys/vfs/hammer2/hammer2_vnops.c:938",
    "sys/platform/pc64/x86_64/machdep.c:1486",
    "sys/kern/kern_usched.c:234",
    "bin/sh/options.c:98"
  ],
  "poc_changes": "exploit-dev session: (1) mapped the whole msdosfs dirent-write byte surface (LFN records = 26/32 attacker bytes; 8.3 StartCluster/FileSize = 4 more with cluster/size forging); (2) discovered mount_msdos -L <locale> + kldload msdos_iconv enables UCS-2 LFN control and calibrated the kernel xlat16 pair map in-guest (5842 pairs + GB18030 vendor-quirk divergences e.g. U+74EB->U+75EB, verified-encodable (EB,73) etc.); (3) identified the no-ASLR guest (vm.randomize_mmap=0) and the >32-bit libc base problem, solved via a non-PIE setuid binarys own rodata \"/bin/sh\" at 0x403c18 encoding into the 8.3 cluster/size fields; (4) picked chpass/fflush (window block 0x100000) as the landing with an isatty RET-guard; (5) built the pair-constrained stub solver with rel8 hop chains, bridge instructions over structural dirent bytes, and cluster/size-encoded instructions (54 5E / 0F 05); (6) grooming runners with readdir oracle, 6 cpu pairings, junk-pool churn and a kern.nbuf=2048 boot tunable.",
  "attempts": 9,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (+ one kern.nbuf=2048 boot of the same kernel)",
  "runtime_sec": 46800,
  "guest_dirty": 0,
  "build_cmd": "in-guest root: kldload libiconv; kldload /boot/kernel/msdos_iconv.ko; cc -O2 -o /root/chain16 /root/chain16.c; vnconfig -c vn0 /root/chain4.img; mount_msdos -L zh_CN.GB18030 -u maxx -g maxx /dev/vn0 /mnt",
  "run_cmd": "as root (mount owner): /root/chain16 /mnt 0   [all post-mount steps readdir/pread/create only; trigger execs /usr/bin/chpass]",
  "code_hash": "",
  "notes": "uid0: false. The chain is complete offline (flow trace byte-exact; every payload byte guest-calibrated) but runtime-blocked by the O-then-V qenter ordering vs hammer2s one-buffer-per-block caching; all churn strategies re-triggered the bug against live filesystem metadata (2x msdosfs_readdir+0x477 teardown panics, 1x hammer2_base_delete flush panic, deget root-cache corruption wedge; pack/panic_chainwalk.txt) before adjacency landed. Strongest runtime impact this session: unprivileged grooming alone -> reproducible kernel panic + live hammer2 metadata corruption (same class as the original touch+sync demonstration). Guest reset with-src after the final wedge; left up and clean.",
  "recommended_fix": "unchanged (fix.diff, already validated): getblk(): bound size + (loffset & PAGE_MASK) <= MAXBSIZE; allocbuf(): account page offset in the b_kvasize check; msdosfs: reject 64KB clusters at non-page-aligned data offsets at mount"
}