Remote OOB read: inbound port/icmp_id indexed into alias arrays without bounds check
Summary
Inbound path(:204/:209/:215): s2=alias->tcp_in[*old_port-ALIAS_BEGIN] / udp_in / icmp_in[*old_port]. *old_port is raw network-order dport/icmp_id from inbound packet. NO bounds check. TCP/UDP: dport<1024(network order interpretation) -> negative index before array. Values producing index>=64511 read past end. ICMP: icmp_id>=64511 read past icmp_in. s2==NULL check(:221) AFTER OOB read. Non-NULL garbage in OOB slot -> wild pointer deref at :335-341 (s2->alias_addr/src_addr). Remote attacker sends crafted packet to NAT alias IP. Fix: validate ntohs(old_port) bounds before indexing.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0570 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0570_oob_trigger.c | trigger-source | raw ICMP echo sender, attacker-chosen icmp_id sweep | 3.4 KB | view raw |
| fix.diff | suggested-fix | ntohs + bounds check at inbound read sites :204/:209/:215 | 1.4 KB | view raw |
| build.sh | build-script | cc -O2 build of the trigger | 444 B | view raw |
| run.sh | run-script | NAT setup + inbound ICMP OOB sweep | 1.6 KB | view raw |
| VERDICT.md | verdict | full mechanism + before/after evidence | 6.6 KB | β raw |
| diag_dmesg_icmp.txt | run-log | BEFORE: diagnostic module proving OOB read executes (idx 64511..65535) | 558 B | view raw |
| fix_dmesg.txt | run-log | AFTER: fixed module showing BOUNDS-DENIED x1025, 0 OOB reads | 460 B | view raw |
| env.txt | environment | uname, cc, ncpus | 309 B | view 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-0570 β VERDICT
Verdict: REPRODUCED + FIX VALIDATED
The inbound-path OOB read is real, reachable by a remote unauthenticated attacker, and proven at runtime by an instrumented module that prints the out-of-bounds array index the kernel actually computes from the attacker's packet. The fix (host-order conversion + bounds validation before indexing) eliminates every OOB access.
This is a pure out-of-bounds READ primitive. By Phase-6 rules a read-only
primitive has no escalation chain to uid=0; the impact ceiling is remote
kernel-heap information disclosure and remote DoS (panic).
Root-cause mechanism (line-by-line)
The vulnerable inbound path (ip_fw3_nat, args->oif == NULL)
// sys/net/ipfw3_nat/ip_fw3_nat.c:190 (inbound packet branch)
if (args->oif == NULL) {
old_addr = &ip->ip_dst;
...
LIST_FOREACH(alias, &nat->alias, next)
if (alias->ip.s_addr == ntohl(args->f_id.dst_ip)) break; // alias IP match
switch (ip->ip_p) {
case IPPROTO_TCP:
old_port = &L3HDR(struct tcphdr, ip)->th_dport; // :203 network order
s2 = alias->tcp_in[*old_port - ALIAS_BEGIN]; // :204 *** OOB ***
case IPPROTO_UDP:
old_port = &L3HDR(struct udphdr, ip)->uh_dport; // :208
s2 = alias->udp_in[*old_port - ALIAS_BEGIN]; // :209 *** OOB ***
case IPPROTO_ICMP:
old_port = &L3HDR(struct icmp, ip)->icmp_id; // :214
s2 = alias->icmp_in[*old_port]; // :215 *** OOB, no -ALIAS_BEGIN ***
}
if (s2 == NULL) goto oops; // :221 guard is AFTER the read
}
old_port points at the raw network-order port/icmp_id bytes from the
inbound packet. It is used directly as the array index with no
ntohs() and no bounds check. The s2 == NULL guard at :221 runs
after* the OOB access, so it cannot prevent it.
tcp_in[]/udp_in[] have ALIAS_RANGE (64511) entries indexed from
ALIAS_BEGIN (1024); icmp_in[] has 64511 entries indexed from 0. An
attacker who can deliver a packet to the NAT alias IP controls the index
fully:
| proto | index expression | attacker-reachable range | OOB? |
|---|---|---|---|
| TCP | *old_port - 1024 |
[-1024 .. 64511] | yes |
| UDP | *old_port - 1024 |
[-1024 .. 64511] | yes |
| ICMP | *old_port |
[0 .. 65535] (array 64511) | yes |
Why the byte-swap matters
*old_port is read as a little-endian uint16_t from the network-order
field. An attacker sending on-wire destination port P produces
*old_port = bswap(P). So the attacker controls the index through both the
port value AND the byte order, making the OOB index fully attacker-chosen.
Runtime evidence (instrumented module β the smoking gun)
Because DragonFly's kernel direct-maps all physical RAM, an OOB read of
adjacent (mapped) pages does not fault β so a plain trigger produces no
crash and the externally-observable behaviour (packet dropped) is identical
with/without the bug. To prove the OOB access actually executes with an
attacker-controlled index, a diagnostic build adds a kprintf right after
each inbound array read that prints the computed index and the value
returned whenever the index is out of bounds.
Before (unpatched module) β OOB reads execute
DF0570 icmp OOB idx=65531 s2=0 DF0570 icmp OOB idx=64764 s2=0 DF0570 icmp OOB idx=65020 s2=0 DF0570 icmp OOB idx=65276 s2=0 DF0570 icmp OOB idx=65532 s2=0 ... (idx ranges over the attacker-chosen 64511..65535 set)
The inbound ICMP path executes alias->icmp_in[idx] for idx in
[64511..65535] β every one is past the 64511-element array. The read
returned 0 (NULL) here because the adjacent heap pages were zeroed
(fresh cfg_alias, M_ZERO); with non-zero residue the non-NULL garbage
would be dereferenced at :335 (s2->alias_addr) and either leak kernel
memory or panic (see DF-0569's cleanup-callout crash for the non-NULL case:
nat_cleanup_func_dispatch dereferenced s2=0x1af).
Reachability is also confirmed by the firewall counter: 1027 inbound ICMP
packets with OOB icmp_id matched the nat 1 icmp ... in rule and entered
ip_fw3_nat.
After (fixed module) β bounds check denies before the read
DF0570FIX icmp BOUNDS-DENIED in_port=64511 DF0570FIX icmp BOUNDS-DENIED in_port=64512 DF0570FIX icmp BOUNDS-DENIED in_port=64513 ... TOTAL BOUNDS-DENIED: 1025 (one per OOB packet) new OOB reads: 0
The fix converts to host order, validates ALIAS_BEGIN <= port <
ALIAS_BEGIN+ALIAS_RANGE (ICMP: port < ALIAS_RANGE), and goto oops
before the array access. No OOB read executes.
Exploit chain / impact ceiling
The primitive is a pure out-of-bounds READ of kernel heap, attacker-
controlled index, triggered by an unauthenticated remote packet to the
NAT alias IP. Per Phase-6 rules a read-only primitive has no escalation
chain to uid=0 (valid hard blocker). Its realistic ceiling is:
- Remote kernel-heap information disclosure β when the OOB slot holds
non-NULL heap residue,
s2->alias_addr/s2->src_addr(:335-339) read kernel addresses and the resulting packet is rewritten/delivered carrying that data. - Remote DoS (panic) β when the OOB slot holds a non-canonical /
unmapped pointer, the deref at
:335faults (the DF-0569 cleanup crash demonstrates this non-NULL-garbage path).
On this clean-heap, kmem-direct-map guest the OOB slots read NULL, so the observed effect this run is silent packet drop (no bytes extracted, no crash) β but the primitive is proven and the leak/DoS follow from non-NULL heap residue, which a real NAT under load readily provides.
PoC changes
Authored the entire evidence pack from scratch:
* df0570_oob_trigger.c β raw ICMP echo sender with attacker-chosen
icmp_id sweep [64511..65535] (runs as root on the box only to simulate
the unauthenticated remote attacker; raw ICMP sockets need local
privilege, but the exploit itself is unprivileged from the attacker's
side).
* fix.diff β adds ntohs() + bounds validation at :204/:209/:215.
* diag_dmesg_icmp.txt / fix_dmesg.txt β before/after instrumented output.
Fix
fix.diff introduces a host-order in_port and bounds-checks it before
each inbound array index, goto oops-ing on any OOB value. It supersedes
the DF-0569 read-side ntohs() changes at the same lines (:204/:209/:215)
because it adds the bounds check DF-0569's fix lacks.
Fix verification
fixedVALIDATED via module hot-swap + diagnostic before/after.
BEFORE: OOB idx 64511-65535 s2=0. AFTER: BOUNDS-DENIED 0 OOB reads.
Confirmed kernel references
Detail
Exploit chain
none -- root/KLD module or read-only or concurrency. See notes.
Evidence (decisive lines)
BEFORE: OOB idx 64511-65535 s2=0. AFTER: BOUNDS-DENIED 0 OOB reads.
PoC changes
Various PoCs + fix.diff + VERDICT.md + manifest.json per finding.
Verified recommended fix
ntohs + bounds check at inbound read sites :204/:209/:215. goto-oops on OOB before read. Full diff in findings/poc/DF-0570/fix.diff.
Verdict
REPRODUCED. Inbound port/icmp_id indexed into alias arrays without ntohs/bounds check at :204/:209/:215. Remote OOB read proven by diagnostic module: idx 64511-65535 all past array.
No comments yet.