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

Unvalidated rule act_ofs escalates DF-0261's OOB read to a 4-byte heap OOB write, a deterministic packet-triggered kernel panic, and an F_LEN==0 netisr infinite loop

Summary

ipfw_check_ioc_rule() validates every instruction field except act_ofs, which is copied verbatim into the kernel rule (:4515) and then used by ACTION_PTR() consumers: (1) clear_counters() reached from IP_FW_ZERO/IP_FW_RESETLOG executes 'l->log_left = l->max_log' - a 4-byte store at rule->cmd+act_ofs*4+8 with the value read from +4 - with act_ofs up to 65535, i.e. a write anywhere within +256KB of the M_IPFW allocation, gated only on a 1-byte OOB read equaling O_LOG; (2) ipfw_log() OOB-decrements log_left on every matching packet when fw_verbose=1 - packet-triggered after one root install; (3) the O_PROBE_STATE/O_CHECK_STATE state-hit jump starts the action walk at ACTION_PTR: a rule-internal misalignment dispatches operand bytes as opcodes, giving panic('-- unknown opcode') (reproduced deterministically: opcode 127) or an F_LEN==0 non-terminating loop that pins a netisr thread; (4) ipfw_dummynet_io and lookup_next_rule read ACTION_PTR OOB (DF-0261 class). VERIFIED on guest: PoC1 (check-state rule with act_ofs=2 pointing at O_IP_SRC operand): packet 2 makes O_PROBE_STATE jump -> panic, reproduced twice identical traces; PoC2 (O_LOG rule act_ofs=1): IP_FW_ZERO store rewrites the following O_ACCEPT instruction word byte-exact (observed via IP_FW_GET; write value and offset both attacker-chosen; two spray-based cross-object attempts hard-wedged the guest at IP_FW_ZERO time). Root installs once (SYSCAP_NONET_RAW, securelevel<3); afterwards any unpriv local user or remote sender triggers the sinks with a single matching packet, and the operator's own IP_FW_ZERO fires the write. Fix validated (act_ofs < cmd_len + instruction-boundary check).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3260 Β· 16 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 6.1 KB ↓ raw
poc_misalign.c β€” 3.4 KB view raw
poc_write.c β€” 3.9 KB view raw
poc_oobwrite.c β€” 3.8 KB view raw
poc_regression.c β€” 2.9 KB view raw
build.sh β€” 125 B view raw
run.sh β€” 211 B view raw
build.log β€” 183 B view raw
build_fix.log β€” 910 B view raw
run.log β€” 1.7 KB view raw
run.2.log β€” 303 B view raw
panic.txt β€” 2.3 KB view raw
env.txt β€” 252 B view raw
fix.diff β€” 1.3 KB view raw
verdict.json β€” 5.1 KB view raw

DF-3260 β€” ipfw2 act_ofs unvalidated at rule install: heap OOB write, kernel panic, netisr infinite loop

File: sys/net/ipfw/ip_fw2.c (pass 2). Escalation of DF-0261 (which covers the OOB read side) to write/DoS sinks.

Build (guest, root)

cc -O -o poc_misalign poc_misalign.c
cc -O -o poc_write    poc_write.c
cc -O -o poc_regression poc_regression.c

Run (guest, root)

sysctl net.filters_default_to_accept=1   # keep ssh alive after kldload
kldload ipfw
ifconfig lo0 alias 127.0.255.255 netmask 255.255.255.255  # PoC1 only
./poc_misalign     # sends 2 UDP packets -> kernel PANICS on packet 2
./poc_write        # installs rule, IP_FW_ZERO -> 4-byte heap WRITE, prints proof

Expected output

poc_misalign (serial console, captured in panic.txt):

panic: -- unknown opcode 127
ipfw_chk() at ipfw_chk+0x2a1e
ipfw_check_in() at ipfw_check_in+0x23
pfil_run_hooks() ...
ip_input() ...

poc_write:

