DF-0861 / 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 | /* * DF-0861 — Deterministic harness for the unbounded code-page inner-loop * heap OOB WRITE in hpfs_cpinit() * (sys/vfs/hpfs/hpfs_subr.c:274,276,282,292,297 + bcopy :230). * * This is a FAITHFUL userspace transcription of the buggy mount-time loop * against the EXACT on-disk struct layouts from sys/vfs/hpfs/hpfs.h: * * struct cpdblk = 136 bytes (b_country u16, b_cpid u16, b_dbcscnt u16, * b_upcase[0x80]=128, b_dbcsrange u16) * struct cpiblk = 16 bytes * struct cpisec = 512 bytes (s_magic u32, s_cpicnt u32, s_cpifirst u32, * s_next lsn_t, s_cpi[0x1F]=31*16=496) * struct cpdsec = 434 bytes (d_magic u32, d_cpcnt u16, d_cpfirst u16, * d_checksum[3], d_offset[3], d_cpdblk[3]) * * Buggy kernel logic transcribed verbatim: * * cpicnt = sp_cpinum; // :274 * hpm_cpdblk = kmalloc(cpicnt * sizeof(struct cpdblk)); // :276 * cpdbp = hpm_cpdblk; * while (cpicnt > 0) { // :282 * cpisp = (struct cpisec *)bread(lsn)->b_data; * cpibp = cpisp->s_cpi; * for (i=0; i < cpisp->s_cpicnt; // :292 * i++, cpicnt--, cpdbp++, cpibp++) { * hpfs_cpload(cpibp, cpdbp); // :297 * // hpfs_cpload: bcopy(cpdsp->d_cpdblk[k], cpdbp, 136) // :230 * } * } * * The OUTER while() decrements `cpicnt` (total), but the INNER for() is * bounded by `cpisp->s_cpicnt` (per-sector) and runs to completion even * after `cpicnt` has gone <= 0. `cpdbp` is advanced on every inner iter, * so a sector advertising s_cpicnt > sp_cpinum writes 136 B * (s_cpicnt - * sp_cpinum) past the kmalloc'd hpm_cpdblk array. * * Poisoned allocator: hpm_cpdblk is placed at the END of the first mmap'd * page; the next page(s) are poisoned with 0xAA. Any byte written past the * array lands in the poison region, modelling the slab neighbourhood the * attacker does not own. * * Compile: cc -O2 -o harness harness.c * Run: ./harness * * Expected (BUG PRESENT, sp_cpinum=1, s_cpicnt=0x1F): * inner loop ran 31 iters ; in-bounds writes=1 ; OOB writes=30 * OOB write = 30 * 136 = 4080 bytes past the 136-byte hpm_cpdblk array * poison HIT * DF_0861_BUG_CONFIRMED=1 DF_0861_BUG_OOB_WRITE_BYTES=4080 * * Expected (FIXED): inner loop clamped so cpicnt never goes < 0; OR sp_cpinum * rejected as EINVAL -> 0 OOB writes, no poison hit. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <sys/mman.h> #include <unistd.h> /* ---- Exact struct layouts from sys/vfs/hpfs/hpfs.h --------------------- */ typedef struct cpdblk { uint16_t b_country; uint16_t b_cpid; uint16_t b_dbcscnt; uint8_t b_upcase[0x80]; uint16_t b_dbcsrange; } cpdblk_t; /* sizeof = 136 */ typedef struct cpiblk { uint16_t b_country; uint16_t b_cpid; uint32_t b_checksum; uint32_t b_cpdsec; /* lsn_t */ uint16_t b_vcpid; uint16_t b_dbcscnt; } cpiblk_t; /* sizeof = 16 */ typedef struct cpisec { uint32_t s_magic; uint32_t s_cpicnt; /* <-- attacker-controlled, UNVALIDATED */ uint32_t s_cpifirst; uint32_t s_next; cpiblk_t s_cpi[0x1F]; /* 31 entries */ } cpisec_t; /* sizeof = 512 */ typedef struct cpdsec { uint32_t d_magic; uint16_t d_cpcnt; uint16_t d_cpfirst; uint32_t d_checksum[3]; uint16_t d_offset[3]; cpdblk_t d_cpdblk[3]; } cpdsec_t; #define CPDB_SIZE (sizeof(cpdblk_t)) /* 136 */ #define CPIB_SIZE (sizeof(cpiblk_t)) /* 16 */ #define CPIS_MAX 0x1F /* 31 cpiblk per sector */ /* ---- Poisoned allocator ----------------------------------------------- * * Place the (cpicnt*136)-byte hpm_cpdblk array at the very end of the first * page; the following pages are poisoned 0xAA. Any OOB write lands in the * poison region, exactly as it would land in a slab neighbour in the kernel. */ static char * poison_alloc_array(size_t need_bytes, size_t pagesz, uint8_t poison) { /* need at least 2 pages so there is always a poison guard ahead */ size_t npg = 2; while ((npg - 1) * pagesz < need_bytes) npg++; size_t total = npg * pagesz; char *m = mmap(NULL, total, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); if (m == MAP_FAILED) { perror("mmap"); exit(2); } /* poison everything except the last `need_bytes` of the first page-block */ char *array = m + (npg - 1) * pagesz - need_bytes; /* poison from m up to array (within first page) and all later pages */ memset(m, poison, (size_t)(array - m)); memset(array + need_bytes, poison, total - (size_t)(array + need_bytes - m)); return array; } /* ---- Build a forged cpisec sector ------------------------------------- */ static void forge_cpisec(cpisec_t *cs, uint32_t scpicnt, uint32_t cpdsec_lsn) { memset(cs, 0, sizeof(*cs)); cs->s_magic = 0x494521F7; cs->s_cpicnt = scpicnt; cs->s_cpifirst = 0; cs->s_next = 0; uint32_t n = scpicnt > CPIS_MAX ? CPIS_MAX : scpicnt; for (uint32_t k = 0; k < n; k++) { cs->s_cpi[k].b_country = 1; cs->s_cpi[k].b_cpid = 1; /* matches cpdsec */ cs->s_cpi[k].b_cpdsec = cpdsec_lsn; cs->s_cpi[k].b_vcpid = 1; } } /* ---- Build a forged cpdsec sector with attacker content --------------- */ static void forge_cpdsec(cpdsec_t *ds) { memset(ds, 0, sizeof(*ds)); ds->d_magic = 0x894521F7; ds->d_cpcnt = 1; ds->d_cpfirst = 0; ds->d_cpdblk[0].b_country = 1; ds->d_cpdblk[0].b_cpid = 1; /* matches cpiblk */ /* attacker marker in b_upcase so OOB bytes are unmistakable */ for (int j = 0; j < 0x80; j++) ds->d_cpdblk[0].b_upcase[j] = (uint8_t)(0x41 + j); } /* ---- Transcription of hpfs_cpload bcopy (subr.c:228-236) ------------- * * Walks d_cpfirst..d_cpcnt looking for matching b_cpid; on match, bcopies * 136 bytes from d_cpdblk[k] into cpdbp. Returns 1 if a bcopy happened. */ static int cpload_bcopy(const cpdsec_t *ds, const cpiblk_t *cb, cpdblk_t *cpdbp, char *buf_end, int *poison_hit) { for (uint16_t i = ds->d_cpfirst; i < ds->d_cpcnt; i++) { if (ds->d_cpdblk[i].b_cpid == cb->b_cpid) { char *dst = (char *)cpdbp; for (size_t b = 0; b < CPDB_SIZE; b++) { if (dst + b >= buf_end && dst[b] == 0xAA) *poison_hit = 1; dst[b] = ((char *)&ds->d_cpdblk[i])[b]; } return 1; } } return 0; /* no match -> ENOENT (kernel returns, mount aborts) */ } /* ---- Transcription of the buggy hpfs_cpinit loop (subr.c:282-305) ---- * * FIXED=0 reproduces the kernel bug exactly; FIXED=1 clamps the inner loop * so it never advances cpdbp past the array (the proposed fix). */ static void run_cpinit(char *hpm_cpdblk, int cpicnt, const cpisec_t *cs, const cpdsec_t *ds, int fixed, uint32_t *inbounds_out, uint32_t *oob_iters_out, size_t *oob_bytes_out, int *poison_hit, char *buf_end) { cpdblk_t *cpdbp = (cpdblk_t *)hpm_cpdblk; cpiblk_t *cpibp = cs->s_cpi; char *array_end = hpm_cpdblk + (size_t)cpicnt * CPDB_SIZE; uint32_t inb = 0, oob = 0; int i; int local_cpicnt = cpicnt; for (i = 0; i < (int)cs->s_cpicnt; i++, local_cpicnt--, cpdbp++, cpibp++) { /* FIXED: stop once we've exhausted the legitimately-allocated slots */ if (fixed && cpdbp >= (cpdblk_t *)array_end) break; int wrote = cpload_bcopy(ds, cpibp, cpdbp, buf_end, poison_hit); if (!wrote) { /* kernel returns ENOENT and aborts mount; emulate by stopping */ break; } if ((char *)cpdbp >= array_end) oob++; else inb++; } (void)local_cpicnt; *inbounds_out = inb; *oob_iters_out = oob; *oob_bytes_out = (size_t)oob * CPDB_SIZE; } int main(void) { size_t pagesz = sysconf(_SC_PAGESIZE); /* ---- struct-size sanity (must match the kernel layout exactly) ---- */ printf("=== DF-0861 deterministic OOB-WRITE proof ===\n"); printf("struct sizes: cpdblk=%zu cpiblk=%zu cpisec=%zu (CPIS_MAX=%d)\n", sizeof(cpdblk_t), sizeof(cpiblk_t), sizeof(cpisec_t), CPIS_MAX); if (sizeof(cpdblk_t) != 136 || sizeof(cpiblk_t) != 16 || sizeof(cpisec_t) != 512) { printf("ERROR: struct size mismatch with hpfs.h layout\n"); return 2; } printf("\n"); /* Forged values (same as the crafted image): sp_cpinum=1, s_cpicnt=0x1F */ uint32_t sp_cpinum = 1; uint32_t s_cpicnt = 0x1F; /* 31 */ cpisec_t cs; forge_cpisec(&cs, s_cpicnt, /*cpdsec_lsn=*/0x70); cpdsec_t ds; forge_cpdsec(&ds); int cpicnt = (int)sp_cpinum; size_t alloc_bytes = (size_t)cpicnt * CPDB_SIZE; /* 1*136 = 136 */ printf("--- BUG MODE (unpatched hpfs_cpinit) ---\n"); printf(" sp_cpinum=%u -> cpicnt=%d -> kmalloc(hpm_cpdblk)=%zu bytes\n", sp_cpinum, cpicnt, alloc_bytes); printf(" cpisec.s_cpicnt=%u -> inner for() runs %u times\n", s_cpicnt, s_cpicnt); char *arr = poison_alloc_array(alloc_bytes, pagesz, 0xAA); char *buf_end = arr + alloc_bytes; /* end of the legitimate array */ /* but the poison region extends well beyond; give cpload a generous * "buffer end" = far past, so the per-byte poison check inside cpload * fires on the 0xAA guard rather than just the array end. */ char *far_end = arr + 16 * pagesz; uint32_t inb = 0, oob = 0; size_t oob_bytes = 0; int poison = 0; run_cpinit(arr, cpicnt, &cs, &ds, /*fixed=*/0, &inb, &oob, &oob_bytes, &poison, far_end); printf(" result: in-bounds writes=%u OOB writes=%u OOB bytes=%zu poison=%s\n", inb, oob, oob_bytes, poison ? "HIT" : "no"); printf(" DF_0861_BUG_OOB_WRITE_BYTES=%zu\n", oob_bytes); printf(" DF_0861_BUG_CONFIRMED=%d\n\n", oob ? 1 : 0); /* ---- Verify OOB content is attacker-controlled (marker 0x41..) ---- */ int content_ok = 0; if (oob) { /* first OOB slot = arr + CPDB_SIZE; check b_upcase[0..3] */ char *first_oob = arr + CPDB_SIZE; if ((uint8_t)first_oob[CPDB_SIZE - 136 + 6] == 0x41) content_ok = 1; /* simpler: byte at arr+136+6 (b_upcase[0] of first OOB cpdblk) */ uint8_t up0 = (uint8_t)arr[CPDB_SIZE + 6]; uint8_t up1 = (uint8_t)arr[CPDB_SIZE + 7]; printf(" first-OOB cpdblk b_upcase[0..1] = 0x%02x 0x%02x (marker 0x41 0x42) -> %s\n", up0, up1, (up0==0x41 && up1==0x42) ? "ATTACKER-CONTROLLED" : "not-marker"); if (up0 == 0x41 && up1 == 0x42) content_ok = 1; } /* ---- FIXED MODE: inner loop clamped to the allocated slots -------- */ printf("--- FIXED MODE (inner loop bounded by allocated slots) ---\n"); char *arr2 = poison_alloc_array(alloc_bytes, pagesz, 0xAA); uint32_t inb2 = 0, oob2 = 0; size_t oob_bytes2 = 0; int poison2 = 0; run_cpinit(arr2, cpicnt, &cs, &ds, /*fixed=*/1, &inb2, &oob2, &oob_bytes2, &poison2, far_end); printf(" result: in-bounds writes=%u OOB writes=%u OOB bytes=%zu poison=%s\n", inb2, oob2, oob_bytes2, poison2 ? "HIT" : "no"); printf(" DF_0861_FIX_OOB_WRITE_BYTES=%zu\n", oob_bytes2); printf(" DF_0861_FIX_REJECTS_OVERFLOW=%d\n\n", oob2 ? 0 : 1); printf("=== SUMMARY ===\n"); printf("DF_0861_BUG_CONFIRMED=%d\n", oob ? 1 : 0); printf("DF_0861_BUG_OOB_WRITE_BYTES=%zu\n", oob_bytes); printf("DF_0861_BUG_OOB_ATTACKER_CONTROLLED=%d\n", content_ok); printf("DF_0861_FIX_REJECTS_OVERFLOW=%d\n", oob2 ? 0 : 1); return (oob && oob_bytes2 == 0) ? 0 : 1; } |