⬢ DragonFlyBSD Kernel Audit
DF-3398 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-3398",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "Reproduced on the 6-vCPU stock INVARIANTS guest as a cross-object heap corruption: the outbound ICMP NAT store at ip_fw3_nat.c:326 uses s->alias_port (htons of the 32-bit-wrapped (src*dst)%%64511 residue) as the raw icmp_in[] index; for residues whose low byte is 0xFC..0xFF (1012 of 64511, ~1.57%%, attacker-steerable by picking src/dst) the index is 64512..65532, writing an 8-byte nat_state2 heap pointer 8..8184 bytes past the 1,548,288-byte cfg_alias allocation. /dev/kmem differential: baseline shows the neighbouring cfg_aliass ip field (10.0.2.15) replaced by 0xfffff8004f1025c0 (an M_IPFW3_NAT pointer) at exactly +16 bytes past the flow cpus alias end, while all other cpus neighbours are intact; an instrumented diagnostic module printed rawport=64512..64515 confirming the raw index. fix.diff (store at host-order residue + bounds/byte-order-checked inbound lookup) validated on a rebuilt kernel+module: neighbour ip intact, state stored in-bounds (icmp_in[252]=0xfffff8004f1025e0), NAT still translates. Impact field panic reflects kernel heap corruption class; on this guest the write is silent corruption (no panic observed) -- the primitive is a repeatable, attacker-offset heap pointer store, not a one-shot crash. uid=0 not pursued: the subsystem is only reachable after root configures NAT, so the primitives realistic value is remote-triggered corruption/DoS of NAT routers.",
  "exploit_chain": "root: kldload ipfw3_nat; ipfw3 nat 1 config ip <alias>; ipfw3 add 100 nat 1 icmp out -> any unprivileged local raw-socket user (or internal host) sends ICMP echo requests whose (src*dst mod 2^32) %% 64511 has low byte 0xFC..0xFF (brute-forced dst in the PoC) -> kernel writes nat_state2* at chosen offset in the 8KB window past a 1.5MB kmem allocation -> demonstrated corruption of neighbouring cfg_alias (alias ip field). Escalation to uid=0 not developed (root-config-gated subsystem, corruption primitive is pointer-valued at fixed relative offsets).",
  "evidence": [
    "findings/poc/DF-3398/run.log: baseline kcheck (cpu0 +16 = 0xfffff8004f1025c0 heap pointer where all neighbours show ip 0x0f02000a) + NATDBG rawport prints",
    "findings/poc/DF-3398/fix_run.log: fixed module, all cpus +16 = intact 0x0f02000a, in-bounds icmp_in[252]=0xfffff8004f1025e0",
    "findings/poc/DF-3398/fix.diff: verified one-hunk store fix + inbound lookup bounds check"
  ],
  "kernel_refs": [
    "sys/net/ipfw3_nat/ip_fw3_nat.c:310",
    "sys/net/ipfw3_nat/ip_fw3_nat.c:326",
    "sys/net/ipfw3_nat/ip_fw3_nat.h:133",
    "sys/net/ipfw3/ip_fw3.c:413"
  ],
  "poc_changes": "Seed sketch had no solver and no byte-order model. Rebuilt twice: (1) modular-inverse src solver assumed full-width product -- WRONG: C computes the product in 32-bit wrapping arithmetic, so the first-generation third packet tcpdump oracle was invalid (flows hit in-bounds indices); (2) final: brute-force dst solver replicating the C wrap, /dev/kmem differential reader (kcheck) resolving ip_fw3_nat_ctx -> nats[0] -> alias from the loaded modules runtime symbol, plus a diagnostic instrumented module to print the raw index. Proof is the baseline-vs-fixed kmem differential at the exact predicted offset.",
  "attempts": 6,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 05:08:20 UTC 2026\troot@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64",
  "runtime_sec": 4200,
  "guest_dirty": 0,
  "build_cmd": "guest: cc -O2 -o /root/icmp_oob icmp_oob.c; cc -O2 -o /root/kcheck kcheck.c; baseline module: cd /usr/src/sys/net/ipfw3_nat && make && make install (pristine source); fixed: same after patch -p1 < fix.diff; kernel: make -j6 nativekernel && make -j6 installkernel",
  "run_cmd": "ipfw3 nat 1 config ip 10.0.2.15; ipfw3 add 100 nat 1 icmp from any to any out via vtnet0; /root/icmp_oob 5 0xfc 0x1fc 0x2fc 0x3fc; /root/kcheck 0 <runtime ip_fw3_nat_ctx addr>",
  "code_hash": "79da7048367768ac3f70aadb6a8d7cbd10284d224a71c6d0c7017c81e4e85f19",
  "notes": "Impact recorded as panic (kernel-heap-corruption bucket) though no panic occurred on the guest: the demonstrated effect is silent cross-object corruption; the honest impact ceiling is memory corruption + DoS. The invalid first-generation oracle is documented in VERDICT.md. Guest reset to clean-source snapshot (vm.sh reset with-src) after the run; kernel #0, no modules loaded, no PoC files present.",
  "recommended_fix": "Store the return state at icmp_in[(src*dst) % ALIAS_RANGE] (host-order, always < ALIAS_RANGE) instead of the raw htonsd s->alias_port, and bounds-check + ntohs the inbound icmp_id lookup (fix.diff, validated)",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 05:08:20 UTC 2026\troot@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64 (kernel rebuilt from same source; ipfw3_nat module carries fix.diff)",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Baseline (pristine module) reproduces the OOB store: neighbouring cfg_alias ip field at +16 past the flow cpus alias shows the nat_state2 pointer 0xfffff8004f1025c0 (other cpus intact at the same offset). Patched module (fix.diff), identical trigger: neighbour ip intact (0x0f02000a) on all cpus, state stored in-bounds (icmp_in[252] = 0xfffff8004f1025e0), ICMP NAT translation still functional (tcpdump quad with ip.forwarding=1). Corruption eliminated, no functional regression.",
  "fix_evidence": "findings/poc/DF-3398/fix_run.log (all-cpu kcheck dump + immediate in-bounds slot read after trigger)"
}