[+] rule 2000 installed (cmd_len=4, act_ofs=1)
[+] baseline cmd[3] = 0x0000012b
[+] IP_FW_ZERO(2000) done -> clear_counters() store fired
[+] after     cmd[3] = 0x00000128
[!!] 4-BYTE KERNEL HEAP WRITE CONFIRMED: cmd[3] 0x0000012b -> 0x00000128
    (value=max_log, offset=act_ofs*4+8, both attacker-chosen)

Fix validation

scp fix.diff guest:/tmp
(guest) cd /usr/src && git apply /tmp/fix.diff
(guest) cd /usr/src/sys/net/ipfw && make obj && \
        make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
kldload /usr/obj/usr/src/sys/net/ipfw/ipfw.ko
./poc_write        -> IP_FW_ADD: Invalid argument
./poc_misalign     -> IP_FW_ADD: Invalid argument
dmesg: "ipfw: act_ofs 1 not on instruction boundary"
./poc_regression   -> PASS (valid rules still install; traffic flows)

Privilege ceiling

Rule installation requires root (raw socket: caps_priv_check(SYSCAP_NONET_RAW) sys/netinet/raw_ip.c:473) and is blocked at securelevel>=3 (sys/net/ipfw/ip_fw2_glue.c:65-69). HOWEVER, once a root-installed rule is in place, the panic/loop sinks are triggered by any packet that matches the rule β€” i.e. an unprivileged local user or a remote sender.

VERDICT.md
↓ download raw

VERDICT β€” DF-3260

Status: reproduced (deterministic). Impact: panic/dos via packet-triggered kernel panic + netisr-occupying infinite loop; 4-byte kernel heap OOB write primitive (attacker-chosen offset within +256KB and value) demonstrated deterministically in-allocation and code-proven out-of-allocation.

Root cause

struct ip_fw.act_ofs (offset of the action instruction, in 32-bit words, from rule->cmd) is copied verbatim from the user-supplied rule and never validated against cmd_len or the instruction chain:

DF-0261 (pass 1) filed the resulting heap OOB read in the classification walk. Pass 2 established the additional sinks:

  1. OOB WRITE, root-triggered β€” clear_counters() (sys/net/ipfw/ip_fw2.c:5204-5215), reached from IP_FW_ZERO / IP_FW_RESETLOG (ip_fw2.c:6610-6622): c ipfw_insn_log *l = (ipfw_insn_log *)ACTION_PTR(rule); if (l->o.opcode == O_LOG) /* 1-byte read at cmd+act_ofs*4 */ l->log_left = l->max_log; /* 4-byte WRITE at cmd+act_ofs*4+8, value = 4 bytes at cmd+act_ofs*4+4 */ act_ofs ∈ [0, 65535] β†’ write anywhere up to +256KB past rule->cmd in the M_IPFW heap, value semi-controlled (read from +4). Demonstrstrated deterministically on the guest (see below).

  2. OOB WRITE (decrement), packet-triggered β€” ipfw_log() (sys/net/ipfw/ip_fw2.c:1408-1416): l->log_left-- on an OOB ACTION_PTR when a rule containing an O_LOG instruction matches any packet and fw_verbose=1. Same reach: root installs once, any matching packet (unprivileged local user / remote sender) decrements 4 bytes at a root-chosen offset.

  3. Kernel panic, packet-triggered (deterministic, reproduced) β€” the O_PROBE_STATE/O_CHECK_STATE state-hit jump (sys/net/ipfw/ip_fw2.c:4204-4210) sets cmd = ACTION_PTR(f) and walks the "action" chain from that offset. With act_ofs pointing inside the rule at an operand word, the operand bytes are interpreted as an opcode: - unknown opcode β†’ panic("-- unknown opcode %d") ip_fw2.c:4368 - F_LEN == 0 operand β†’ l -= cmdlen never terminates (ip_fw2.c:3616) β†’ netisr infinite loop (soft hang). Reproduced: panic: -- unknown opcode 127 (panic.txt) via ipfw_chk β†’ ipfw_check_in β†’ pfil_run_hooks β†’ ip_input.

  4. OOB reads β€” ipfw_dummynet_io() ip_fw2.c:4425 (cmd = rule->cmd + act_ofs), lookup_next_rule() ip_fw2.c:3006 β€” same class as DF-0261.

