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)
PoC verification
Evidence pack
findings/poc/DF-3260 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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:
- copy: sys/net/ipfw/ip_fw2.c:4515 (
rule->act_ofs = ioc_rule->act_ofs;) - validation:
ipfw_check_ioc_rule()(sys/net/ipfw/ip_fw2.c:5310-5551) validates sizes, opcodes, table ids, action placement β but act_ofs is never mentioned.
DF-0261 (pass 1) filed the resulting heap OOB read in the classification walk. Pass 2 established the additional sinks:
-
OOB WRITE, root-triggered β
clear_counters()(sys/net/ipfw/ip_fw2.c:5204-5215), reached fromIP_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 pastrule->cmdin the M_IPFW heap, value semi-controlled (read from +4). Demonstrstrated deterministically on the guest (see below). -
OOB WRITE (decrement), packet-triggered β
ipfw_log()(sys/net/ipfw/ip_fw2.c:1408-1416):l->log_left--on an OOBACTION_PTRwhen a rule containing anO_LOGinstruction matches any packet andfw_verbose=1. Same reach: root installs once, any matching packet (unprivileged local user / remote sender) decrements 4 bytes at a root-chosen offset. -
Kernel panic, packet-triggered (deterministic, reproduced) β the
O_PROBE_STATE/O_CHECK_STATEstate-hit jump (sys/net/ipfw/ip_fw2.c:4204-4210) setscmd = 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 == 0operand βl -= cmdlennever terminates (ip_fw2.c:3616) β netisr infinite loop (soft hang). Reproduced:panic: -- unknown opcode 127(panic.txt) viaipfw_chk β ipfw_check_in β pfil_run_hooks β ip_input. -
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
fixedWith 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
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.
No comments yet.