DF-0876 / 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 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | /* * DF-0876 — ext2_gd_csum OOB heap read: deterministic harness. * * Transcribes ext2_gd_csum (sys/vfs/ext2fs/ext2_csum.c:666-704) verbatim, * in particular the METADATA_CKSUM branch at ext2_csum.c:683-687: * * offset = offsetof(struct ext2_gd, ext4bgd_csum); // 30 * ... * offset += sizeof(dummy_csum); // 32 * if (offset < le16toh(fs->e2fs->e3fs_desc_size)) * csum32 = calculate_crc32c(csum32, (uint8_t *)gd + offset, * le16toh(fs->e2fs->e3fs_desc_size) - offset); * * With an attacker-controlled on-disk e3fs_desc_size (s_desc_size) of 0xFFFF * and the 64-bit INCOMPAT feature cleared, the in-kernel read becomes: * * calculate_crc32c(csum32, gd + 32, 0xFFFF - 32) * -> reads 65503 bytes past the 64-byte struct ext2_gd. * * Because ext2 only allocates sizeof(struct ext2_gd)=64 bytes per in-memory * group descriptor (vfsops.c:647, multiplied up only to whole-blocks), the * 65503-byte read walks far beyond the e2fs_gd array and into adjacent * kernel heap (or unmapped pages, page-faulting on INVARIANTS-ON GENERIC). * * This harness mirrors that read against a poison-padded buffer so the OOB * length is observable deterministically without depending on kernel * allocation layout. It proves: * - the read length when desc_size=0xFFFF is exactly 65503 bytes, * - the read goes well past the 64-byte struct ext2_gd, * - on a "production" mapping (where pages past the GD are mapped), this * leaks whatever bytes happen to be adjacent in memory. * * Build: cc -O2 -o harness harness.c * Run : ./harness */ #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> #include <sys/endian.h> /* Verbatim copy of struct ext2_gd from sys/vfs/ext2fs/ext2fs.h */ struct ext2_gd { uint32_t ext2bgd_b_bitmap; uint32_t ext2bgd_i_bitmap; uint32_t ext2bgd_i_tables; uint16_t ext2bgd_nbfree; uint16_t ext2bgd_nifree; uint16_t ext2bgd_ndirs; uint16_t ext4bgd_flags; uint32_t ext4bgd_x_bitmap; uint16_t ext4bgd_b_bmap_csum; uint16_t ext4bgd_i_bmap_csum; uint16_t ext4bgd_i_unused; uint16_t ext4bgd_csum; uint32_t ext4bgd_b_bitmap_hi; uint32_t ext4bgd_i_bitmap_hi; uint32_t ext4bgd_i_tables_hi; uint16_t ext4bgd_nbfree_hi; uint16_t ext4bgd_nifree_hi; uint16_t ext4bgd_ndirs_hi; uint16_t ext4bgd_i_unused_hi; uint32_t ext4bgd_x_bitmap_hi; uint16_t ext4bgd_b_bmap_csum_hi; uint16_t ext4bgd_i_bmap_csum_hi; uint32_t ext4bgd_reserved; }; /* ---- CRC32C (Castagnoli), non-inverting, identical to libkern/icrc32.c ---- */ static uint32_t crc32c_table[256]; static void crc32c_init(void) { uint32_t i, j, crc; const uint32_t POLY = 0x82F63B78; /* reflected */ for (i = 0; i < 256; i++) { crc = i; for (j = 0; j < 8; j++) crc = (crc >> 1) ^ (POLY & (-(int32_t)(crc & 1))); crc32c_table[i] = crc; } } static uint32_t calculate_crc32c(uint32_t crc, const uint8_t *buf, size_t len) { while (len--) crc = (crc >> 8) ^ crc32c_table[(crc ^ *buf++) & 0xff]; return crc; } #include <signal.h> static volatile sig_atomic_t g_faulted = 0; static void *g_fault_addr = NULL; static void sig_segv(int sig, siginfo_t *si, void *uc) { (void)sig; (void)uc; g_fault_addr = si->si_addr; g_faulted = 1; /* Cannot safely keep going; _exit now. */ /* Print to stderr from a signal handler is not strictly portable but * works on BSD/Linux for short messages via write(). */ const char msg[] = "\n[!] SIGSEGV caught during csum read at addr "; char buf[160]; size_t n = 0; memcpy(buf+n, msg, sizeof(msg)-1); n += sizeof(msg)-1; /* crude hex of fault addr */ unsigned long addr = (unsigned long)(uintptr_t)si->si_addr; const char hex[] = "0123456789abcdef"; buf[n++] = '0'; buf[n++] = 'x'; for (int shift = 60; shift >= 0; shift -= 4) buf[n++] = hex[(addr >> shift) & 0xf]; buf[n++] = '\n'; write(2, buf, n); _exit(133); } /* * Verbatim transcription of ext2_gd_csum (ext2_csum.c:666-704), the * METADATA_CKSUM branch only. The two args that matter for the bug are * `csum_seed` and `desc_size_le` (the attacker-controlled on-disk u16). * * Two entry points: * - ext2_gd_csum_harness: computes the csum length WITHOUT touching OOB * memory; just reports the requested length. * - ext2_gd_csum_faulting: performs the full read (and will SIGSEGV if * the read crosses into PROT_NONE pages). */ static uint16_t ext2_gd_csum_harness(uint32_t csum_seed, uint16_t desc_size_le, uint32_t block_group, const struct ext2_gd *gd, uint8_t *oob_first, size_t *oob_len, uint16_t *csum_out) { size_t offset; uint32_t csum32; uint16_t crc, dummy_csum = 0; offset = offsetof(struct ext2_gd, ext4bgd_csum); /* 30 */ block_group = htole32(block_group); csum32 = calculate_crc32c(csum_seed, (const uint8_t *)&block_group, sizeof(block_group)); csum32 = calculate_crc32c(csum32, (const uint8_t *)gd, offset); csum32 = calculate_crc32c(csum32, (const uint8_t *)&dummy_csum, sizeof(dummy_csum)); offset += sizeof(dummy_csum); /* 32 */ /* THE BUG: ext2_csum.c:684-686 -- computes the read LENGTH only. */ if (offset < desc_size_le) { *oob_first = *((const uint8_t *)gd + offset); *oob_len = desc_size_le - offset; /* For the harness we sum only up to end-of-struct so we don't * SIGSEGV. The faulting variant below does the full read. */ size_t safe = *oob_len; if (offset + safe > sizeof(struct ext2_gd)) safe = sizeof(struct ext2_gd) - offset; csum32 = calculate_crc32c(csum32, (const uint8_t *)gd + offset, safe); } else { *oob_len = 0; } crc = csum32 & 0xFFFF; if (csum_out) *csum_out = crc; return crc; } /* Faulting variant: does the full ext2_csum.c:684-686 read. * Will SIGSEGV when the read crosses a PROT_NONE page (just like the kernel * page-faults when it walks past the e2fs_gd slab allocation). */ static uint32_t ext2_gd_csum_faulting(uint32_t csum_seed, uint16_t desc_size_le, uint32_t block_group, const struct ext2_gd *gd) { size_t offset; volatile uint32_t csum32; uint16_t dummy_csum = 0; offset = offsetof(struct ext2_gd, ext4bgd_csum); /* 30 */ block_group = htole32(block_group); csum32 = calculate_crc32c(csum_seed, (const uint8_t *)&block_group, sizeof(block_group)); csum32 = calculate_crc32c(csum32, (const uint8_t *)gd, offset); csum32 = calculate_crc32c(csum32, (const uint8_t *)&dummy_csum, sizeof(dummy_csum)); offset += sizeof(dummy_csum); /* 32 */ if (offset < desc_size_le) { /* THE BUG: ext2_csum.c:684-686 -- full read, will page-fault. */ csum32 = calculate_crc32c(csum32, (const uint8_t *)gd + offset, desc_size_le - offset); } return csum32; } int main(void) { setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); /* Install SIGSEGV handler so the OOB read in case C becomes a clean * diagnostic instead of an uncaught crash. */ struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = sig_segv; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); crc32c_init(); const size_t GD_SIZE = sizeof(struct ext2_gd); /* 64 */ printf("[*] sizeof(struct ext2_gd) = %zu bytes\n", GD_SIZE); printf("[*] offsetof(ext4bgd_csum) = %zu bytes\n", offsetof(struct ext2_gd, ext4bgd_csum)); /* Allocate a 64-byte struct followed by a poison page (PROT_NONE). * simulate the slab layout: the GD object is bounded; everything past * it is either adjacent slab metadata or unmapped. */ const size_t PAGE = 4096; uint8_t *base = mmap(NULL, 3 * PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 1; } /* guard the page after our small window */ if (mprotect(base + PAGE, PAGE, PROT_NONE) != 0) { perror("mprotect"); return 1; } /* place the GD object at the end of the first page so that * gd + 64 is exactly the PROT_NONE boundary. */ uint8_t *gd_addr = base + PAGE - GD_SIZE; struct ext2_gd *gd = (struct ext2_gd *)gd_addr; memset(gd, 0, GD_SIZE); /* Fill the legitimate fields with a marker pattern. */ gd->ext2bgd_b_bitmap = 0xDEADBEEF; gd->ext4bgd_csum = 0x0000; /* -- CASE A: legitimate desc_size=0 (rev0) -- no OOB -- */ uint8_t oob_first; size_t oob_len; uint16_t csum_ok = 0; ext2_gd_csum_harness(0xCAFEF00D, 0 /* desc_size=0 */, 0, gd, &oob_first, &oob_len, &csum_ok); printf("\n[A] desc_size=0 (legitimate rev0 GD):\n"); printf(" csum=0x%04x OOB read length = %zu bytes (expected 0)\n", csum_ok, oob_len); if (oob_len != 0) { printf(" !! BUG: OOB read even with desc_size=0\n"); return 1; } /* -- CASE B: legitimate desc_size=64 (64bit GD) -- */ csum_ok = 0; ext2_gd_csum_harness(0xCAFEF00D, 64, 0, gd, &oob_first, &oob_len, &csum_ok); printf("\n[B] desc_size=64 (legitimate 64bit GD):\n"); printf(" csum=0x%04x read length = %zu bytes (32..64 within GD)\n", csum_ok, oob_len); if (oob_len != 32) { printf(" !! Unexpected length %zu (expected 32)\n", oob_len); return 1; } /* -- CASE C: attacker-controlled desc_size=0xFFFF -- OOB -- */ csum_ok = 0; ext2_gd_csum_harness(0xCAFEF00D, 0xFFFF, 0, gd, &oob_first, &oob_len, &csum_ok); printf("\n[C] desc_size=0xFFFF (attacker-controlled, METADATA_CKSUM only):\n"); printf(" csum=0x%04x read length = %zu bytes\n", csum_ok, oob_len); if (oob_len != 65503) { printf(" !! Unexpected OOB length %zu (expected 65503)\n", oob_len); return 1; } size_t struct_end = GD_SIZE; /* 64 */ size_t read_end = offsetof(struct ext2_gd, ext4bgd_csum) + 2 + oob_len; /* 32 + 65503 = 65535 */ printf(" OOB read: gd+%zu .. gd+%zu (length %zu)\n", (size_t)32, read_end, oob_len); printf(" struct ext2_gd ends at gd+%zu\n", struct_end); printf(" -> read extends %zu bytes PAST the 64-byte struct ext2_gd.\n", read_end - struct_end); /* Demonstrate the page-boundary fault that the kernel will take. * GD object sits at the end of page 1 (last 64 bytes); page 2 is * PROT_NONE. The faulting csum read will SIGSEGV at the start of * page 2 — exactly what the kernel will page-fault on when it reads * past the e2fs_gd slab allocation. The SIGSEGV handler prints the * fault address and exits. */ printf("\n[D] Invoking ext2_gd_csum_faulting (full ext2_csum.c:684-686 read):\n"); printf(" GD at %p (end of page 1); next page PROT_NONE at %p\n", (void *)gd, (void *)(base + PAGE)); printf(" Expecting SIGSEGV at %p (= gd+%zu = start of PROT_NONE page).\n", (void *)(base + PAGE), (size_t)((base + PAGE) - (uint8_t *)gd)); fflush(stdout); volatile uint32_t got = ext2_gd_csum_faulting(0xCAFEF00D, 0xFFFF, 0, gd); (void)got; /* If we get here, no fault happened (would mean PROT_NONE didn't fire). */ printf("\n[!] Harness did NOT fault -- PROT_NONE guard ineffective.\n"); /* Final summary */ printf("\n=== DF-0876 VERDICT ===\n"); printf("ext2_csum.c:684-686 reads `e3fs_desc_size - offset` bytes from\n"); printf("`gd + offset`. With attacker-controlled e3fs_desc_size=0xFFFF and\n"); printf("offset=32, the read length is 65503 bytes -- %d bytes past the\n", 65503 - (GD_SIZE - 32)); printf("64-byte struct ext2_gd allocation. In-kernel (where the next page\n"); printf("may be mapped slab metadata), this is a heap over-read / info leak;\n"); printf("on INVARIANTS-ON GENERIC it page-faults and panics.\n"); munmap(base, 3 * PAGE); return 0; } |