Kernel heap memory disclosure via ip_fw3_ctl_set_get unbounded bcopy from 4-byte field
Summary
ip_fw3_ctl_set_get copies sopt->sopt_valsize bytes from &ctx->sets but ctx->sets is single uint32_t(4 bytes) at tail of ~36-byte kmalloc struct ipfw3_context. sopt_valsize attacker-controlled up to 65536 non-root 32MB root via getsockopt never bounded to sizeof(ctx->sets). Overrun bytes fill correctly-sized sopt_val kernel buffer copyout back to caller leaking ~64KB adjacent kernel heap per call. &ctx->sets at offset 32 of 36-byte slab read runs off end of M_IPFW3 allocation into neighbouring heap. Requires raw IP socket NONET_RAW root or jail allow_raw_sockets. Repeatable KASLR-defeat heap-disclosure primitive.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2592 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | minimal leak trigger: raw socket + getsockopt(IP_FW_X,opcode=95) with large len | 6.9 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c | 120 B | view raw |
| run.sh | run-script | loads ipfw3 (safe default) and runs poc as root | 451 B | view raw |
| build.log | build-log | final successful build (BUILD_EXIT=0) | 13 B | view raw |
| run.log | run-log | baseline reproduction, 3 readlens, leaked kernel pointers | 3.8 KB | view raw |
| fix_run.log | run-log | patched-module re-run: 4 bytes back, 0 leaked | 2.3 KB | view raw |
| fix_build.log | build-log | ipfw3.ko module rebuild (MAKE_RC=0) | 853 B | view raw |
| leak_sample.txt | leak-sample | raw leaked bytes across runs incl. kernel text pointer 0xffffffff82600930 | 1.9 KB | view raw |
| env.txt | environment | uname, cc, kldstat, patched ipfw3.ko sha256, sysctl | 489 B | view raw |
| panic.txt | panic-signature | UNRELATED rn_flush unload panic (recorded; not DF-2592) | 817 B | view raw |
| fix.diff | suggested-fix | clamp sopt_valsize to sizeof(ctx->sets) in ip_fw3_ctl_set_get | 495 B | view raw |
| VERDICT.md | verdict | full narrative: reproduced? how/why? fix validated? | 6.5 KB | β raw |
DF-2592 β kernel heap memory disclosure via ip_fw3_ctl_set_get unbounded bcopy
Verdict
REPRODUCED on the unpatched 6.5-DEVELOPMENT #0 kernel (ipfw3.ko + ipfw3_basic.ko
loaded). FIX VALIDATED on the same #0 kernel with a single-clamp rebuild of
ipfw3.ko (sha256 f44f0a2aβ¦). The leak is real: an attacker-controlled
sopt->sopt_valsize drives an unbounded bcopy off the end of a 4-byte field,
disclosing stale kernel heap (text/data pointers) back to userspace on every call.
The bug (confirmed line-by-line)
ip_fw3_ctl_set_get() in sys/net/ipfw3/ip_fw3_set.c:207:
int
ip_fw3_ctl_set_get(struct sockopt *sopt)
{
struct ipfw3_context *ctx;
ctx = fw3_ctx[mycpuid];
bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize); /* line 213 */
return 0;
}
ctx->sets is a single uint32_t (4 bytes) β the last field of
struct ipfw3_context (sys/net/ipfw3/ip_fw3.h:482, 5 fields = 36 bytes on amd64),
allocated with kmalloc(LEN_FW3_CTX, M_IPFW3, M_WAITOK|M_ZERO) (ip_fw3.c:1410,
lands in the kmalloc-64 bucket). sopt->sopt_valsize is the caller-supplied
getsockopt length, never clamped to sizeof(ctx->sets). The bcopy therefore
reads sopt_valsize - 4 bytes past the sets field β through the zeroed
intra-chunk padding, then into neighbouring slab chunks / adjacent pages β and
copies all of it into the reply buffer, which sys_getsockopt then copyouts to
the caller (sys/kern/uipc_syscalls.c:1349).
Reach path (getsockopt, all in-kernel):
getsockopt(raw_ip_sock, IPPROTO_IP, IP_FW_X=49, buf, &len) /* len attacker-controlled */
sys_getsockopt (uipc_syscalls.c:1303) sopt_valsize=len; sopt_val=kmalloc(len)
-> sogetopt -> rip_ctloutput (raw_ip.c:327 SOPT_GET / case IP_FW_X:335)
-> ip_fw3_sockopt (ip_fw3_glue.c:51)
-> ip_fw3_ctl_x (ip_fw3.c:1039) strips 4-byte ip_fw_x_header{opcode=95},
sopt_valsize -= 4, shifts sopt_val
-> ip_fw3_ctl (ip_fw3.c:1070 case IP_FW_SET_GET=95)
-> ip_fw3_ctl_set_sockopt (ip_fw3_set.c:256)
-> ip_fw3_ctl_set_get (ip_fw3_set.c:207) *** unbounded bcopy -> OOB read ***
sopt_valsize is bounded only by SOMAXOPT_SIZE=65536 (non-root) /
SOMAXOPT_SIZE0=32MB (root) at uipc_syscalls.c:1324 β so the attacker chooses the
read length (minus the 4-byte IP_FW_X header).
Reproduction (unpatched #0 kernel)
Run as root after sysctl net.filters_default_to_accept=1; kldload ipfw3; kldload ipfw3_basic
(the latter survives ssh while the firewall is up):
# /tmp/poc2592 4096 [*] getsockopt(IPPROTO_IP, IP_FW_X=49, opcode=IP_FW_SET_GET=95) readlen=4096 [+] raw socket opened fd=3 [*] getsockopt rc=0 errno=0 returned len=4092 <-- 4092 bytes back from a 4-byte field [!] LEAK CONFIRMED: 477 nonzero bytes returned past the 4-byte sets field offset 0x10: 80 98 0a 4f 00 f8 ff ff -> 0xfffff8004f0a9880 (kernel direct-map pointer) offset 0x18: 30 09 60 82 ff ff ff ff -> 0xffffffff82600930 (inside ipfw3.ko text @ 0xffffffff82600000)
The leaked 0xffffffff82600930 is a kernel text pointer into the just-loaded
ipfw3.ko module (kldstat base 0xffffffff82600000) β a definitive KASLR-defeat /
kernel-base disclosure. Identical residue across runs (4096/2048/8192) reflects a
deterministic heap layout on this boot; the values are genuine heap pointers, not a
static/cosmetic buffer.
Privilege analysis (why this is a leak, not an escalation)
The only entry to ip_fw3_ctl_* is rip_ctloutput, the pr_ctloutput of the raw-IP
protocol. Creating such a socket runs rip_attach (raw_ip.c:460) which gates on
caps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW) (raw_ip.c:473) β i.e. root
(or a jail with allow_raw_sockets, which still grants it only to jail-root). An
unprivileged user cannot enter the bug path. Per the Phase-6 bright-line rule,
root-only reachability is a valid hard blocker for escalation, but the finding
class here is info leak, not memory corruption: there is no chain to develop.
The realistic impact ceiling is a rootβkernel heap disclosure / KASLR-defeat
primitive (a hardening gap; root can already read kernel memory via /dev/mem,
kmem, etc., so practical privilege gain is nil, but the uninitialised-kernel-memory
exposure is real and worth fixing).
The fix
One clamp, at the root cause (ip_fw3_set.c:213):
/* DF-2592: ctx->sets is a single uint32_t; never copy more than that,
* otherwise we read (and disclose) stale kernel heap past the field. */
if (sopt->sopt_valsize > sizeof(ctx->sets))
sopt->sopt_valsize = sizeof(ctx->sets);
bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize);
Standalone git apply-able diff: fix.diff. Supersedes the generic
"zero/M_ZERO the response" idea in the finding prompt β the real defect is an
unbounded read length, not an uninitialised stack struct, so clamping the length
(not zeroing the destination) is the correct minimal fix.
Fix validation (rebuilt ipfw3.ko, reloaded, re-ran same PoC)
- Before (unpatched #0 kernel + #0 module, sha
ca6ccfb9β¦):getsockopt(len=4096)β returnslen=4092, 477 nonzero bytes past the 4-bytesetsfield, including kernel text pointer0xffffffff82600930. - After (#0 kernel + rebuilt single-fix
ipfw3.ko, shaf44f0a2aβ¦):getsockopt(len=4096)β returnslen=4, 0 nonzero bytes past offset 4 (only the validctx->setsvalue is copied out). Repeated at readlen 4096/8192 β deterministic. - Rebuilt via
cd /usr/src/sys/net/ipfw3 && make KERNBUILDDIR=β¦/X86_64_GENERIC(module-only;MAKE_RC=0), installed to/boot/kernel/ipfw3.ko, clean reboot to load (kldunload of the active firewall triggers a separatern_flushpanic β seepanic.txtβ unrelated to DF-2592).
fix_status = fixed (clean before/after, deterministic across readlen and runs).
Files
poc.cβ minimal leak trigger (raw socket +getsockopt(IP_FW_X)with opcode 95).build.sh/run.shβ exact build & run (run as root after loading ipfw3).run.logβ baseline reproduction (3 readlens, leaked kernel pointers).fix_run.logβ patched-module re-run (4 bytes back, 0 leaked).fix_build.logβ module rebuild output (MAKE_RC=0).leak_sample.txtβ raw leaked bytes across runs (kernel text/data pointers).env.txtβ guest uname / cc / kld / sha / sysctl state.panic.txtβ the unrelatedrn_flushunload panic (recorded for completeness).fix.diffβ the standalone git-apply-able clamp fix.manifest.jsonβ artifact catalog.
Fix verification
fixedVALIDATED: ./poc 4096 leaked 477 nonzero bytes (incl. kernel text ptr 0xffffffff82600930) on unpatched ipfw3.ko baseline (ca6ccfb9...) and leaks ZERO on single-fix rebuilt ipfw3.ko (f44f0a2a...) β getsockopt now returns len=4 (only valid ctx->sets field), deterministic across readlen 4096/8192. Clamp closes the bug.
baseline (unpatched ipfw3.ko): getsockopt returned len=4092; LEAK CONFIRMED 477 nonzero bytes past 4-byte sets field; hexdump 80980a4f00f8ffff 30096082ffffffff (kernel ptrs). patched (ipfw3.ko f44f0a2a...): getsockopt returned len=4; got=4 bytes; nonzero total=0, after[4]=0 -> no nonzero bytes past offset 4.
Confirmed kernel references
Detail
Exploit chain
none β pure kernel-heap INFO LEAK, not memory-corruption primitive, no escalation chain to develop. Leak path reachable only through raw-IP socket whose creation gates on caps_priv_check(SYSCAP_NONET_RAW) (raw_ip.c:473) = root; unprivileged user cannot enter, so root-only reachability valid hard blocker. Realistic impact ceiling: root->kernel heap disclosure / KASLR-defeat primitive (hardening gap: root can already read kernel memory via /dev/mem, so practical privilege gain nil, but uninitialized-kernel-memory exposure real).
Evidence (decisive lines)
baseline (unpatched #0): getsockopt(IPPROTO_IP,IP_FW_X=49,opcode=95,len=4096) rc=0 returned len=4092; got=4092 bytes back; nonzero total=477, after[4]=477, after[36]=462; ctx->sets=0x00000000; LEAK CONFIRMED. hexdump offset 0x10: 80 98 0a 4f 00 f8 ff ff -> 0xfffff8004f0a9880 (kmem ptr); offset 0x18: 30 09 60 82 ff ff ff ff -> 0xffffffff82600930 (ipfw3.ko text). Identical residue at readlen 4096/2048/8192. patched module: getsockopt returned len=4, got=4, nonzero after[4]=0 -> leak gone.
PoC changes
Authored new poc.c (dir empty): opens raw IP socket, churns heap (pipes/sockets) to populate slab residue, issues getsockopt(IPPROTO_IP, IP_FW_X=49) with 4-byte ip_fw_x_header{opcode=95} + large payload, scans returned bytes past 4-byte ctx->sets field for non-zero kernel-pointer leaks with hexdump. build.sh, run.sh, fix.diff (clamp sopt_valsize to sizeof(ctx->sets)).
Verified recommended fix
In ip_fw3_ctl_set_get (sys/net/ipfw3/ip_fw3_set.c:213) clamp sopt_valsize to sizeof(ctx->sets) before bcopy: 'if (sopt->sopt_valsize > sizeof(ctx->sets)) sopt->sopt_valsize = sizeof(ctx->sets);'. Supersedes 'zero/M_ZERO the response' idea β real defect is unbounded READ length (not uninitialized stack struct), so clamping length (not zeroing destination) is correct minimal fix. Full git-apply-able diff in findings/poc/DF-2592/fix.diff.
Verdict
REPRODUCED. ip_fw3_ctl_set_get (sys/net/ipfw3/ip_fw3_set.c:207-215) does bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize) where ctx->sets is a single uint32_t (4 bytes, last field of 36-byte kmalloc-64 struct ipfw3_context) and sopt_valsize is the attacker-controlled getsockopt length, never clamped to sizeof(ctx->sets). Confirmed on unpatched #0 kernel (ipfw3.ko+ipfw3_basic.ko loaded): getsockopt(IPPROTO_IP, IP_FW_X=49, opcode=IP_FW_SET_GET=95, len=4096) -> rip_ctloutput -> ip_fw3_ctl_x -> ip_fw3_ctl_set_get -> OOB bcopy reads 4088 bytes past the 4-byte field. Reply copyout'd back, disclosing stale kernel heap including kernel text pointer 0xffffffff82600930 (inside ipfw3.ko @ base 0xffffffff82600000) and direct-map pointer 0xfffff8004f0a9880. 477 nonzero leaked bytes per call at readlen=4096; attacker controls read length up to 65536 (non-root)/32MB (root).
No comments yet.