DF-0788 / 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 | /* * harness.c - Deterministic userspace replication of the unvalidated resident * attribute data-offset OOB read in ntfs_attrtontvattr() * (sys/vfs/ntfs/ntfs_subr.c:557-564). * * ROOT CAUSE (confirmed by source trace of sys/vfs/ntfs/ntfs_subr.c): * 554: } else { // resident attribute * 555: vap->va_compressalg = 0; * 557: vap->va_datalen = rap->a_r.a_datalen; // u16 from disk, NO bound * 558: vap->va_allocated = rap->a_r.a_datalen; * 561: vap->va_datap = kmalloc(vap->va_datalen, M_NTFSRDATA, M_WAITOK); * 563: memcpy(vap->va_datap, * 564: (caddr_t) rap + rap->a_r.a_dataoff, // <-- a_dataoff u16 UNCHECKED * rap->a_r.a_datalen); // <-- a_datalen u16 UNCHECKED * * rap is a struct attr * pointing into mfrp, the MFT record buffer: * mfrp = kmalloc(ntfs_bntob(ntmp->ntm_bpmftrec), M_TEMP, M_WAITOK); // line 263 * typically 1024 or 4096 bytes. The on-disk u16 a_dataoff/a_datalen are * trusted without any bounds check, so a crafted image with a_dataoff past * the record boundary makes memcpy read past mfrp into adjacent M_TEMP slab * heap. The leaked bytes land in vap->va_datap and are later exposed to * userspace via ntfs_readntvattr_plain():1594 uiomove(vap->va_datap+roff, * rsize, uio). * * WHY A HARNESS * ------------ * The live trigger (mount_ntfs on a crafted image) reaches the vulnerable * memcpy at mount time during ntfs_loadntnode(), but whether the OOB read * manifests as a panic or a silent leak depends on what byte pattern lives * in the adjacent slab chunk (determined by prior kernel allocations). This * harness makes the OOB read DETERMINISTIC by placing a guard page * (PROT_NONE) immediately after the record buffer, so any read past the * buffer end faults (SIGSEGV) — proving the primitive exists regardless of * slab layout. It also demonstrates that the proposed fix (bounding * a_dataoff+a_datalen against the record size) eliminates the OOB read. * * Build: cc -O2 -o harness harness.c * Run: ./harness {clean|oob_dataoff} [apply_fix] * Exit: 0 = memcpy completed within bounds (clean, or fix rejected bad input) * 2 = SIGSEGV caught -> OOB read past record (proof of the bug) */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> /* * Mirror the exact struct layouts from sys/vfs/ntfs/ntfs.h. * * struct attrhdr { // 16 bytes * u_int32_t a_type; * u_int32_t reclen; * u_int8_t a_flag; * u_int8_t a_namelen; * u_int8_t a_nameoff; * u_int8_t reserved1; * u_int8_t a_compression; * u_int8_t reserved2; * u_int16_t a_index; * }; * struct attr { * struct attrhdr a_hdr; * union { * struct { // resident (a_S_r) * u_int16_t a_datalen; * u_int16_t reserved1; * u_int16_t a_dataoff; * u_int16_t a_indexed; * } a_S_r; * ... * } a_S; * }; * #define a_r a_S.a_S_r */ struct attrhdr { uint32_t a_type; uint32_t reclen; uint8_t a_flag; uint8_t a_namelen; uint8_t a_nameoff; uint8_t reserved1; uint8_t a_compression; uint8_t reserved2; uint16_t a_index; } __attribute__((packed)); struct attr_resident { struct attrhdr hdr; uint16_t a_datalen; /* offset 16 */ uint16_t reserved1; /* offset 18 */ uint16_t a_dataoff; /* offset 20 -- THE BUG: unchecked */ uint16_t a_indexed; /* offset 22 */ } __attribute__((packed)); #define REC_BYTES 1024 /* classic MFT record size; also test 4096 */ #define REC_BYTES_BIG 4096 /* matches the sibling image geometry */ #define NTFS_AF_INRUN 0x01 /* ---- signal handling so we can catch the OOB SIGSEGV cleanly ---- */ static sigjmp_buf jb; static volatile int got_sig; static void segv_handler(int sig) { (void)sig; got_sig = SIGSEGV; siglongjmp(jb, 1); } /* * Mirror of ntfs_attrtontvattr() resident path, lines 554-565. * rap = pointer to the attribute inside the MFT record buffer * rec_size = size of the MFT record buffer (mfrp allocation) * rec_base = start of the MFT record buffer (for bounds computation) * * Returns: 0 = memcpy within bounds; 1 = OOB detected by fix (rejected); * exits via SIGSEGV if the buggy path reads past the guard page. */ static int resident_memcpy_buggy(struct attr_resident *rap, uint8_t *rec_base, int rec_size, uint8_t *out, int out_sz) { uint16_t datalen = rap->a_datalen; /* line 557 */ uint16_t dataoff = rap->a_dataoff; /* unchecked */ /* line 561: kmalloc(datalen) -> out (caller-provided) */ int n = (datalen < out_sz) ? datalen : out_sz; /* line 563-564: memcpy(out, rap + dataoff, datalen) -- THE BUG */ memcpy(out, (uint8_t *)rap + dataoff, n); return 0; } /* * The proposed fix: validate that (rap + a_dataoff + a_datalen) does not * exceed the MFT record buffer boundary before the memcpy. */ static int resident_memcpy_fixed(struct attr_resident *rap, uint8_t *rec_base, int rec_size, uint8_t *out, int out_sz) { uint16_t datalen = rap->a_datalen; uint16_t dataoff = rap->a_dataoff; uintptr_t rap_addr = (uintptr_t)rap; uintptr_t rec_end = (uintptr_t)rec_base + rec_size; /* NEW: validate a_dataoff+a_datalen fits within the record buffer */ if (rap_addr + dataoff + datalen > rec_end) return 1; /* EINVAL: reject the malformed attribute */ int n = (datalen < out_sz) ? datalen : out_sz; memcpy(out, (uint8_t *)rap + dataoff, n); return 0; } /* * Build a synthetic resident attribute inside the record buffer. * mode "clean": a_dataoff points to valid data within the record. * mode "oob_dataoff": a_dataoff pushed past the record end so memcpy * reads into the guard page. */ static struct attr_resident * fill_record(uint8_t *rec, int rec_size, const char *mode, int attr_off) { memset(rec, 0xA5, rec_size); /* fill with sentinel (non-zero residue) */ struct attr_resident *rap = (struct attr_resident *)(rec + attr_off); rap->hdr.a_type = 0x80; /* NTFS_A_DATA */ rap->hdr.a_flag = 0; /* resident */ rap->hdr.reclen = 64; if (strcmp(mode, "clean") == 0) { /* legitimate resident attribute: data right after the header */ rap->a_datalen = 32; /* 32 bytes of data */ rap->a_dataoff = 24; /* data starts at hdr+24 (offset 24) */ /* write some known data at rap+24 */ memset((uint8_t *)rap + 24, 'D', 32); } else if (strcmp(mode, "oob_dataoff") == 0) { /* BUG: a_dataoff pushed past the record end. * rap is at attr_off within the record; set dataoff so that * rap + dataoff + datalen exceeds rec_base + rec_size. */ rap->a_datalen = 64; /* push the read target past the buffer end: * rap_addr + dataoff + 64 > rec_base + rec_size * dataoff > rec_size - attr_off - 64 */ rap->a_dataoff = (uint16_t)(rec_size - attr_off); /* start AT the end */ } else { fprintf(stderr, "unknown mode '%s'\n", mode); exit(3); } return rap; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s {clean|oob_dataoff} [apply_fix] [recsize]\n", argv[0]); fprintf(stderr, " recsize: 1024 (default) or 4096\n"); return 3; } const char *mode = argv[1]; int apply_fix = (argc >= 3 && strcmp(argv[2], "apply_fix") == 0); int rec_size = (argc >= 4) ? atoi(argv[3]) : REC_BYTES; if (rec_size != REC_BYTES && rec_size != REC_BYTES_BIG) { fprintf(stderr, "recsize must be 1024 or 4096\n"); return 3; } long pagesz = 4096; size_t mapsz = ((size_t)rec_size + pagesz - 1) & ~((size_t)pagesz - 1); /* Allocate rec_size writable bytes + one guard page right after. */ uint8_t *base = mmap(NULL, mapsz + pagesz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 3; } if (mprotect(base + mapsz, pagesz, PROT_NONE) != 0) { perror("mprotect"); return 3; } /* Place rec at the END of the writable region so the guard page is * immediately after byte rec_size-1 (truest mirror of a slab chunk). */ uint8_t *rec = base + mapsz - rec_size; /* attribute starts at offset 56 within the record (after a typical * FILE header + fixup array), mirroring fr_attroff. */ int attr_off = 56; struct attr_resident *rap = fill_record(rec, rec_size, mode, attr_off); /* install SIGSEGV handler */ struct sigaction sa, oldsa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = segv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGSEGV, &sa, &oldsa); uint8_t out[256]; memset(out, 0, sizeof(out)); got_sig = 0; int rc; if (sigsetjmp(jb, 1) == 0) { if (apply_fix) rc = resident_memcpy_fixed(rap, rec, rec_size, out, sizeof(out)); else rc = resident_memcpy_buggy(rap, rec, rec_size, out, sizeof(out)); } else { rc = 2; /* SIGSEGV -> OOB read past the guard page */ } sigaction(SIGSEGV, &oldsa, NULL); const char *verdict; switch (rc) { case 0: verdict = "memcpy within bounds (clean)"; break; case 1: verdict = "FIX REJECTED malformed attribute (EINVAL) - OOB prevented"; break; case 2: verdict = "SIGSEGV -> OOB READ past record into guard page " "(= adjacent slab heap in kernel): LEAK CONFIRMED"; break; default: verdict = "unknown"; break; } printf("mode=%-12s apply_fix=%-3d recsz=%-4d attr_off=%d " "a_dataoff=0x%04x a_datalen=%-3u -> rc=%d %s\n", mode, apply_fix, rec_size, attr_off, rap->a_dataoff, rap->a_datalen, rc, verdict); /* For the clean case, show the copied data to prove it works normally. */ if (rc == 0 && strcmp(mode, "clean") == 0) { printf(" copied %u bytes: first 8 = ", rap->a_datalen); for (int i = 0; i < 8 && i < (int)rap->a_datalen; i++) printf("%02x ", out[i]); printf("(expected 44 44 ... = 'D' sentinel)\n"); } return (rc == 0 || rc == 1) ? 0 : rc; } |