DF-0871 / 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 | /* * DF-0871 deterministic harness (v2 -- defeats the compiler's array-bounds UB * assumption that, at -O2, let the original transcription "pass" without * faulting). * * Transcribes the EXACT unbounded wchar->char do/while of ntfs_mountfs() * (sys/vfs/ntfs/ntfs_vfsops.c:444,458-460) into userspace, backed by a * poisoned allocator that places the victim array at the very end of a * guard-paged page so the heap OOB write is detected byte-exactly. * * ntmp->ntm_ad = kmalloc(num * sizeof(struct ntvattrdef), M_NTFSMNT, ...); // :444 * j = 0; // :457 * do { * ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j]; // :459 DEST is char[0x40]=64 * } while(ad.ad_name[j++]); // :460 NO bound on j * ntmp->ntm_ad[i].ad_namelen = j - 1; // :461 repairs bytes 64..67 * ntmp->ntm_ad[i].ad_type = ad.ad_type; // :462 repairs bytes 68..71 * * struct attrdef { wchar ad_name[0x40]; u32 ad_type; u32 r1[2]; u32 ad_flag; u64 min; u64 max; } = 160 B * struct ntvattrdef { char ad_name[0x40]; int ad_namelen; u32 ad_type; } = 72 B * * On the kernel the source `ad` is a 160-byte stack struct; when its 64 wchars * AND trailing 32 B are all non-zero half-words the do/while reads PAST the * struct (kernel stack) until it happens to hit a zero wchar. The DEST write * has already left the 72-byte ntvattrdef[i] at j=72. * * To faithfully reproduce this WITHOUT letting the optimizer exploit the * `ad_name[64]` array bounds (which is what made v1 silently pass), the source * is presented as an OVERSIZED wchar array `src[SRCLEN]` (all non-zero up to a * controlled NUL), accessed through the SAME indexing expression the kernel * uses. This defeats the bounds assumption while preserving the exact loop * semantics. * * Build (guest): cc -O2 -o harness harness.c * Run: ./harness [num] * num=1 : 72 B object at page-end; do/while SIGSEGVs at j=72 (1st byte past * the object) => deterministic proof the write leaves the ntvattrdef. * num>=2: array poisoned at page-end; the loop runs on the LAST entry whose * overflow crosses the whole allocation => SIGSEGV; before that, * earlier entries' overflows corrupt the next entry (reported). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <sys/mman.h> #include <unistd.h> #include <signal.h> #include <setjmp.h> typedef uint16_t wchar_nt; /* typedef u_int16_t wchar; (ntfs.h:37) */ #define NTFS_ATTRNAME_MAXLEN 0x40 /* ntfs.h:203 */ struct ntvattrdef { /* ntfs.h:215, 72 B */ char ad_name[0x40]; /* +0 64 */ int ad_namelen; /* +64 4 */ uint32_t ad_type; /* +68 4 */ }; /* == 72 */ /* Oversized source: 256 wchars. [0..159] mimic an evil struct attrdef (64 * non-zero name wchars + 32 B of non-zero trailing fields == 16 non-zero * wchars at indices 64..79); [80..254] are non-zero "stack residue"; [255]=0 * is a guaranteed terminator so the walk always stops somewhere (as it must * on a real stack). The dest overflow at j=72.. happens long before j=255. */ #define SRCLEN 256 static wchar_nt src_buf[SRCLEN]; static void make_evil_source(void) { for (int i = 0; i < SRCLEN - 1; i++) /* all non-zero ... */ src_buf[i] = (wchar_nt)0x4141; src_buf[SRCLEN - 1] = 0; /* ... except the final NUL */ } static long PAGE = 0; static sigjmp_buf jb; static void segv(int sig) { (void)sig; siglongjmp(jb, 1); } static unsigned char *poison_alloc(size_t size) { long pg = PAGE; char *base = mmap(NULL, pg * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) { perror("mmap"); exit(2); } if (mprotect(base + pg, pg, PROT_NONE) != 0) { perror("mprotect"); exit(2); } return (unsigned char *)(base + pg - size); } /* Verbatim transcription of ntfs_vfsops.c:457-462 for entry index `i`. * The source is indexed exactly as the kernel indexes ad.ad_name[j]; using the * oversized src_buf keeps the compiler from assuming the trip count is < 64. */ static void buggy_copy(struct ntvattrdef *arr, int i) { int j = 0; /* :457 */ do { arr[i].ad_name[j] = (char)src_buf[j]; /* :459 DEST char[64] */ } while(src_buf[j++]); /* :460 -- UNBOUNDED */ arr[i].ad_namelen = j - 1; /* :461 repairs bytes 64..67 */ arr[i].ad_type = 0xFFFFFFFFu; /* :462 repairs bytes 68..71 */ } int main(int argc, char **argv) { PAGE = sysconf(_SC_PAGESIZE); int num = (argc > 1) ? atoi(argv[1]) : 1; if (num < 1) num = 1; struct sigaction sa; memset(&sa, 0, sizeof sa); sa.sa_handler = segv; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_NODEFER; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); make_evil_source(); size_t allocsz = (size_t)num * sizeof(struct ntvattrdef); /* :444 */ printf("[harness] DF-0871 ntfs_mountfs $AttrDef OOB-write transcription (v2)\n"); printf("[harness] num entries = %d\n", num); printf("[harness] kmalloc size (num*72) = %zu -> dest char ad_name[64] each\n", allocsz); printf("[harness] source: 256 non-zero wchars + final NUL -> do/while is unbounded\n"); printf("[harness] BUG: do { dest[j]=src[j]; } while(src[j++]) with NO bound on j\n"); unsigned char *block = poison_alloc(allocsz); memset(block, 0x5A, allocsz); struct ntvattrdef *arr = (struct ntvattrdef *)block; printf("[harness] victim array at %p (last %zu B of a page; next page PROT_NONE)\n", (void *)block, allocsz); /* To exercise cross-entry corruption visibly (for num>=2), first run the * buggy copy on entry[0] into a SEPARATE non-guarded array, then report * how many bytes of entry[1] it clobbered. This isolates the cross-entry * claim from the guard-fault demo. */ if (num >= 2) { unsigned char *viz = calloc(1, (size_t)num * 72 + 16); struct ntvattrdef *varr = (struct ntvattrdef *)viz; memset(viz, 0x5A, (size_t)num * 72 + 16); if (sigsetjmp(jb, 1) == 0) { buggy_copy(varr, 0); /* entry[0] into non-guarded array */ int cor = 0; for (int k = 0; k < 72; k++) if (varr[1].ad_name[k] != (char)0x5A) cor++; printf("[harness] cross-entry demo: entry[0]'s do/while overwrote %d " "byte(s) of entry[1].ad_name before its NUL stop (j~255).\n", cor); } free(viz); } printf("[harness] running buggy do/while on entry[%d] (last) into guarded array...\n", num - 1); fflush(stdout); if (sigsetjmp(jb, 1) == 0) { buggy_copy(arr, num - 1); /* <-- BUG; last entry overflows past alloc */ printf("[harness] ERROR: overflow was NOT caught -- guard missing\n"); return 3; } /* SIGSEGV longjmp lands here. */ printf("[harness] SIGSEGV caught -> do/while wrote PAST the %zu-byte allocation.\n", allocsz); printf("[harness] PROOF (deterministic): the unbounded copy at\n"); printf("[harness] ntfs_vfsops.c:458-460 writes dest[j] for j>=72, i.e. past the\n"); printf("[harness] 72-byte ntvattrdef object. On the live kernel this is a heap\n"); printf("[harness] OOB write into the M_NTFSMNT slab zone (kmalloc-128/256 bucket).\n"); return 0; } |