DF-0791 / 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 | /* * harness.c - Deterministic userspace proof of the DF-0791 unvalidated * subnode-dive OOB read in ntfs_ntlookupfile() * (sys/vfs/ntfs/ntfs_subr.c:1006-1011). * * Mirrors the EXACT kernel walk + dive logic: * * aoff = sizeof(struct attr_indexroot); // 32 * do { * iep = (struct attr_indexentry *)(rdbuf + aoff); * for (; !(iep->ie_flag & LAST) && (rdsize > aoff); * aoff += iep->reclen, * iep = (struct attr_indexentry *)(rdbuf + aoff)) * { * res = name_cmp(iep->fname, lookup); // entry "zzzzzz" > lookup "a" * if (res > 0) break; * ... * } * if (iep->ie_flag & SUBNODE) { * cn = *(cn_t *)(rdbuf + aoff + iep->reclen - sizeof(cn_t)); // <-- BUG * ... * } * } while (1); * * The buffer is placed at the END of a writable page immediately before a * PROT_NONE guard page. Any read past rdsize/the allocation faults * deterministically (SIGSEGV), proving the primitive regardless of kernel * slab layout. * * Modes: * clean reclen sane, dive read in-bounds -> rc=0 (cn read OK) * oob reclen=0xFFFF, dive read past buffer -> rc=2 SIGSEGV (OOB read) * tiny reclen=2 (< sizeof(cn_t)) -> rc=2 SIGSEGV (OOB read) * * With apply_fix=1, the proposed guard (reclen>=8 && aoff+reclen<=rdsize) runs * and rejects the malformed entry cleanly -> rc=-1 (EINVAL) instead of faulting. * * Build: cc -O2 -o harness harness.c * Run: ./harness [clean|oob|tiny] [0|1] */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> #include <unistd.h> typedef uint64_t cn_t; typedef uint32_t u32; typedef uint16_t u16; #define SUBNODE 0x01u #define LAST 0x02u #define INDEXROOT_HDR 32 /* sizeof(struct attr_indexroot) */ #define IEP_FIXED 82 /* fixed part of attr_indexentry up to ie_fname */ #define CN_SIZE sizeof(cn_t) /* 8 */ /* minimal on-disk index entry shape we care about */ struct iep_disk { u32 number; u32 unk1; u16 reclen; u16 size; u32 flag; u32 fpnumber; u32 unk2; char times[32]; char rest[8]; /* ie_fnamelen @ +80, ie_fnametype @ +81, ie_fname @ +82 */ }; static sigjmp_buf jb; static volatile int faulted; static void handler(int s, siginfo_t *si, void *uc) { (void)s; (void)si; (void)uc; faulted = 1; siglongjmp(jb, 1); } /* Build a buffer of `cap` usable bytes backed by pages, with a PROT_NONE * guard page immediately after the usable region. Returns the usable base * via *out and the raw mmap base via *base (for munmap). */ static void make_guard_buf(unsigned cap, unsigned char **out, unsigned char **base, unsigned *maplen) { long ps = sysconf(_SC_PAGESIZE); unsigned pages = (cap + ps - 1) / ps; unsigned len = (pages + 1) * ps; /* +1 guard page */ void *m = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); if (m == MAP_FAILED) { perror("mmap"); exit(3); } /* guard the LAST page */ if (mprotect((char*)m + pages*ps, ps, PROT_NONE) != 0) { perror("mprotect"); exit(3); } /* usable region = first `cap` bytes; the guard sits right after byte cap-1 * only if cap is page-aligned; otherwise there is padding before the guard. * To make the OOB fault as early as possible we place the data at the END * of the last usable page so any overshoot hits the guard immediately. */ unsigned region_pages = pages * ps; unsigned char *usable = (unsigned char*)m + (region_pages - cap); memset(usable, 0xAA, cap); /* fill with residue marker */ *out = usable; *base = m; *maplen = len; } /* Replicate the kernel dive read. Returns: * 0 -> cn read in bounds (no OOB) * -1 -> fix rejected malformed entry (EINVAL) * If the read faults, the signal handler longjmps and we report rc=2. */ static int do_dive(unsigned char *rdbuf, u32 rdsize, u32 aoff, u16 reclen, int apply_fix, cn_t *cn_out) { if (apply_fix) { /* proposed fix: entry (incl trailing 8B VCN) must fit in valid data */ if (reclen < CN_SIZE) return -1; if (aoff > rdsize) return -1; if (reclen > (u16)(rdsize - aoff)) return -1; /* also guard the cn read itself */ uint64_t end = (uint64_t)aoff + (uint64_t)reclen; if (end > rdsize) return -1; } /* the BUGGY kernel line: cn = *(cn_t*)(rdbuf + aoff + reclen - 8); */ cn_t cn = *(cn_t *)(rdbuf + aoff + reclen - CN_SIZE); if (cn_out) *cn_out = cn; return 0; } int main(int argc, char **argv) { const char *mode = (argc > 1) ? argv[1] : "oob"; int apply_fix = (argc > 2) ? atoi(argv[2]) : 0; struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = handler; sa.sa_flags = SA_SIGINFO | SA_NODEFER; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); /* layout: INDEX_ROOT header (32) + one index entry "zzzzzz". * Real entry footprint ~ 88 bytes (IEP_FIXED + name). The dive consults * the entry's reclen field, which we set per mode. rdsize = header + real * entry bytes (so the walk ENTERS the entry), but the dive read goes to * aoff+reclen-8 which for malformed reclen is past rdsize -> guard fault. */ unsigned entry_real = (IEP_FIXED + 12 + 7) & ~7u; /* 96 bytes */ unsigned rdsize = INDEXROOT_HDR + entry_real; /* 128 */ unsigned cap = rdsize; /* usable bytes */ unsigned char *rdbuf, *base; unsigned maplen; make_guard_buf(cap, &rdbuf, &base, &maplen); u32 aoff = INDEXROOT_HDR; /* walk start */ /* plant the entry header at rdbuf+aoff */ struct iep_disk *ie = (struct iep_disk *)(rdbuf + aoff); ie->flag = SUBNODE; /* trigger the dive */ ie->reclen = (mode && strcmp(mode, "tiny") == 0) ? 2 : (strcmp(mode, "clean") == 0) ? (u16)entry_real : 0xFFFF; rdbuf[aoff + 80] = 6; /* ie_fnamelen */ memcpy(rdbuf + aoff + 82, "z\0z\0z\0z\0z\0z\0", 12); /* ie_fname "zzzzzz" */ fprintf(stderr, "[harness] mode=%s apply_fix=%d cap=%u rdsize=%u aoff=%u " "entry_reclen=%u dive_off=%llu (vs rdsize=%u)\n", mode, apply_fix, cap, rdsize, aoff, ie->reclen, (unsigned long long)((uint64_t)aoff + ie->reclen - CN_SIZE), rdsize); faulted = 0; int rc; cn_t cn = 0; if (sigsetjmp(jb, 1) == 0) { rc = do_dive(rdbuf, rdsize, aoff, ie->reclen, apply_fix, &cn); } else { /* faulted inside the dive read -> OOB read past the buffer */ printf("mode=%-6s apply_fix=%d -> rc=2 SIGSEGV -> OOB READ past rdbuf " "(dive offset %llu >= rdsize %u)\n", mode, apply_fix, (unsigned long long)((uint64_t)aoff + ie->reclen - CN_SIZE), rdsize); munmap(base, maplen); return 2; } if (rc == 0) { printf("mode=%-6s apply_fix=%d -> rc=0 dive read OK, cn=0x%016llx " "(in bounds)\n", mode, apply_fix, (unsigned long long)cn); } else { printf("mode=%-6s apply_fix=%d -> rc=-1 FIX REJECTED malformed entry " "(reclen=%u) -> EINVAL in kernel\n", mode, apply_fix, ie->reclen); } munmap(base, maplen); return (rc == 0) ? 0 : 1; } |