DF-0771 / df0771_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 | /* * DF-0771 — deterministic code-level harness reproducing the * unvalidated-data_len logic in hammer_get_inode(). * * Cited bug: sys/vfs/hammer/hammer_inode.c:524-525 * ip->ino_leaf = cursor.node->ondisk->elms[cursor.index].leaf; * ip->ino_data = cursor.data->inode; <-- 128-byte struct copy * * cursor.data is set by hammer_btree_extract() (hammer_btree.c:737) to * cursor->data = hammer_bread_ext(hmp, data_off, data_len, ...); * which returns (char *)buffer->ondisk + xoff where xoff = data_off & 0x3FFF. * The ONLY length validation anywhere on the path is (hammer_btree.c:734) * KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE); // <= 65536 * i.e. data_len is NEVER checked against sizeof(struct hammer_inode_data). * * The CRC gate that *should* catch a wrong-sized inode record is bypassable: * hammer_crc.h:267-271 hammer_crc_get_leaf(): * case HAMMER_RECTYPE_INODE: * if (leaf->data_len != sizeof(struct hammer_inode_data)) * return(0); // <-- returns 0 for a wrong-sized inode * hammer_crc.h:294-296 hammer_crc_test_leaf(): * if (leaf->data_crc == hammer_crc_get_leaf(...)) // on-disk 0 == 0 * return(1); // <-- CRC PASSES * * So a crafted image whose root-inode B-Tree leaf has * data_len = 1 (anything != 128) * data_crc = 0 * sails through the CRC test, and the 128-byte struct copy at line 525 then * reads 128 bytes starting at cursor.data — which, if the on-disk * data_offset's within-buffer xoff is > (HAMMER_BUFSIZE - 128) = 16256, * reads PAST the 16 KiB buffer into adjacent kernel heap. The leaked bytes * populate ip->ino_data.{mode, uid, gid, size, ext.symlink[24], mtime, atime}, * which are returned to userspace via stat()/readlink() => kernel heap * info leak (KASLR-defeat class). Requires mount capability * (root/operator/removable media). * * This harness reproduces BOTH halves of the bug with the real kernel * structs and the verbatim CRC inline logic, without needing a crafted * HAMMER image. crc32/iscsi_crc32 are stubbed because the bypass path * returns 0 before ever calling them (that is precisely the defect). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> typedef uint32_t hammer_crc_t; typedef uint64_t hammer_off_t; typedef uint64_t hammer_tid_t; /* ---- copied verbatim from sys/vfs/hammer/hammer_disk.h ---- */ #define HAMMER_RECTYPE_INODE 0x0001 typedef char hammer_uuid_t[16]; typedef struct hammer_inode_data { uint16_t version; uint16_t mode; uint32_t uflags; uint32_t rmajor; uint32_t rminor; uint64_t ctime; int64_t parent_obj_id; hammer_uuid_t uid; hammer_uuid_t gid; uint8_t obj_type; uint8_t cap_flags; uint16_t reserved01; uint32_t reserved02; uint64_t nlinks; uint64_t size; union { char symlink[24]; } ext; uint64_t mtime; uint64_t atime; } *hammer_inode_data_t; #define HAMMER_INODE_CRCSIZE (offsetof(struct hammer_inode_data, mtime)) /* ---- copied verbatim from sys/vfs/hammer/hammer_btree.h ---- */ typedef struct hammer_base_elm { int64_t obj_id; int64_t key; hammer_tid_t create_tid; hammer_tid_t delete_tid; uint16_t rec_type; uint8_t obj_type; uint8_t btype; uint32_t localization; } *hammer_base_elm_t; typedef struct hammer_btree_leaf_elm { struct hammer_base_elm base; uint32_t created_ts; uint32_t deleted_ts; hammer_off_t data_offset; int32_t data_len; hammer_crc_t data_crc; } *hammer_btree_leaf_elm_t; /* ---- crc32 / iscsi_crc32 stubs: NOT reached on the bypass path ---- */ static hammer_crc_t crc32(const void *buf, size_t n) { (void)buf; (void)n; return 0xDEAD; } static hammer_crc_t iscsi_crc32(const void *buf, size_t n) { (void)buf; (void)n; return 0xBEEF; } #define HAMMER_VOL_VERSION_SEVEN 7 #define HAMMER_VOL_VERSION_SIX 6 static hammer_crc_t hammer_datacrc(uint32_t vol_version, const void *buf, size_t size) { return (vol_version >= HAMMER_VOL_VERSION_SEVEN) ? iscsi_crc32(buf, size) : crc32(buf, size); } /* ---- copied verbatim from sys/vfs/hammer/hammer_crc.h:263-274 ---- */ static hammer_crc_t hammer_crc_get_leaf(uint32_t vol_version, const void *data, hammer_btree_leaf_elm_t leaf) { hammer_crc_t crc; if (leaf->data_len == 0) return(0); switch(leaf->base.rec_type) { case HAMMER_RECTYPE_INODE: if (leaf->data_len != sizeof(struct hammer_inode_data)) return(0); /* "This shouldn't happen" -- but it does for a crafted image */ crc = hammer_datacrc(vol_version, data, HAMMER_INODE_CRCSIZE); break; default: crc = hammer_datacrc(vol_version, data, leaf->data_len); break; } return(crc); } /* ---- copied verbatim from sys/vfs/hammer/hammer_crc.h:293-302 ---- */ static int hammer_crc_test_leaf(uint32_t vol_version, const void *data, hammer_btree_leaf_elm_t leaf) { if (leaf->data_crc == hammer_crc_get_leaf(vol_version, data, leaf)) return(1); if (vol_version >= HAMMER_VOL_VERSION_SEVEN) { if (leaf->data_crc == hammer_crc_get_leaf(HAMMER_VOL_VERSION_SIX, data, leaf)) return(1); } return(0); } /* ---- simulate hammer_get_inode lines 523-525 ---- */ struct hammer_inode_mem { struct hammer_btree_leaf_elm ino_leaf; struct hammer_inode_data ino_data; }; static void load_inode_like_kernel(struct hammer_inode_mem *ip, hammer_btree_leaf_elm_t leaf, const void *cursor_data) { /* hammer_inode.c:524-525 */ ip->ino_leaf = *leaf; /* struct copy of leaf */ ip->ino_data = *(hammer_inode_data_t)cursor_data; /* 128-byte struct copy */ } int main(void) { printf("DF-0771 harness: sizeof(struct hammer_inode_data) = %zu\n", sizeof(struct hammer_inode_data)); /* ============================================================ * TEST 1: CRC gate is bypassable for a wrong-sized INODE record. * ============================================================ */ struct hammer_btree_leaf_elm bad_leaf; memset(&bad_leaf, 0, sizeof(bad_leaf)); bad_leaf.base.rec_type = HAMMER_RECTYPE_INODE; bad_leaf.data_len = 1; /* NOT sizeof(inode_data) (128) */ bad_leaf.data_crc = 0; /* crafted on-disk CRC = 0 */ bad_leaf.data_offset= 0; int crc_ok_v6 = hammer_crc_test_leaf(HAMMER_VOL_VERSION_SIX, (void*)"x", &bad_leaf); int crc_ok_v7 = hammer_crc_test_leaf(HAMMER_VOL_VERSION_SEVEN, (void*)"x", &bad_leaf); printf("CRC test for INODE leaf with data_len=1, data_crc=0:\n"); printf(" vol_version=6 -> %s (1=CRC PASSES = inode loaded)\n", crc_ok_v6 ? "PASS" : "FAIL"); printf(" vol_version=7 -> %s (1=CRC PASSES = inode loaded)\n", crc_ok_v7 ? "PASS" : "FAIL"); if (crc_ok_v6 && crc_ok_v7) { printf("BUG PART 1 CONFIRMED: CRC gate bypassed -- wrong-sized INODE record\n" " is accepted because hammer_crc_get_leaf() returns 0 for INODE with\n" " data_len!=sizeof(inode_data), and on-disk data_crc=0 matches 0==0.\n"); } else { printf("CRC gate NOT bypassed -- finding would be a false positive on CRC half.\n"); } /* ============================================================ * TEST 2: struct copy reads past the 16 KiB HAMMER data buffer * when data_offset's xoff > (16384 - 128) = 16256. * * hammer_bread_ext() returns buffer->ondisk + xoff. The struct * copy at hammer_inode.c:525 then reads 128 bytes unconditionally. * ============================================================ */ #define HAMMER_BUFSIZE 16384 /* Allocate one big region, treat first 16 KiB as the "HAMMER data buffer" and fill the bytes just past it with a recognizable leak marker so we can see the OOB read picking them up. */ size_t total = HAMMER_BUFSIZE + 256; /* 16 KiB buffer + adjacent-heap stand-in */ unsigned char *big = calloc(1, total); /* fill the "HAMMER buffer" region with 0x11, the "adjacent heap" with 0x77 */ memset(big, 0x11, HAMMER_BUFSIZE); memset(big + HAMMER_BUFSIZE, 0x77, 256); /* attacker-controlled data_offset with xoff = 16280 (within the 16 KiB buffer: only 16384-16280 = 104 bytes remain; struct copy needs 128) */ int xoff = 16280; unsigned char *cursor_data = big + xoff; /* == what hammer_bread_ext returns */ struct hammer_inode_mem ip; load_inode_like_kernel(&ip, &bad_leaf, cursor_data); /* The struct copy read bytes [xoff .. xoff+128). Bytes [HAMMER_BUFSIZE .. xoff+128) are PAST the buffer. */ int oob_start = HAMMER_BUFSIZE; /* first byte past buffer */ int oob_end = xoff + (int)sizeof(struct hammer_inode_data); int oob_bytes = (oob_end > oob_start) ? (oob_end - oob_start) : 0; printf("\nTEST 2: xoff=%d, struct copy reads 128 bytes at [%d..%d)\n", xoff, xoff, xoff + (int)sizeof(struct hammer_inode_data)); printf(" HAMMER buffer occupies [0..%d). OOB region = [%d..%d) = %d bytes past buffer end.\n", HAMMER_BUFSIZE, oob_start, oob_end, oob_bytes); /* Show that the copied inode fields contain bytes from PAST the buffer (the 0x77 marker from the adjacent-heap stand-in). The tail of the inode struct (mtime/atime = last 16 bytes) is the part that lands in the OOB region for xoff=16280. */ unsigned char *ino_bytes = (unsigned char *)&ip.ino_data; int leaked_77 = 0; for (int i = 0; i < (int)sizeof(struct hammer_inode_data); i++) { if (ino_bytes[i] == 0x77) leaked_77++; } printf(" inode_data bytes copied from PAST the 16 KiB buffer (leaked 0x77 marker): %d\n", leaked_77); printf(" inode_data.atime (last 8 bytes, all in OOB region) hex:"); for (int i = sizeof(struct hammer_inode_data) - 8; i < (int)sizeof(struct hammer_inode_data); i++) printf(" %02x", ino_bytes[i]); printf("\n"); if (oob_bytes > 0 && leaked_77 > 0) { printf("BUG PART 2 CONFIRMED: struct copy at hammer_inode.c:525 read %d bytes\n" " past the 16 KiB HAMMER data buffer end into adjacent kernel heap.\n" " Those leaked bytes populate inode_data fields (mode/uid/gid/size/\n" " ext.symlink[24]/mtime/atime) returned to userspace via stat()/readlink().\n", oob_bytes); } /* ============================================================ * VERDICT * ============================================================ */ int bug_repro = (crc_ok_v6 && crc_ok_v7 && oob_bytes > 0 && leaked_77 > 0); printf("\n==== DF-0771 %s ====\n", bug_repro ? "REPRODUCED (info-leak / heap OOB read)" : "NOT REPRODUCED"); printf("Threat model: crafted HAMMER filesystem image mounted by root/operator/\n" " removable media. Realistic impact ceiling: kernel heap info leak\n" " (KASLR-defeat class); NOT a write primitive -> no privilege-escalation\n" " chain derivable (read-only primitive = valid Phase-6 stop).\n"); free(big); return bug_repro ? 0 : 1; } |