DF-0776 / 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 | /* * harness.c - Deterministic DF-0776 production-primitive proof. * * Transcribes the EXACT vulnerable loops from * sys/vfs/hammer/hammer_btree.c:1390-1413 (hammer_btree_search_node) * sys/vfs/hammer/hammer_btree.c:1289-1338 (btree_search leaf loop) * sys/vfs/hammer/hammer_btree.c:819-824 (hammer_btree_insert bcopy) * and demonstrates the OOB kernel-heap READ and WRITE that occur on a * PRODUCTION kernel (INVARIANTS OFF, where the KKASSERTs at :818/:1278 are * compiled out). * * On GENERIC (INVARIANTS ON): * - the KKASSERT at :1278 (node->count <= HAMMER_BTREE_LEAF_ELMS) fires * FIRST, before the leaf search loop -> panic (DoS). See panic.txt. * - the KKASSERT at :818 (node->count < HAMMER_BTREE_LEAF_ELMS) fires * before the insert bcopy -> panic. * On production kernels those assertions are absent and the code proceeds: * * (1) OOB READ -- hammer_btree_search_node() does a binary search with * s = node->count = 200, so the midpoint i can be up to 100, indexing * node->elms[100] -- 37 elements (2368 bytes) past the end of the * fixed elms[63] array. Then the leaf linear loop * while (i < node->count) { elm = &node->elms[i]; ... ++i; } * walks elms[0..199], reading elms[63..199] = 137 elements = 8768 bytes * of OOB kernel heap. Each hammer_btree_cmp() dereferences the base * fields of the forged element -- a deterministic OOB read. * * (2) OOB WRITE -- hammer_btree_insert() (reached on a create/mkdir/echo * that positions a cursor then inserts): * bcopy(&node->elms[i], &node->elms[i+1], (count-i)*sizeof(*elm)); * with count=200, i=0 copies 200*64 = 12800 bytes starting at elms[1], * sweeping elms[1]..elms[200] -- i.e. 12800-63*64 = 8768 bytes PAST the * end of the node's elms[] array and on past the 4096-byte node buffer * into neighbouring kernel heap. This is an attacker-forced * kernel-heap write of controlled-size with partly-controlled content * (the shifted bytes are existing element data). * * This harness reproduces both primitives against a "poisoned" allocation * (filled with recognisable sentinels simulating the 4096-byte node buffer * plus adjacent kernel heap) so the OOB extents are observable and * deterministic WITHOUT needing to actually corrupt a live kernel. * * Build: cc -O2 -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> /* ---- HAMMER constants (hammer_btree.h) ---- */ #define HAMMER_BTREE_LEAF_ELMS 63 #define HAMMER_BTREE_INT_ELMS 62 #define NODE_SIZE 4096 #define ELM_SIZE 64 /* sizeof(union hammer_btree_elm) */ #define NOFF_count 16 #define NOFF_type 20 #define NOFF_elms 64 /* a recognisable "hammer_base_elm" tail so we can count OOB reads */ struct hammer_base_elm { uint8_t pad[32]; }; /* * Transcription of hammer_btree_cmp (simplified): it reads the element's * base fields. For the harness we just touch the bytes to model the read. * Returns <0 always so the loop keeps walking (maximises OOB extent). */ static int harness_btree_cmp(const uint8_t *elm_bytes) { volatile uint8_t b = elm_bytes[0]; /* model the field read */ (void)b; return 1; /* r > 1 in the leaf loop -> ++i; continue (keeps walking) */ } /* * Transcription of hammer_btree_search_node() (hammer_btree.c:1390-1413). * Binary search with s = node->count. Returns an index that can be OOB. */ static int harness_search_node(int32_t count, const uint8_t *elms_base, size_t elms_bytes) { int b = 0; int s = count; /* node->count -- FORGED to 200 */ while (s - b > 4) { int i = b + (s - b) / 2; size_t off = (size_t)i * ELM_SIZE; if (off + ELM_SIZE > elms_bytes) { printf(" [search_node] OOB READ at elms[%d] (byte +%zu, +%zu past elms[] end)\n", i, off, off - elms_bytes); } else { harness_btree_cmp(elms_base + off); } /* model: r > 1 so b = i (search rightward, maximising index) */ b = i; } return b; } int main(void) { int32_t forged_count = 200; /* what the crafted B-tree leaf carries */ printf("==== DF-0776 production (INVARIANTS-OFF) OOB primitive proof ====\n"); printf("forged node->count = %d (HAMMER_BTREE_LEAF_ELMS = %d)\n", forged_count, HAMMER_BTREE_LEAF_ELMS); printf("elms[] array holds %d elements (%d bytes); node buffer = %d bytes\n\n", HAMMER_BTREE_LEAF_ELMS, HAMMER_BTREE_LEAF_ELMS * ELM_SIZE, NODE_SIZE); /* ---- model the kernel allocator: a 4096-byte node buffer + adjacent heap ---- * In the real kernel the node is a hammer_buffer->ondisk region. elms[] * occupies [64 .. 64+63*64) = [64 .. 4096) -- i.e. elms[] ends exactly at * the node boundary. Anything at elms[63+] is PAST the 4096-byte node * buffer = neighbouring kernel heap. */ size_t total = NODE_SIZE + 65536; /* node + generous adjacent-heap model */ uint8_t *space = (uint8_t *)malloc(total); memset(space, 0xAA, total); /* node region */ memset(space + NODE_SIZE, 0xCC, total - NODE_SIZE); /* adjacent heap sentinel */ uint8_t *node = space; /* the 4096-byte node buffer */ uint8_t *elms_base = node + NOFF_elms; size_t elms_bytes = NODE_SIZE - NOFF_elms; /* 4096-64 = 4032 = 63 elms */ /* ============================================================ * (1) OOB READ -- hammer_btree_search_node + leaf linear loop * ============================================================ */ printf("---- (1) OOB READ: hammer_btree_search_node() + btree_search leaf loop ----\n"); printf(" hammer_btree_search_node(key, node) [hammer_btree.c:1390]\n"); int start = harness_search_node(forged_count, elms_base, elms_bytes); printf("\n btree_search leaf loop [hammer_btree.c:1289-1290]\n"); printf(" i = search_node_result = %d\n", start); printf(" while (i < node->count) { elm = &node->elms[i]; cmp(); ++i; }\n"); int oob_reads = 0; int first_oob = -1; int i = start; while (i < forged_count) { size_t off = (size_t)i * ELM_SIZE; if (off >= elms_bytes) { if (first_oob < 0) first_oob = i; oob_reads++; } /* model hammer_btree_cmp reading elm->leaf.base */ if (off + ELM_SIZE <= total - NOFF_elms) harness_btree_cmp(elms_base + off); ++i; } size_t oob_read_bytes = (size_t)oob_reads * ELM_SIZE; size_t past_node = 0; if (first_oob >= 0) { size_t first_off = (size_t)first_oob * ELM_SIZE; /* bytes of the read that land PAST the 4096-byte node buffer */ if (first_off > elms_bytes) past_node = oob_read_bytes; else past_node = (first_off + oob_read_bytes) - elms_bytes; } printf(" => walked elms[%d..%d]; first OOB at elms[%d]\n", start, forged_count-1, first_oob); printf(" => %d OOB element reads = %zu bytes beyond elms[] end\n", oob_reads, oob_read_bytes); printf(" => %zu bytes read PAST the 4096-byte node buffer into kernel heap\n", past_node); printf(" >>> OOB KERNEL-HEAP READ (info leak / panic on guard page) <<<\n"); /* sample the "leaked" bytes (0xCC = adjacent heap sentinel) */ if (first_oob >= 0) { size_t smp_off = (size_t)first_oob * ELM_SIZE; printf(" sample bytes from first OOB element: "); for (int k = 0; k < 16 && smp_off + k < total - NOFF_elms; k++) printf("%02x ", elms_base[smp_off + k]); printf("\n (0xCC = adjacent kernel heap; real kernel exposes live slab data)\n"); } /* ============================================================ * (2) OOB WRITE -- hammer_btree_insert() bcopy shift * ============================================================ */ printf("\n---- (2) OOB WRITE: hammer_btree_insert() bcopy [hammer_btree.c:819-824] ----\n"); /* refresh the model */ memset(space, 0xAA, NODE_SIZE); memset(space + NODE_SIZE, 0xCC, total - NODE_SIZE); /* plant a recognisable "new element" being inserted */ uint8_t new_elm[ELM_SIZE]; memset(new_elm, 0xBB, ELM_SIZE); int insert_i = 0; /* cursor->index */ printf(" count=%d, i(cursor->index)=%d\n", forged_count, insert_i); printf(" if (i != count) bcopy(&elms[i], &elms[i+1], (count-i)*sizeof(elm));\n"); size_t bcopy_bytes = (size_t)(forged_count - insert_i) * ELM_SIZE; printf(" => bcopy of %zu bytes (%d * %d)\n", bcopy_bytes, forged_count - insert_i, ELM_SIZE); /* the source starts at elms[0] (offset 0 within elms_base); dest at elms[1] */ size_t src_off = (size_t)insert_i * ELM_SIZE; size_t dst_off = (size_t)(insert_i + 1) * ELM_SIZE; size_t end_dst = dst_off + bcopy_bytes; /* end of the write, relative to elms_base */ printf(" source: elms[%d] .. elms[%d] (within/just past elms[])\n", insert_i, forged_count - 1); printf(" dest : elms[%d] .. elms[%d]\n", insert_i + 1, forged_count); /* perform the modelled bcopy into the poisoned space (clamped to total) */ size_t copy_now = bcopy_bytes; if (dst_off + copy_now > total - NOFF_elms) copy_now = (total - NOFF_elms) - dst_off; memmove(space + NOFF_elms + dst_off, space + NOFF_elms + src_off, copy_now); /* how far past the node buffer does the write go? */ size_t elms_end = (size_t)HAMMER_BTREE_LEAF_ELMS * ELM_SIZE; /* 4032 */ long past_node_write = 0; if (end_dst > elms_end) past_node_write = (long)end_dst - (long)elms_end; /* and past the 4096-byte node buffer itself? */ long past_buffer = 0; if (end_dst > (NODE_SIZE - NOFF_elms)) past_buffer = (long)end_dst - (long)(NODE_SIZE - NOFF_elms); printf(" write ends at elms_base+%zu (elms[] ends at +%zu; node buf ends at +%zu)\n", end_dst, elms_end, (size_t)(NODE_SIZE - NOFF_elms)); printf(" => %ld bytes written past elms[] end\n", past_node_write); printf(" => %ld bytes written PAST the 4096-byte node buffer into kernel heap\n", past_buffer); /* show that the adjacent-heap sentinel region got clobbered */ if (past_buffer > 0) { size_t clob = NODE_SIZE; /* first byte past the node buffer */ printf(" adjacent-heap bytes clobbered by the shift (were 0xCC): "); for (int k = 0; k < 16 && clob + k < total; k++) printf("%02x ", space[clob + k]); printf("\n (0xAA = shifted node data now overwriting neighbouring heap)\n"); } printf(" >>> OOB KERNEL-HEAP WRITE (corruption / controlled-size shift) <<<\n"); printf("\n==== summary ====\n"); printf("GENERIC (INVARIANTS ON): KKASSERT panic at hammer_btree.c:1278/818 (DoS).\n"); printf("production (INVARIANTS OFF): %zu-byte OOB heap read + %ld-byte OOB heap write\n", past_node, past_buffer); printf(" trigger: mount crafted HAMMER image (root) + unpriv ls/stat (read) /\n"); printf(" unpriv create/mkdir (write). CRC forged so load passes.\n"); free(space); return 0; } |