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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0570 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md verdict full mechanism + before/after evidence
↓ download 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 :335 faults (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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via module hot-swap + diagnostic before/after.

BEFORE: OOB idx 64511-65535 s2=0. AFTER: BOUNDS-DENIED 0 OOB reads.
↓ fix.diff6.5-DEVELOPMENT #0 (module hot-swap ipfw3_nat.ko)

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.