DF-0769 / 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 | /* * harness.c - Deterministic DF-0769 production-primitive proof. * * Transcribes the EXACT vulnerable arithmetic from * sys/vfs/hammer/hammer_vnops.c:1728-1738 (hammer_vop_readdir) * sys/kern/vfs_subr.c:2560-2577 (vop_write_dirent) * and demonstrates the OOB kernel-heap read that occurs on a PRODUCTION * kernel (INVARIANTS OFF, where the KKASSERT at :1728 is compiled out). * * On GENERIC (INVARIANTS ON) the KKASSERT(data_len > 16) panics first, so * this primitive is never reached -- that is the GENERIC manifestation. * On production kernels the assertion is absent and the code proceeds: * * d_namlen = (uint16_t)(data_len - HAMMER_ENTRY_NAME_OFF) * = (uint16_t)(8 - 16) = (uint16_t)(-8) = 65528 * * then in vop_write_dirent: * bcopy(d_name, dp->d_name, 65528); * * reads 65528 bytes starting at cursor.data->entry.name, which lives inside * a 16KB hammer_buffer->ondisk allocation. offset of name within that buffer * is 16, so the bcopy reads (16384-16)=16368 valid bytes then ~49160 bytes * BEYOND the buffer = OOB kernel-heap read of neighbouring slab/page data. * * This harness reproduces that bcopy against a "poisoned" 16KB allocation * (filled with recognisable sentinel bytes simulating adjacent kernel heap) * so the OOB read extent is observable and deterministic. * * Build: cc -O2 -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #define HAMMER_ENTRY_NAME_OFF 16 #define HAMMER_BUFSIZE 16384 /* hammer_buffer ondisk size */ /* mirror of vop_write_dirent's narrowing of the length argument */ static uint16_t forge_d_namlen(int32_t data_len) { /* vnops.c:1737 -- the expression is computed in int, then passed to a * uint16_t parameter (implicit narrowing). */ int expr = data_len - HAMMER_ENTRY_NAME_OFF; /* 8 - 16 = -8 */ return (uint16_t)expr; /* 65528 */ } int main(void) { /* ---- simulate the kernel allocator: a 16KB hammer_buffer->ondisk ---- */ uint8_t *ondisk = (uint8_t *)malloc(HAMMER_BUFSIZE); /* the direntry data begins at the start of the buffer; name at +16 */ memset(ondisk, 0xAA, HAMMER_BUFSIZE); memcpy(ondisk, "OBJIDXXXX", 8); /* direntry obj_id (8 bytes) */ memcpy(ondisk + HAMMER_ENTRY_NAME_OFF, "X", 1); /* the 1-char name */ /* ---- simulate adjacent kernel heap (what the OOB read will sweep) ---- * In the real kernel the bytes immediately past hammer_buffer->ondisk * are whatever the slab allocator placed there (other kmalloc'd objects, * cred pointers, etc.). We tag successive regions so the leak is visible. */ size_t poison_sz = 70000; uint8_t *poison = (uint8_t *)malloc(poison_sz); memset(poison, 0xCC, poison_sz); /* the real buffer is 16KB; everything past it is "neighbouring heap" */ memcpy(poison, ondisk, HAMMER_BUFSIZE); /* first 16KB is the real buffer */ free(ondisk); /* 'poison' now models the kernel address space: [0..16384) = ondisk buffer, * [16384..70000) = adjacent kernel heap that the bcopy will read OOB. */ int32_t forged_data_len = 8; /* what the crafted B-tree leaf carries */ uint16_t d_namlen = forge_d_namlen(forged_data_len); printf("forged data_len = %d\n", forged_data_len); printf("HAMMER_ENTRY_NAME_OFF = %d\n", HAMMER_ENTRY_NAME_OFF); printf("data_len - NAME_OFF = %d\n", forged_data_len - HAMMER_ENTRY_NAME_OFF); printf("=> d_namlen (uint16_t) = %u (0x%x)\n", d_namlen, d_namlen); /* source pointer == cursor.data->entry.name (offset 16 within buffer) */ const char *d_name = (const char *)(poison + HAMMER_ENTRY_NAME_OFF); /* destination == freshly-allocated dirent (M_ZERO'd), as in vop_write_dirent */ size_t len = ((16 /*offsetof(d_name)*/ + d_namlen + 1 + 7) & ~(size_t)7); /* _DIRENT_RECLEN */ printf("=> _DIRENT_RECLEN(d_namlen) = %zu bytes (kmalloc'd + uiomove'd to user)\n", len); uint8_t *dp_d_name = (uint8_t *)calloc(1, d_namlen); /* the vulnerable bcopy -- reads 65528 bytes from d_name */ memcpy(dp_d_name, d_name, d_namlen); /* measure the OOB extent: how far past the 16KB buffer does the read go? */ size_t valid_in_buf = HAMMER_BUFSIZE - HAMMER_ENTRY_NAME_OFF; /* 16368 */ long oob; if (d_namlen > valid_in_buf) oob = (long)d_namlen - (long)valid_in_buf; else oob = 0; printf("\nvop_write_dirent bcopy(d_name, dp->d_name, %u)\n", d_namlen); printf(" source region: 16KB hammer_buffer->ondisk (name at +%d)\n", HAMMER_ENTRY_NAME_OFF); printf(" valid bytes in source buffer = %zu\n", valid_in_buf); printf(" bytes read by bcopy = %u\n", d_namlen); printf(" >>> OOB READ beyond buffer = %ld bytes <<<\n", oob); printf(" uiomove then copies all %zu bytes (incl OOB data) to the getdents user buffer\n", len); printf(" => ~%ld-byte kernel-heap INFO LEAK via unprivileged getdents\n", oob); /* show a few bytes that came from past the buffer (the "leaked" bytes) */ size_t leak_start = valid_in_buf; /* index into dp_d_name past the buffer */ printf("\nSample leaked bytes (from dp_d_name[%zu..], i.e. past the hammer buffer):\n ", leak_start); for (size_t i = 0; i < 16 && leak_start + i < d_namlen; i++) printf("%02x ", dp_d_name[leak_start + i]); printf("\n(0xCC = adjacent kernel heap sentinel; real kernel exposes live heap data)\n"); free(dp_d_name); free(poison); return 0; } |