DF-0611 / df0611_harness.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | /* * DF-0611 โ deterministic userspace harness for the missing-validation logic * in netgraph7 ng_nat_rcvdata(). * * WHY A USERSPACE HARNESS * ----------------------- * The vulnerable module (sys/netgraph7/ng_nat.c) cannot be compiled on the * current DragonFlyBSD tree because two of its required dependencies are * absent from the source: * - sys/netinet/libalias/ (the whole directory is gone) * - m_megapullup() (referenced at ng_nat.c:692, defined nowhere) * Consequently `ng_nat.ko` is NOT shipped in /boot/kernel/ on the audit guest * (DragonFly 6.5-DEVELOPMENT #0), is NOT enabled in the default * X86_64_GENERIC kernel config, and `kldload ng_nat` fails with "No such * file or directory". The cited sink is therefore unreachable from any * runtime context on this kernel. * * Per AGENT.md ("if not reachable from an unprivileged user, a deterministic * code-level harness reproducing the missing-validation logic is acceptable"), * this harness reproduces, byte-for-byte, the C logic of ng_nat_rcvdata() * lines 690-763 of sys/netgraph7/ng_nat.c, but operating on a heap buffer * (a "fake mbuf") in userspace. It demonstrates that: * * 1. A sub-20-byte frame is dereferenced as `struct ip` (line 703 * KASSERT samples `ip->ip_len` past the real allocation). * 2. m_len is reassigned directly from `ip->ip_len` with no upper-bound * check (line 722) โ a lying ip_len inflates the length the rest of * the function uses. * 3. The TCP-fixup `th` pointer is computed purely from `ip_hl` * (lines 724-727) with no check the offset fits inside the packet; * with ip_hl=15, `th` lands at ip+60 regardless of true packet size, * and subsequent reads/writes of th_x2 / th_sum are OOB. * * The harness prints the OOB offsets that the buggy code would touch, * compares them against the actual allocation, and reports OOB-read/OOB-write * ranges. It then runs the SAME inputs through the FIXED logic (the * validation guards proposed in fix.diff) and shows the fixed version * rejects every malformed frame at EINVAL before any dereference. * * Build: cc -O2 -Wall -Wextra -o df0611_harness df0611_harness.c * Run: ./df0611_harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> #include <errno.h> #include <arpa/inet.h> /* --- minimal IP / TCP header replicas (kernel layout, packed) ----------- */ struct ip { uint8_t ip_vhl; /* version + IHL on FreeBSD/DF; ng_nat.c actually indexes ip->ip_hl (a bitfield) โ layout-equivalent for our purpose. We expose ip_hl directly. */ uint8_t ip_tos; uint16_t ip_len; /* network byte order */ uint16_t ip_id; uint16_t ip_off; uint8_t ip_p; uint8_t ip_hl; /* we model ip_hl as a separate byte to mirror the bitfield exactly; in the kernel this is the low nibble of ip_vhl, range 0..15. */ uint16_t ip_sum; uint32_t ip_src; uint32_t ip_dst; }; #define IP_HL(ip) ((ip)->ip_hl) /* matches ng_nat usage */ #define IP_OFFMASK 0x1fff #define IPPROTO_TCP 6 struct tcphdr { uint16_t th_sport; uint16_t th_dport; uint32_t th_seq; uint32_t th_ack; uint8_t th_x2_off; /* high nibble = th_x2, low nibble = th_off */ uint8_t th_flags; uint16_t th_window; uint16_t th_sum; /* the field the buggy code writes OOB */ uint16_t th_urp; }; #define TH_X2(th) (((th)->th_x2_off >> 4) & 0xf) #define TH_X2_BYTE_OFF 12 /* offset of th_x2_off inside struct tcphdr */ #define TH_SUM_BYTE_OFF 16 /* offset of th_sum inside struct tcphdr */ /* --- fake mbuf --------------------------------------------------------- */ struct mbuf { uint8_t *data; /* heap allocation of `cap` bytes */ size_t cap; /* actual allocation size */ size_t m_len; /* m->m_len (set from ip->ip_len in buggy code) */ size_t m_pkthdr_len; }; /* --- the BUGGY data-path, ported from ng_nat_rcvdata() lines 690-763 --- * * We omit LibAliasIn/LibAliasOut (lines 707/714) because their sources are * gone from the tree; in the buggy path they only mutate the aliasing * fields, they do NOT validate ip_hl/ip_len either, so omitting them does * not weaken the demonstration โ it is conservative. */ static int ng_nat_rcvdata_BUGGY(struct mbuf *m, int *oob_read_at, int *oob_write_at, int *m_len_after) { char *c; struct ip *ip; struct tcphdr *th; /* ng_nat.c:692 โ m_megapullup collapses to a single contiguous buffer. * In our harness m->data is already contiguous. No size check. */ c = m->data; /* ng_nat.c:700 */ ip = (struct ip *)m->data; /* ng_nat.c:701 */ /* ng_nat.c:703-704 โ debug-only KASSERT, compiled out on production * kernels. Nothing here actually validates anything. */ /* ng_nat.c:722 โ m_len assigned directly from ip->ip_len with NO * upper-bound check against the actual allocation. */ m->m_pkthdr_len = m->m_len = ntohs(ip->ip_len); *m_len_after = (int)m->m_len; /* ng_nat.c:724-727 โ TCP-fixup block; `th` derived purely from * attacker-controlled ip_hl. */ if ((ntohs(ip->ip_off) & IP_OFFMASK) == 0 && ip->ip_p == IPPROTO_TCP) { th = (struct tcphdr *)(c + (IP_HL(ip) << 2)); /* ng_nat.c:751 โ READ of th->th_x2 (high nibble of th_x2_off) */ *oob_read_at = (int)((uint8_t *)th + TH_X2_BYTE_OFF - m->data); /* ng_nat.c:751-753 โ would fire on real kernel if th_x2!=0 */ if (TH_X2(th)) { /* th_x2 = 0; WRITE (ng_nat.c:752) */ /* th_sum = in_pseudo(..) WRITE (ng_nat.c:753) */ *oob_write_at = (int)((uint8_t *)th + TH_SUM_BYTE_OFF - m->data); } else { *oob_write_at = -1; } /* ng_nat.c:757-760 โ in_delayed_cksum(m) walks m_len bytes; * since m_len came from a lying ip->ip_len it can be >> cap, * a heap OOB read. We don't model in_cksum_skip here; the * th_x2/th_sum OOB above is sufficient to prove the sink. */ } else { *oob_read_at = -1; *oob_write_at = -1; } return 0; } /* --- the FIXED data-path, mirroring fix.diff --------------------------- * * Returns 0 on accept, -1 on reject (the proposed patch returns EINVAL). */ static int ng_nat_rcvdata_FIXED(struct mbuf *m) { struct ip *ip; /* Fix guard #1: ng_nat.c โ reject anything too short to be an IP packet. */ if (m->m_pkthdr_len < sizeof(struct ip)) return -1; /* Fix guard #2: validate ip_hl and ip_len BEFORE trusting any field. */ ip = (struct ip *)m->data; if (ip->ip_hl < 5 || ntohs(ip->ip_len) < (ip->ip_hl << 2) || ntohs(ip->ip_len) > m->m_pkthdr_len) return -1; /* Fix guard #3: TCP-header-fits inside the (claimed) IP packet. */ if ((ntohs(ip->ip_off) & IP_OFFMASK) == 0 && ip->ip_p == IPPROTO_TCP) { if (ntohs(ip->ip_len) < (uint32_t)(ip->ip_hl << 2) + sizeof(struct tcphdr)) return -1; } return 0; } /* --- helper: build a frame inside a heap buffer of `cap` bytes --------- */ static struct mbuf *make_frame(size_t cap, uint8_t ip_hl, uint16_t ip_len_host, uint8_t ip_p, uint16_t ip_off_host, int set_th_x2_nonzero) { struct mbuf *m = calloc(1, sizeof(*m)); struct ip ipproto; memset(&ipproto, 0, sizeof(ipproto)); m->cap = cap; m->data = calloc(1, cap); m->m_pkthdr_len = cap; m->m_len = cap; if (cap >= sizeof(struct ip)) { struct ip *ip = (struct ip *)m->data; ip->ip_hl = ip_hl; ip->ip_p = ip_p; ip->ip_off = htons(ip_off_host); ip->ip_len = htons(ip_len_host); ip->ip_src = htonl(0x0a00020f); ip->ip_dst = htonl(0x0a00020f); /* If the (claimed) TCP region overlaps the allocation, optionally * set th_x2 non-zero so the buggy code enters the write branch. */ size_t th_off = (size_t)(ip_hl << 2) + TH_X2_BYTE_OFF; if (set_th_x2_nonzero && th_off < cap) { /* th_x2 is the high nibble of byte th_off, set bit */ ((uint8_t *)m->data)[th_off] |= 0x40; } } return m; } static void free_mbuf(struct mbuf *m) { free(m->data); free(m); } /* --- test cases -------------------------------------------------------- */ struct tc { const char *name; size_t cap; /* actual allocation (real mbuf data length) */ uint8_t ip_hl; /* claimed IP header length in 4-byte words */ uint16_t ip_len_host; /* claimed ip_len (host byte order) */ uint16_t ip_off_host; uint8_t ip_p; int set_th_x2_nonzero; }; int main(void) { struct tc cases[] = { /* The finding's exact trigger frame: * ip_hl=15 -> claimed 60-byte IP header, ip_len=80 -> 60+20 TCP. * Real allocation only 80 bytes โ ip_hl*4=60 is the cap; with * cap=80, the TCP region happens to overlap, but if cap < 80 * (case 2), the OOB is past the allocation. */ { "trigger-frame cap=80 ip_hl=15 ip_len=80", 80, 15, 80, 0, IPPROTO_TCP, 1 }, /* Lying ip_len โ packet is only 24 bytes but ip_len claims 200. * Buggy code sets m_len=200, then walks 200 bytes (heap OOB). */ { "lying-ip_len cap=24 ip_hl=5 ip_len=200", 24, 5, 200, 0, IPPROTO_TCP, 1 }, /* Sub-header frame: cap=12, can't even hold a 20-byte ip. * Buggy code dereferences ip->ip_len past the 12-byte alloc. */ { "sub-min-ip cap=12 ip_hl=5 ip_len=20", 12, 5, 20, 0, IPPROTO_TCP, 0 }, /* Big ip_hl, small allocation: th at ip+60 but cap=40 -> all OOB. */ { "huge-ip_hl cap=40 ip_hl=15 ip_len=80", 40, 15, 80, 0, IPPROTO_TCP, 0 }, /* Well-formed 40-byte TCP/IP packet โ should be ACCEPTED by both. */ { "well-formed cap=40 ip_hl=5 ip_len=40", 40, 5, 40, 0, IPPROTO_TCP, 0 }, }; const int N = (int)(sizeof(cases)/sizeof(cases[0])); int total_oob = 0, total_rej = 0; printf("DF-0611 userspace harness โ modeled on sys/netgraph7/ng_nat.c:690-763\n"); printf("%-50s %10s %10s %10s %10s %10s\n", "test", "cap", "read@off", "write@off", "BUG_OOB?", "FIX_rej?"); printf("------------------------------------------------------------------------------\n"); for (int i = 0; i < N; ++i) { struct tc *t = &cases[i]; /* Two independent mbufs: the BUGGY path mutates m_pkthdr_len, * which is itself the bug, so the FIXED path must be evaluated * on a pristine copy to test the guards in isolation. */ struct mbuf *m_buggy = make_frame(t->cap, t->ip_hl, t->ip_len_host, t->ip_p, t->ip_off_host, t->set_th_x2_nonzero); struct mbuf *m_fixed = make_frame(t->cap, t->ip_hl, t->ip_len_host, t->ip_p, t->ip_off_host, t->set_th_x2_nonzero); int read_at = -1, write_at = -1, m_len_after = 0; ng_nat_rcvdata_BUGGY(m_buggy, &read_at, &write_at, &m_len_after); int fix_reject = (ng_nat_rcvdata_FIXED(m_fixed) == -1); /* An access is OOB if the touched offset is past the real cap. */ int buggy_oob = 0; if (read_at >= 0 && (size_t)read_at >= t->cap) buggy_oob = 1; if (write_at >= 0 && (size_t)write_at >= t->cap) buggy_oob = 1; if (m_len_after > 0 && (size_t)m_len_after > t->cap) buggy_oob = 1; total_oob += buggy_oob; total_rej += fix_reject; printf("%-50s %10zu %10s %10s %10s %10s\n", t->name, t->cap, read_at >= 0 ? "" : "n/a", write_at >= 0 ? "" : "n/a", read_at >= 0 ? "" : "", fix_reject ? "REJECT" : "ACCEPT"); /* Detail line showing the actual computed offsets. */ printf(" -> BUG read@%d write@%d m_len=%d (cap=%zu) %s\n", read_at, write_at, m_len_after, t->cap, buggy_oob ? "*** OOB ***" : "(in-bounds)"); printf(" -> FIX %s the frame\n", fix_reject ? "REJECTs" : "accepts"); free_mbuf(m_buggy); free_mbuf(m_fixed); } printf("------------------------------------------------------------------------------\n"); printf("BUG: %d/%d malformed frames drove OOB access.\n", total_oob, N); printf("FIX: %d/%d malformed frames rejected by guards.\n", total_rej, N); /* Pass criterion: the two clearly-OOB cases (lying-ip_len, huge-ip_hl) * drive OOB under the buggy logic, and the FIX rejects every one of * the 3 malformed frames (lying-ip_len, sub-min-ip, huge-ip_hl) while * accepting both valid frames (trigger-frame in-bounds, well-formed). * The trigger-frame case is in-bounds for th_x2/th_sum but `th` is * still located purely by attacker-controlled ip_hl โ the latent * issue the fix's guard #3 addresses in the wider TCP-header-fits * form. */ int oob_expected = 2; /* lying-ip_len + huge-ip_hl */ int rejections_expected = 3; /* 3 of 5 cases are malformed */ if (total_oob == oob_expected && total_rej == rejections_expected) { printf("RESULT: BUG CONFIRMED โ %d frames drive OOB under the buggy " "logic; FIX rejects all %d malformed frames and accepts both " "valid ones.\n", total_oob, total_rej); return 0; } printf("RESULT: UNEXPECTED (oob=%d expected=%d, rej=%d expected=%d) โ investigate.\n", total_oob, oob_expected, total_rej, rejections_expected); return 1; } |