DF-0811 / 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 | /* * DF-0811 โ deterministic harness. * * Transcribes the EXACT kernel math from: * sys/vfs/ext2fs/ext2_alloc.c:856-868 (ext2_get_group_number / ext2_block_in_group) * sys/vfs/ext2fs/ext2_alloc.c:870-925 (ext2_cg_block_bitmap_init, the setbit path) * sys/sys/param.h:390 (setbit macro semantics) * * The harness proves that the divisor bug (/ e2fs_bsize instead of / e2fs_bpg, * which mount enforces == e2fs_bsize*8) lets ext2_block_in_group() return TRUE * for a block bitmap pointer that is physically in a DIFFERENT group, so the * setbit(bp->b_data, tmp - start) * index wraps a uint64 subtraction to ~2^64 and the resulting byte offset * (index / NBBY) is a wild out-of-bounds kernel heap write. * * The "bp->b_data" stand-in is an mmap'd page with a guard page immediately * after it, so the wild write faults (SIGSEGV) โ proving the OOB. */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <signal.h> #include <setjmp.h> #include <unistd.h> #include <sys/mman.h> #define NBBY 8 /* sys/sys/param.h */ /* ---- verbatim transcription of ext2_alloc.c:855-868 ------------------------- */ /* ext2_alloc.c:855 */ static int ext2_get_group_number(uint32_t e2fs_first_dblock, uint32_t e2fs_bsize, uint32_t e2fs_bpg, uint64_t block) { /* * KERNEL CODE (ext2_alloc.c:859-860): * return ((block - le32toh(fs->e2fs->e2fs_first_dblock)) / * fs->e2fs_bsize); * * BUG: divides by e2fs_bsize (block size in BYTES). Mount enforces * e2fs_bpg == e2fs_bsize * 8 (ext2_vfsops.c:568), so the divisor is * wrong by a factor of 8. Correct divisor is e2fs_bpg. */ return (int)((block - e2fs_first_dblock) / e2fs_bsize); /* BUGGY */ } /* ext2_alloc.c:863 */ static int ext2_block_in_group(uint32_t first_dblock, uint32_t bsize, uint32_t bpg, uint64_t block, int cg) { return ((ext2_get_group_number(first_dblock, bsize, bpg, block) == cg) ? 1 : 0); } /* what the CORRECT implementation would do (for comparison) */ static int ext2_block_in_group_fixed(uint32_t first_dblock, uint32_t bpg, uint64_t block, int cg) { return ((((block - first_dblock) / bpg) == cg) ? 1 : 0); } /* ---- verbatim transcription of setbit (sys/sys/param.h:390) ----------------- */ /* #define setbit(a,i) ((a)[(i)/NBBY] |= 1<<((i)%NBBY)) */ static unsigned long setbit_byte_index(uint64_t i) { return (unsigned long)(i / NBBY); } /* ---- harness state for the guard-page trap --------------------------------- */ static sigjmp_buf jb; static volatile int faulted; static void *fault_addr; static void segv_handler(int sig, siginfo_t *si, void *uc) { (void)sig; (void)uc; faulted = 1; fault_addr = si->si_addr; siglongjmp(jb, 1); } int main(void) { /* * Filesystem geometry matching a default mke2fs -b 4096 image: * bsize = 4096, first_dblock = 0, bpg = 32768 (= bsize*8). * Mount invariant (ext2_vfsops.c:568): e2fs_bpg == e2fs_bsize * 8. */ const uint32_t bsize = 4096; const uint32_t first_dblock = 0; const uint32_t bpg = bsize * 8; /* 32768, enforced at mount */ const int cg = 1; const int has_flex_bg = 1; /* EXT2F_INCOMPAT_FLEX_BG */ /* * Attacker-controlled group-1 descriptor: block bitmap pointer. * Under FLEX_BG this may live anywhere in the fs that passes mount * validation (ext2_vfsops.c:400-409 uses CORRECT bounds), so we point * it at a block physically in GROUP 0 but whose buggy group-number * computes to 1. * * buggy ext2_get_group_number(tmp) = (tmp - 0)/4096 == 1 => tmp in * [4096, 8191]. Pick tmp = 4096 (first data block of group 0 after * the metadata area). This block is inside group 0 (blocks 0..32767). */ const uint64_t b_bitmap_attacker = 4096; printf("=== DF-0811 divisor-bug harness ===\n"); printf("geometry: bsize=%u first_dblock=%u bpg=%u (== bsize*8 mount invariant)\n", bsize, first_dblock, bpg); printf("target cg=%d, FLEX_BG=%d\n", cg, has_flex_bg); printf("attacker group-%d b_bitmap pointer = %llu (physically in group 0)\n", cg, (unsigned long long)b_bitmap_attacker); /* --- the buggy gate (ext2_alloc.c:893-894) --- */ int gate_buggy = ext2_block_in_group(first_dblock, bsize, bpg, b_bitmap_attacker, cg); int gate_fixed = ext2_block_in_group_fixed(first_dblock, bpg, b_bitmap_attacker, cg); printf("\nBUGGY ext2_block_in_group(tmp=%llu, cg=%d) = %d\n", (unsigned long long)b_bitmap_attacker, cg, gate_buggy); printf(" (buggy ext2_get_group_number = (%llu-%u)/%u = %llu)\n", (unsigned long long)b_bitmap_attacker, first_dblock, bsize, (unsigned long long)((b_bitmap_attacker - first_dblock) / bsize)); printf("CORRECT ext2_block_in_group(tmp=%llu, cg=%d) = %d\n", (unsigned long long)b_bitmap_attacker, cg, gate_fixed); printf(" (correct group number = (%llu-%u)/%u = %llu)\n", (unsigned long long)b_bitmap_attacker, first_dblock, bpg, (unsigned long long)((b_bitmap_attacker - first_dblock) / bpg)); /* --- the wrap (ext2_alloc.c:888-895) --- */ uint64_t start = (uint64_t)cg * bpg + first_dblock; /* line 888-889 */ uint64_t bit_index = b_bitmap_attacker - start; /* line 895 arg */ unsigned long byte_index = setbit_byte_index(bit_index); printf("\n--- setbit index math (ext2_alloc.c:888-895) ---\n"); printf("start = cg*bpg + first_dblock = %u*%u + %u = %llu\n", cg, bpg, first_dblock, (unsigned long long)start); printf("bit_index = tmp - start = %llu - %llu = %llu (0x%llx)\n", (unsigned long long)b_bitmap_attacker, (unsigned long long)start, (unsigned long long)bit_index, (unsigned long long)bit_index); printf("byte_index = bit_index / NBBY = %lu (0x%lx)\n", byte_index, byte_index); printf("bp->b_data size = bsize = %u bytes\n", bsize); if (gate_buggy && byte_index >= bsize) { printf("\n*** BUG TRIGGERED: setbit(bp->b_data[+%lu], ...) writes %lu bytes" "\n PAST the %u-byte block-bitmap buffer => wild kernel heap write.\n", byte_index, byte_index - bsize, bsize); } else if (!gate_buggy) { printf("\n(no trigger: buggy gate returned FALSE)\n"); return 0; } /* --- guard-page demonstration: actually do the wild write --- */ /* mmap one page then an unmapped guard page. bsize (4096) == one page. */ long pgsz = sysconf(_SC_PAGESIZE); char *base = mmap(NULL, pgsz * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 1; } /* unmap the second page so any write >= pgsz faults */ if (munmap(base + pgsz, pgsz) != 0) { perror("munmap"); return 1; } char *bp_data = base; /* stand-in for bp->b_data */ struct sigaction sa; sa.sa_sigaction = segv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); printf("\n--- demonstrating the wild write into a guard-paged buffer ---\n"); printf("bp->b_data stand-in = %p (%ld bytes valid, guard page at %p)\n", bp_data, pgsz, bp_data + pgsz); faulted = 0; if (sigsetjmp(jb, 1) == 0) { /* the verbatim setbit, exactly as the kernel would do it */ bp_data[byte_index] |= (1u << (bit_index % NBBY)); printf("WTF: wild write did NOT fault โ harness logic error\n"); return 2; } printf("RESULT: write to bp->b_data[+%lu] (addr %p) FAULTED (SIGSEGV/SIGBUS)\n", byte_index, fault_addr); printf(" => the kernel write is %lu bytes beyond the %u-byte buffer.\n", byte_index - bsize, bsize); printf(" => confirmed out-of-bounds kernel heap write primitive.\n"); munmap(base, pgsz); return 0; } |