ip_fw3_chk: unbounded filter_funcs[module][opcode] indexing -> OOB function pointer call on remote traffic
Summary
ip_fw3_chk(:506-507): (filter_funcs[cmd->module][cmd->opcode])(...). filter_funcs[MAX_MODULE=10][MAX_OPCODE_PER_MODULE=100](:163). cmd->module/opcode are uint8_t(0-255) NEVER bounds-checked. module>=10 or opcode>=100 indexes past 1000-element array into fw3_modules/fw3_ctx/fw3_sync_ctx globals reading arbitrary func ptr and calling it. Reachable by remote traffic if matching rule contains such opcode (plantable via cmd_len gap DF-next or directly by root). No NULL check -> unregistered opcode NULL-deref panic. Fix: validate module<MAX_MODULE, opcode<MAX_OPCODE, fn!=NULL before call.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0473 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| oobcall.c | trigger-source | main trigger: install OOB-index rule (cmd_len=2), enable fw, send pkt -> panic | 6.1 KB | view raw |
| add_oob.c | trigger-source | install-only test (safe on unpatched: no fw enable) -> rc=0 on unpatched, EINVAL on patched | 1.6 KB | view raw |
| add_legit.c | trigger-source | regression test: legitimate rule (module=0 opcode=0) must still install | 1.5 KB | view raw |
| nullcall.c | trigger-source | exercises call-site NULL check (in-range/unregistered opcode=50) | 2.6 KB | view raw |
| build.sh | build-script | cc -Wall -o ... for all four binaries | 336 B | view raw |
| run.sh | run-script | kldload ipfw3 + ./oobcall | 647 B | view raw |
| fix.diff | suggested-fix | two-hunk fix: call-site bounds+NULL check (ip_fw3.c:506) + install-time range validation (ip_fw3_ctl_add_rule) | 1.7 KB | view raw |
| fix_build.log | build-log | single-fix ipfw3.ko build, -Werror rc=0 | 1.9 KB | view raw |
| fix_run.log | run-log | before/after fix validation: unpatched accept+panic vs patched EINVAL+no panic | 2.3 KB | view raw |
| run.log | run-log | baseline oobcall output + panic (ssh dies on firewall enable) | 1.4 KB | view raw |
| panic.txt | panic-signature | Fatal trap 9 at ip_fw3_chk+0x1a4 (filter_funcs[module][opcode] indirect call) | 207 B | view raw |
| env.txt | environment | uname, cc version, kldstat, sysctls, patched ipfw3.ko sha256 | 592 B | view raw |
| VERDICT.md | verdict | full analysis: mechanism, distinctness from DF-0472, impact, fix, validation | 7.8 KB | β raw |
| README.md | readme | build/run/expected + threat model | 2.7 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0473 β Out-of-bounds indirect function call in ip_fw3_chk (CWE-129/787)
ip_fw3_chk (sys/net/ipfw3/ip_fw3.c:506-507) indexes
filter_funcs[cmd->module][cmd->opcode] β a [10][100] (1000-entry) array β
with attacker-controlled uint8_t module/opcode that are never
bounds-checked. A crafted rule with module>=10 or opcode>=100 reads an
arbitrary kernel pointer and calls it => wild call => kernel panic (trap 9).
Files
oobcall.cβ main trigger: install OOB-index rule (cmd_len=2), enable firewall, send packet -> panic.add_oob.cβ install-only test (safe on unpatched: no firewall enable).add_legit.cβ regression test (legitimate rule must still install).nullcall.cβ exercises the call-site NULL check (in-range/unregistered opcode).build.sh/run.shβ exact build/run.fix.diffβ two-hunk fix (call-site bounds+NULL check + install-time range validation).VERDICT.mdβ full analysis + before/after fix validation.panic.txt/run.log/fix_run.log/fix_build.log/env.txtβ evidence.
Build
cc -Wall -o oobcall oobcall.c cc -Wall -o add_oob add_oob.c cc -Wall -o add_legit add_legit.c cc -Wall -o nullcall nullcall.c
Run (ROOT only β raw socket + ipfw3 ctl path)
kldload ipfw3 sysctl net.inet.ip.fw3.enable=0 ./oobcall # UNPATCHED: kernel panic at ip_fw3_chk+0x1a4
Expected (bug present): rc=0 on the rule add, then the firewall is
enabled and the guest panics on the next evaluated packet with
Fatal trap 9 ... Stopped at ip_fw3_chk+0x1a4: ret. (ssh dies; panic is in
dfbsd-qemu/boot.log.)
Preconditions / threat model (honest)
- Root-only trigger. The rule-add path (
IP_FW_X/IP_FW_ADD) needs a raw socket (root) and theipfw3KLD module loaded. No unprivileged-user privilege boundary is crossed; the value is the kernel memory-corruption primitive (CWE-129 OOB indirect call / CWE-787 wild call), not LPE. - Distinct from DF-0472. This PoC installs a correctly-sized rule
(
cmd_len=2, full data), so it survives DF-0472'scmd_lenfix β DF-0473 is an independent defect requiring its own call-site/install-time fix.
Fix
fix.diff (two hunks):
1. Call-site (ip_fw3.c:506): bounds-check
module < MAX_MODULE && opcode < MAX_OPCODE_PER_MODULE and NULL-check the
function pointer; on failure skip the instruction (goto next_cmd).
2. Install-time (ip_fw3_ctl_add_rule, :979): walk the rule's
instructions and return EINVAL if any has an out-of-range module/opcode.
After hot-swapping the patched ipfw3.ko: OOB rule add => EINVAL, no panic;
legitimate rules still accepted (rc=0).
DF-0473 β Verdict
Verdict: REPRODUCED (OOB indirect function call -> kernel panic). FIX VALIDATED.
The bug
ip_fw3_chk (sys/net/ipfw3/ip_fw3.c:506-507) evaluates a firewall rule's
instructions and, for each, performs an indirect call:
(filter_funcs[cmd->module][cmd->opcode])
(&cmd_ctl, &cmd_val, &args, &f, cmd, ip_len);
filter_funcs is declared at ip_fw3.c:163 as
filter_func filter_funcs[MAX_MODULE][MAX_OPCODE_PER_MODULE];
with MAX_MODULE = 10 and MAX_OPCODE_PER_MODULE = 100 (ip_fw3.h:94) β a
1000-entry array. cmd->module and cmd->opcode are uint8_t (range
0..255) taken verbatim from the rule (ipfw_insn, ip_fw3.h:124-132) and are
never bounds-checked anywhere:
- the rule-add path
ip_fw3_ctl_add_rule(ip_fw3.c:951) validates only the totalsopt_valsize([sizeof(ioc_rule)-sizeof(ipfw_insn) .. 1020],:956-957), never the per-instructionmodule/opcode; - the eval loop (
:493-507) indexes the array directly.
A rule whose instruction carries module >= 10 or opcode >= 100 therefore
indexes past the 1000-entry array, reads an arbitrary kernel pointer, and
calls it. With module=0x80 opcode=0x80 the index is
0x80*100 + 0x80 = 12928, i.e. 12828 entries past the array β deep into
neighbouring kernel data (the fw3_modules/fw3_ctx/fw3_sync_ctx globals
and beyond).
Distinctness from DF-0472
DF-0472 was the missing cmd_len validation: it fed garbage into the
rule via a cmd_len=255 over-read. DF-0473 is a separate, independent
bug: the OOB module/opcode index use. This PoC proves it by installing a
correctly-sized rule (cmd_len=2, one 8-byte instruction, act_ofs=0,
full data supplied) whose single instruction explicitly carries
module=0x80 opcode=0x80. This rule passes DF-0472's cmd_len validation
(size=48 β [32,1020], cmd_len=2, act_ofs=0 < cmd_len, full data
present) yet still triggers the OOB call. DF-0473 therefore survives a
complete fix for DF-0472 and must be fixed independently at the call site
(and/or the install path).
Mechanism (every hop cited)
- Trigger (root):
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)βsetsockopt(IPPROTO_IP, IP_FW_X=49, [x_hdr.opcode=IP_FW_ADD=50][ioc_rule], 48)(in.h:389).raw_ip.crip_ctloutputβip_fw3_sockopt(ip_fw3_glue.c) βip_fw3_ctl_x(ip_fw3.c:1038) strips the 4-bytex_headerβip_fw3_ctlβip_fw3_ctl_sockopt(:1138) caseIP_FW_ADDβip_fw3_ctl_add_rule(:951). - No per-instruction validation (
:956-979): onlysopt_valsizeand (with DF-0472's fix)cmd_len/act_ofs/sizeare checked;module/opcodeof every instruction are copied verbatim into the rule byadd_rule_dispatch(:655,bcopy(ioc_rule->cmd, rule->cmd, cmd_len*4)). - OOB indirect call (
:506): when the firewall is enabled and a packet is evaluated,ip_fw3_chkwalks the rule and calls(filter_funcs[cmd->module][cmd->opcode])(...). Attackermodule=0x80, opcode=0x80indexes entry 12928 of the 1000-entry array β wild pointer call β kernel panic.
Evidence
OOB-call panic (trap 9)
./oobcall on the unpatched #0 module installs the OOB-index rule
(rc=0), enables the firewall, and the guest panics on the next evaluated
packet (full signature in panic.txt):
Fatal trap 9: general protection fault while in kernel mode cpuid = 0; lapic id = 0 instruction pointer = 0x8:0xffffffff826001a4 current process = Idle Stopped at ip_fw3_chk+0x1a4: ret
RIP 0xffffffff826001a4 = ipfw3.ko base (0xffffffff82600000) + 0x1a4,
symbolised by ddb as ip_fw3_chk+0x1a4 β the
filter_funcs[module][opcode] indirect-call site (:506). The wild call ran
a few instructions off the corrupted pointer and faulted on ret.
Impact (honest)
- Root-only trigger. The rule-add path needs a raw socket (root) and the
ipfw3KLD module loaded. No unprivileged-user privilege boundary is crossed; the value is the kernel memory-corruption primitive (CWE-129 OOB indirect call / CWE-787 wild call), not LPE. Relevance: compromised root process, a setuid ipfw3 front-end, jail escape. - Primitive characterisation. On this guest SMEP/SMAP/KASLR are OFF, so
a heap-grooming chain that lands a chosen value at
filter_funcs[0x80][0x80]would redirect the call to userspace shellcode (commit_creds(prepare_kernel_cred(0))) for rootβkernel code execution; demonstrated here at the panic (DoS) level. Because the trigger is already root, an LPE chain is moot β the deliverable is the memory-corruption primitive itself.
The fix (fix.diff) β two defense-in-depth hunks
Hunk 1 (root cause, what the finding recommends): call-site bounds + NULL
check at ip_fw3.c:506. Before the indirect call, verify
cmd->module < MAX_MODULE && cmd->opcode < MAX_OPCODE_PER_MODULE and that
filter_funcs[cmd->module][cmd->opcode] != NULL; if any fails, set
cmd_val = 0 and goto next_cmd (skip the instruction, treat as non-match).
This is the last line of defence: it catches OOB/unregistered opcodes from
any source (a crafted rule, the DF-0472 over-read path, future bugs, or
corrupted memory).
Hunk 2 (defense-in-depth, front door): install-time range validation in
ip_fw3_ctl_add_rule (ip_fw3.c:979). Walk every instruction of the
candidate rule using the same F_LEN() stepping as ip_fw3_chk and
return EINVAL if any instruction has module >= MAX_MODULE or
opcode >= MAX_OPCODE_PER_MODULE. This rejects the malicious rule before it
is ever installed.
Both hunks are minimal and targeted; a legitimate rule (module=0 BASIC,
opcode=0 ACCEPT) is still accepted (verified: rc=0, no regression).
This fix supplements (does not conflict with) DF-0472's cmd_len fix β
the two address orthogonal defects in the same two functions.
Fix validation (Phase 8)
ipfw3 is a loadable module, so the fix was validated by rebuilding only
ipfw3.ko (make KERNBUILDDIR=.../X86_64_GENERIC in sys/net/ipfw3,
-Werror, rc=0) and hot-swapping it (kldunload/cp/kldload); no
kernel rebuild/reboot required.
| test | unpatched #0 module |
patched module |
|---|---|---|
OOB rule add (module=0x80 opcode=0x80) |
rc=0 (rule ACCEPTED β the bug) |
rc=-1 EINVAL (hunk #2 rejects) |
full oobcall PoC (install + enable + pkt) |
trap 9 panic at ip_fw3_chk+0x1a4 |
add rejected; firewall never enabled; no panic, guest UP |
legit rule add (module=0 opcode=0 ACCEPT) |
rc=0 |
rc=0 (no regression) |
in-range/unregistered opcode (module=0 opcode=50, NULL filter_func) β exercises hunk #1 |
would NULL-deref panic at :506 |
call-site NULL check skips; no panic, guest UP |
ipfw3.ko build |
n/a | -Werror, rc=0 |
Clean before/after: the bad behaviour (OOB rule accepted + panic) is present
on the unpatched #0 module and gone on the single-fix module, with
legitimate rules unaffected and both hunks independently exercised.
fix_status = fixed.
Reproduce
cd findings/poc/DF-0473 ./build.sh # cc -Wall -o oobcall oobcall.c (+ add_oob, add_legit, nullcall) # as root, on the guest: kldload ipfw3 sysctl net.inet.ip.fw3.enable=0 ./oobcall # UNPATCHED: kernel panic at ip_fw3_chk+0x1a4 # after applying fix.diff + hot-swapping ipfw3.ko: ./oobcall # PATCHED: add rejected EINVAL, no panic ./add_legit # PATCHED: legit rule still accepted (rc=0)
Fix verification
fixedVALIDATED: baseline Fatal trap 9; patched EINVAL no panic. Legit+nullcall OK.
BEFORE: panic ip_fw3_chk+0x1a4. AFTER: EINVAL, guest up.
Confirmed kernel references
Detail
Exploit chain
none -- root-only (raw socket + kldload). CWE-129 OOB indirect call. DoS/panic.
Evidence (decisive lines)
BEFORE: Fatal trap 9 ip_fw3_chk+0x1a4 ret. AFTER: add EINVAL, no panic, guest up. Legit rule rc=0.
PoC changes
oobcall.c (correctly-sized OOB rule), add_oob.c/add_legit.c/nullcall.c, fix.diff (call-site bounds+NULL check + install-time range validation), VERDICT.md, manifest.json.
Verified recommended fix
(1) Call-site :506 bounds-check module<MAX_MODULE && opcode<MAX_OPCODE_PER_MODULE + NULL check filter_funcs; (2) install-time walk+validate in ip_fw3_ctl_add_rule. Full diff in findings/poc/DF-0473/fix.diff.
Verdict
REPRODUCED. ip_fw3_chk :506 filter_funcs[cmd->module]cmd->opcode OOB call. filter_funcs[10][100], module/opcode uint8_t 0-255 unchecked. Rule with module=0x80 opcode=0x80 -> entry 12928 (12828 past array) -> Fatal trap 9 ip_fw3_chk+0x1a4. Distinct from DF-0472 (correctly-sized rule). ipfw3 KLD. Root-only.
No comments yet.