β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0473

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0473 Β· 16 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + threat model
↓ download 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 the ipfw3 KLD 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's cmd_len fix β€” 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).

VERDICT.md verdict full analysis: mechanism, distinctness from DF-0472, impact, fix, validation
↓ download raw

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 total sopt_valsize ([sizeof(ioc_rule)-sizeof(ipfw_insn) .. 1020], :956-957), never the per-instruction module/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.c rip_ctloutput β†’ ip_fw3_sockopt (ip_fw3_glue.c) β†’ ip_fw3_ctl_x (ip_fw3.c:1038) strips the 4-byte x_header β†’ ip_fw3_ctl β†’ ip_fw3_ctl_sockopt (:1138) case IP_FW_ADD β†’ ip_fw3_ctl_add_rule (:951).
  • No per-instruction validation (:956-979): only sopt_valsize and (with DF-0472's fix) cmd_len/act_ofs/size are checked; module/ opcode of every instruction are copied verbatim into the rule by add_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_chk walks the rule and calls (filter_funcs[cmd->module][cmd->opcode])(...). Attacker module=0x80, opcode=0x80 indexes 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 ipfw3 KLD 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline Fatal trap 9; patched EINVAL no panic. Legit+nullcall OK.

BEFORE: panic ip_fw3_chk+0x1a4. AFTER: EINVAL, guest up.
↓ fix.diff6.5-DEVELOPMENT #0 (module hot-swap ipfw3.ko)

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.