Unbounded table id in all dispatch/ioctl handlers -> per-CPU heap OOB write, OOB read, and wild function-pointer call
Summary
Every table op indexed by user-controlled ioc_table->id (int) performs table_ctx+=id against per-CPU array ctx->table_ctx[IPFW_TABLES_MAX=32] with NO bounds check anywhere. table_create_dispatch:92-95 reads int id=ioc_table->id table_ctx+=id then writes type/count/name(32B) + rn_inithead writes 2 kernel ptrs at OOB offset. Same at delete:127 append:149 remove:205 flush:244 rename:265. Read paths show:377-379 test:431-433 equally broken. Show calls rnh->rnh_walktree (function ptr off48) through OOB pointer = wild call RCE with grooming. Dispatch tour netisr_forwardmsg_all replicates corruption every CPU. Attacker: root/SYSCAP_NONET_RAW setsockopt(IPPROTO_IP,IP_FW_X,opcode 73-81). Impact: deterministic heap corruption + wild function-ptr call heap-groomable priv-esc/RCE. Jail boundary broken jailed root allow.raw_sockets corrupts host kernel. Fix: if(id<0||id>=IPFW_TABLES_MAX) goto done in every handler.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0668 · 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ipfw3_table_oob.c | trigger-source | setsockopt IP_FW_X TABLE_CREATE with attacker id -> OOB | 2.7 KB | view raw |
| build.sh | build-script | cc ipfw3_table_oob.c | 208 B | view raw |
| run.sh | run-script | set default-accept, kldload ipfw3/basic, run binary | 328 B | view raw |
| build.log | build-log | fixed ipfw3_basic.ko module build | 1.7 KB | view raw |
| run.log | run-log | baseline: unpriv EPERM + root id=16384 success(0) + id=0x10000000 panic | 1.3 KB | view raw |
| fix_run.log | run-log | fixed: id=16384 EINVAL, id=0x10000000 EINVAL no panic, id=5 valid 0 | 736 B | view raw |
| panic.txt | panic-signature | fatal trap 12 in table_create_dispatch+0x45 | 179 B | view raw |
| fix.diff | suggested-fix | bounds check id in all 6 table dispatch functions | 2.1 KB | view raw |
| env.txt | environment | uname / kern.version / cc / module state | 335 B | view raw |
| VERDICT.md | verdict | full narrative, root-only reachability, primitive, fix validation | 5.3 KB | ↓ raw |
| README.md | readme | build/run/expected | 1.4 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-0668 PoC — ipfw3 table op OOB via unchecked ioc_table->id
Build (as any user; runtime needs root)
cd findings/poc/DF-0668 ./build.sh # -> ipfw3_table_oob
Run (as root — needs a raw IP socket + ipfw3 loaded)
./run.sh [id] # default id=0x4000
run.sh sets net.filters_default_to_accept=1 first so that loading ipfw3 (whose
default policy is DENY) does not cut off your ssh session.
Expected (bug present, unfixed ipfw3_basic)
- As an unprivileged user:
socket(SOCK_RAW)→Operation not permitted(root-only). - As root:
id=16384→setsockopt returned 0(OOB accepted — a checked impl would EINVAL).id=0x10000000→ kernel panic:Fatal trap 12: page fault while in kernel mode Stopped at table_create_dispatch+0x45: movl $0,0x30(%rbx)
Expected (after applying fix.diff to /usr/src/sys/net/ipfw3_basic/ip_fw3_table.c and
rebuilding/loading ipfw3_basic.ko)
- id=16384 and id=0x10000000 → setsockopt returned -1 errno=22 (Invalid argument).
- valid id=5 → setsockopt returned 0 (legit tables still work).
Notes
- Loading ipfw3 enables packet filtering with default DENY; you MUST set
net.filters_default_to_accept=1beforekldload ipfw3or you lose network access. - Rebuilding just the module:
cd /usr/src/sys/net/ipfw3_basic && make, then loadipfw3_basic.koafterkldload ipfw3.
DF-0668 — ipfw3 table op OOB via unchecked ioc_table->id
Verdict: REPRODUCED (panic in cited function + fix validated)
Every ipfw3 table dispatch handler indexes a per-CPU array
ctx->table_ctx[IPFW_TABLES_MAX=32] with a user-controlled int id and no bounds
check:
/* sys/net/ipfw3_basic/ip_fw3_table.c:84-95 (table_create_dispatch) */
ioc_table = tbmsg->ioc_table;
int id = ioc_table->id; /* from user, unchecked */
table_ctx = ctx->table_ctx;
table_ctx += id; /* OOB pointer arithmetic */
table_ctx->type = ioc_table->type; /* write */
table_ctx->count = 0; /* write */
strlcpy(table_ctx->name, ...); /* write */
rn_inithead(&table_ctx->mask, ...);/* deposits a kernel pointer at OOB */
rn_inithead(&table_ctx->node, ...);/* deposits a kernel pointer at OOB */
The identical unchecked table_ctx += ioc_tbl->id exists in table_delete_dispatch
(:127), table_append_dispatch (:149), table_remove_dispatch (:205),
table_flush_dispatch (:244), table_rename_dispatch (:265). ctx->table_ctx is
kmalloc(32*sizeof(struct ipfw3_table_context)=1792, M_IPFW3_TABLE)
(ip_fw3_table.c:570); any id >= 32 walks past it. IPFW_TABLES_MAX=32 is defined
but never used to validate id anywhere.
Reachability — ROOT-ONLY (valid hard blocker for uid=0)
The sink is reached via setsockopt(IPPROTO_IP, IP_FW_X, ...) on a raw IP socket:
setsockopt(IPPROTO_IP, IP_FW_X=49, [x_header{opcode=IP_FW_TABLE_CREATE=73}]
[struct ipfw_ioc_table{id,type,...}])
-> raw_ip.c rip_ctloutput -> ip_fw3_sockopt -> ip_fw3_ctl (IP_FW_X)
-> ip_fw3_ctl_x (strip x_header, sopt_name=73) -> ip_fw3_ctl (IP_FW_TABLE_CREATE)
-> ip_fw3_ctl_table_ptr -> ip_fw3_ctl_table_create -> table_create_dispatch [per CPU]
Creating a raw IP socket requires caps_priv_check(SYSCAP_NONET_RAW)
(sys/netinet/raw_ip.c:473). An unprivileged user gets EPERM:
$ ./ipfw3_table_oob 33 # as maxx (uid 1001) socket(SOCK_RAW) failed: Operation not permitted (expected for unpriv)
So the write is reachable only from an already-root context. This is a root→kernel hardening gap (a root user with ipfw3 access can corrupt arbitrary kernel memory), not an unprivileged local privilege escalation. There is no unprivileged→uid=0 chain because the privilege boundary (raw-socket capability) is not crossed.
Primitive characterization (measured on this guest)
| property | value |
|---|---|
| allocation overflowed | kmalloc(1792, M_IPFW3_TABLE) per CPU (ctx->table_ctx) |
| write offset | id * sizeof(struct ipfw3_table_context) = id * 56 bytes (fully attacker-controlled via id) |
| fields written | type (attacker int), count (0), name[32] (attacker bytes), + 2 radix_node_head kernel pointers (rn_inithead) |
| small OOB (id=33..N) | silent corruption of adjacent M_IPFW3_TABLE slab / kmalloc arena |
| large OOB (id=0x10000000) | page fault, panic in table_create_dispatch+0x45 (movl $0,0x30(%rbx) = table_ctx->count = 0) |
Decisive baseline evidence (id=0x10000000, root):
Fatal trap 12: page fault while in kernel mode Stopped at table_create_dispatch+0x45: movl $0,0x30(%rbx)
Escalation ceiling
None for an unprivileged user (root-only reachability — see above). For a root caller
the primitive is a fully-controlled kernel heap write (chosen 56-byte-stride offset +
controlled type/name bytes + kernel pointer deposits), trivially gameable for
root→kernel code execution, but root→kernel is already game-over. The realistic
finding value is a privilege-boundary / robustness bug: ipfw3 must never trust
ioc_table->id, and any future unprivileged ipfw3 control path (e.g. a delegated
socket, jail, or setuid helper) would turn this into local kernel RCE.
PoC changes
- Wrote
ipfw3_table_oob.c: opens a raw IP socket, issuessetsockopt(IPPROTO_IP, IP_FW_X, {x_header opcode=TABLE_CREATE} + {ioc_table id,type}).idis argv[1] (default 0x4000). Build/run viabuild.sh/run.sh(run.sh loads ipfw3 withnet.filters_default_to_accept=1so ssh survives the firewall).
Fix (fix.diff)
Add an id < 0 || id >= IPFW_TABLES_MAX bounds check (return EINVAL, forward the
netmsg and return) to all six table dispatch functions (create/delete/append/
remove/flush/rename). Validated on a freshly-built ipfw3_basic.ko:
- baseline:
id=16384→setsockoptreturns 0 (OOB accepted/silent);id=0x10000000→ panic intable_create_dispatch. - fixed:
id=16384andid=0x10000000→ EINVAL (no panic); validid=5→ 0 (legit tables still work).
The finding markdown had no per-finding proposal to supersede; this fix targets the
root cause (missing bounds check at every table_ctx += id).
Kernel references (verified)
sys/net/ipfw3_basic/ip_fw3_table.c:92-95— create:id = ioc_table->id; table_ctx += idsys/net/ipfw3_basic/ip_fw3_table.c:127,149,205,244,265— delete/append/remove/flush/rename:table_ctx += ioc_tbl->idsys/net/ipfw3_basic/ip_fw3_table.c:570—ctx->table_ctx = kmalloc(... * IPFW_TABLES_MAX, ...)sys/net/ipfw3_basic/ip_fw3_table.h:39—#define IPFW_TABLES_MAX 32sys/netinet/raw_ip.c:473—caps_priv_check(... SYSCAP_NONET_RAW)(root-only)sys/netinet/in.h:389—#define IP_FW_X 49sys/net/ipfw3/ip_fw3.h:397—#define IP_FW_TABLE_CREATE 73
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
—
Detail
Exploit chain
none
Evidence (decisive lines)
—
Verdict
REPRODUCED. ipfw3 table id OOB: table_ctx[id] no bounds check, id=0x10000000 -> panic table_create_dispatch+0x45. Root-only (raw socket).
No comments yet.