DF-3537 / verdict.json
{ "finding_id": "DF-3537", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "Reproduced on DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel): ng_vjcs vjcomp path guarantees only min(19, m_pkthdr.len) bytes but sl_uncompress_tcp_core reads up to 19 bytes unvalidated; a 2-byte frame makes the mandatory th_sum read (slcompress.c:524) cross the packet boundary and 30/30 probes wrote stale mbuf bytes (0x1111 = prior sync frames window field) into kernel cstate, read back via NGM_VJC_GET_STATE; diagnostic runs additionally showed DECODE-walk corruption of urp/win/ack/id with earlier-packet residue delivered out the ip hook by a clean emit frame. Leak-only (all writes stay inside struct cstate). fix.diff bounds-checks every engine read; rebuilt ng_vjc.ko: 30/30 -> 0/30 with legal frames intact.", "exploit_chain": "remote/local frame source -> short vjcomp frame -> sl_uncompress_tcp_core reads 1-17 bytes past m_len (checksum read + DECODE walk, no buflen checks) -> stale mbuf bytes persist in rstate[cid].cs_ip -> next clean compressed frame emits them inside the reconstructed TCP/IP header to the ip hook -> attacker reads reconstructed stream (up to ~10 network-visible bytes per probe pair, unlimited repetitions)", "evidence": "run.log / run.2.log: [oracle] 30/30 rounds leaked non-zero stale bytes via th_sum; leak_sample.txt (cross-packet traces incl. 0x6572, 0x7995, urp=0x33/win+=0x34 from a prior syncs seq field); fix_run.log: 0/30 after fix.diff with [sanity] th_sum = 0xC75D OK", "kernel_refs": [ "sys/netgraph/vjc/ng_vjc.c:456", "sys/netgraph/vjc/ng_vjc.c:461", "sys/netgraph/vjc/ng_vjc.c:468", "sys/net/ppp_layer/slcompress.c:115", "sys/net/ppp_layer/slcompress.c:524", "sys/net/ppp_layer/slcompress.c:571" ], "poc_changes": "Seed sketch was fully rewritten: real graph is built with libnetgraph (NGM_MKPEER/CONNECT/NAME + NGM_VJC_SET_CONFIG with binary struct ngm_vjc_config); deterministic oracle is the 2-byte-probe checksum read observed through NGM_VJC_GET_STATE (a struct slcompress mirror verified by a legal 19-byte frame); v1 path addressing requires trailing colons (vinj:); EINVAL from NgSendData is the nodes synchronous frame-drop, not an error.", "attempts": 14, "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": 3600, "guest_dirty": 0, "build_cmd": "cc -O -o /root/vjc_oob_leak /root/vjc_oob_leak.c -lnetgraph", "run_cmd": "kldload netgraph ng_socket ng_vjc && /root/vjc_oob_leak", "code_hash": "855b59d625de86b3d45bb091ed74209769674b230fbaae3f390dcf53597fc217", "notes": "DECODE-field exfil is mbuf-recycling dependent (often self-masks when stale bytes equal current state); the 2-byte checksum oracle is deterministic. Locally building the graph requires root; threat model is the remote PPP peer (same surface as DF-3272). Guest reset with-src performed after verification.", "recommended_fix": "Bounds-check every read in sl_uncompress_tcp_core against buf+buflen (DECODE macros, changes/CID/checksum, struct ip minimum) and fail the frame via the existing bad path; see fix.diff.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to /usr/src/sys/net/ppp_layer/slcompress.c in the guest; ng_vjc.ko (which compiles slcompress.c) rebuilt and reloaded; identical PoC run: 2-byte-probe oracle went from 30/30 leaked stale bytes to 0/30 while the legal-frame sanity check (th_sum=0xC75D) still passes. Same result for the ng7 twin module.", "fix_evidence": "fix_run.log (0/30 + sanity OK), fix_build.log (module relink), compare run.log (30/30)" } |