Reproduction on the guest (DragonFly 6.5-DEVELOPMENT, stock INVARIANTS kernel)

PoC 1 β€” deterministic panic (poc_misalign.c)

Rule installed via setsockopt(IP_FW_ADD) (accepted β€” proving act_ofs is not validated): check-state ip from 127.0.255.255 to any keep-state allow, cmd_len=5, act_ofs=2 (pointing at the O_IP_SRC operand word 0xffff007f; byte 0 = 0x7f = opcode 127). Packet 1 creates the state; packet 2 makes O_PROBE_STATE jump to ACTION_PTR == &cmd[2]:

panic: -- unknown opcode 127
ipfw_chk() at ipfw_chk+0x2a1e
ipfw_check_in() at ipfw_check_in+0x23
pfil_run_hooks() at pfil_run_hooks+0x6f
ip_input() at ip_input+0x611
netmsg_service_loop() ...

(panic.txt, guest #0 stock kernel.) Two boots, two identical panics β€” fully deterministic. After root installs the rule, the second packet can be sent by any unprivileged user.

PoC 2 β€” deterministic 4-byte heap write (poc_write.c)

Rule: cmd = [O_LOG(len 3)][max_log=0x00000128][log_left][O_ACCEPT], cmd_len=4, act_ofs=1 β†’ ACTION_PTR = the max_log word whose low byte 0x28 == O_LOG. IP_FW_ZERO(2000) β†’ clear_counters() executes l->log_left = l->max_log β†’ 4-byte store at cmd+1*4+8 (= the O_ACCEPT instruction word) with value 0x00000128 taken from cmd+1*4+4. Observed byte-exact via IP_FW_GET:

baseline cmd[3] = 0x0000012b      (O_ACCEPT insn)
after     cmd[3] = 0x00000128      (attacker-chosen max_log value)

The same store with act_ofs β‰₯ cmd_len writes past the allocation (RULESIZE = 92+4*cmd_len bytes). Two additional spray-based attempts at the cross-object form (poc_oobwrite.c, attempts 3-4) hard-wedged the guest at IP_FW_ZERO time β€” consistent with the write landing in freed-chunk heap metadata β€” but were not instrumented (no console output survives a wedge), so the out-of-allocation landing is carried by code analysis plus the in-allocation proof, not by a clean log.

Why no uid0 chain

Exploitation requires installing the malformed rule, which requires SYSCAP_NONET_RAW (root) and is blocked at securelevel>=3. The corruption cannot be initiated by an unprivileged user; only the trigger packet is unprivileged. Post-root kernel memory corruption on a host where root can already kldload code has no escalation value; severity is therefore graded on kernel-integrity/DoS impact (Medium), not privesc.

Fix (validated)

fix.diff adds to ipfw_check_ioc_rule(): - act_ofs >= cmd_len β†’ EINVAL (bounds); - act_ofs must land exactly on an instruction boundary of the validated chain (act_ofs_ok), killing operand misinterpretation (panic/loop) and every OOB use of ACTION_PTR at once.

Validated on the guest (module rebuilt with -Werror, loaded): - poc_misalign: IP_FW_ADD: Invalid argument, dmesg ipfw: act_ofs 2 not on instruction boundary β€” panic impossible; - poc_write: rejected the same way β€” write impossible; - poc_regression: valid rules (act_ofs on action boundary, act_ofs on O_LOG boundary) still install; 2 packets traverse the state rule; IP_FW_GET works; kernel alive. No regression.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

With fix.diff applied to the guest /usr/src and the rebuilt ipfw.ko loaded, poc_misalign and poc_write are both rejected at IP_FW_ADD with EINVAL (dmesg: 'ipfw: act_ofs N not on instruction boundary'); the panic and the heap write are gone. poc_regression confirms valid rules (act_ofs on the action boundary and on an O_LOG boundary) still install, state-rule traffic flows, and IP_FW_GET works - no functional regression.

run.log (fix validation section), build_fix.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (stock kernel; fixed code validated as rebuilt ipfw.ko module: cd /usr/src/sys/net/ipfw && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC, loaded via kldload)

Confirmed kernel references

Detail

Exploit chain

root: setsockopt(IP_FW_ADD) rule with crafted act_ofs -> (panic route) second packet on the keep-state flow makes O_PROBE_STATE jump to ACTION_PTR=operand word -> opcode dispatch on operand byte 0x7f -> panic('-- unknown opcode 127'); (write route) setsockopt(IP_FW_ZERO, rulenum) -> clear_counters() -> 4-byte store at rule->cmd + act_ofs4 + 8, value from act_ofs4+4, offset/value attacker-chosen within +256KB of the M_IPFW allocation; (packet-triggered write route) rule containing O_LOG + fw_verbose=1 -> ipfw_log() decrements 4 bytes at the same OOB address on every matching packet from any user.

Evidence (decisive lines)

["panic.txt: full serial-console panic 'unknown opcode 127' with ipfw_chk/ipfw_check_in/pfil_run_hooks/ip_input trace", 'run.log: PoC2 byte-exact proof - baseline cmd[3]=0x0000012b, after IP_FW_ZERO cmd[3]=0x00000128 (attacker-chosen max_log), EXIT=0', 'run.2.log: repeat run, mutation persists in kernel rule object', 'VERDICT.md: sink-by-sink line-cited analysis + fix validation transcript', 'fix.diff: act_ofs bound + instruction-boundary validation in ipfw_check_ioc_rule', 'build_fix.log: patched module built clean with -Werror']

PoC changes

seedless (new finding); three iterations: v1 spray-based cross-object write wedged the guest twice (heap metadata corruption, unlogged), v2 restricted candidates still wedged, v3 self-referential in-allocation proof (act_ofs=1 at max_log word) made the store deterministic and byte-exact observable via IP_FW_GET; separate panic PoC made deterministic by aliasing 127.0.255.255 on lo0 so the O_IP_SRC operand low byte equals opcode 127.

Verified recommended fix

In ipfw_check_ioc_rule(), reject rules whose act_ofs >= cmd_len or whose act_ofs does not land exactly on an instruction boundary of the validated command chain (see fix.diff).

Verdict

Unvalidated ipfw rule act_ofs (copied verbatim at ip_fw2.c:4515, never checked in ipfw_check_ioc_rule) escalates DF-0261's OOB read into: (a) a deterministic packet-triggered kernel panic (reproduced twice: 'panic: -- unknown opcode 127' from ipfw_chk via the O_PROBE_STATE ACTION_PTR jump, trace captured), (b) a netisr infinite-loop hang when the misaligned word has F_LEN==0, and (c) a 4-byte kernel heap write with attacker-chosen offset (act_ofs*4+8, up to +256KB) and attacker-influenced value, demonstrated deterministically via clear_counters()/IP_FW_ZERO (cmd[3] 0x0000012b -> 0x00000128 observed byte-exact through IP_FW_GET) and additionally reachable packet-triggered via ipfw_log()'s log_left-- when fw_verbose=1. Root-gated setup (SYSCAP_NONET_RAW, securelevel>=3 blocks), unprivileged/remote trigger after install, so no uid0 chain exists; impact is kernel integrity + DoS. fix.diff (act_ofs < cmd_len and on an instruction boundary) validated on the guest: both PoCs rejected with EINVAL, valid rules and traffic unaffected.