"""Replace the eddycoin_old_keys module inside a PyInstaller 6 onefile exe (CArchive -> PYZ) by a hashed variant. Usage (Python 3.12 = same bytecode as the exe): patch_exe.py IN.exe OUT.exe hashed_module.py [orig_module.py] Every other byte of every archive entry is kept as stored; offsets/TOC/cookie are rebuilt; result is re-parsed and checked.""" import sys, struct, zlib, marshal, importlib.util, hashlib MAGIC = b"MEI\014\013\012\013\016"; CK = "!8sIIII64s"; CKL = struct.calcsize(CK); TE = "!IIIIBc"; TEL = struct.calcsize(TE) def read_carchive(b): end = b.rfind(MAGIC); assert end > 0 magic, pkg_len, toc_off, toc_len, pyvers, pylib = struct.unpack(CK, b[end:end + CKL]) start = end + CKL - pkg_len toc = []; p = start + toc_off while p < start + toc_off + toc_len: el, doff, dlen, ulen, cf, tc = struct.unpack(TE, b[p:p + TEL]); name = b[p + TEL:p + el].rstrip(b"\0").decode() toc.append([name, doff, dlen, ulen, cf, tc]); p += el return start, end, (pyvers, pylib), toc def read_pyz(z): assert z[:4] == b"PYZ\0"; pymagic = z[4:8]; off = struct.unpack("!i", z[8:12])[0]; toc = marshal.loads(z[off:]) return pymagic, toc def build_pyz(pymagic, entries): # entries: list of (name, typ, compressed_bytes) out = bytearray(b"PYZ\0" + pymagic + b"\0\0\0\0"); toc = [] for name, typ, data in entries: toc.append((name, (typ, len(out), len(data)))); out += data struct.pack_into("!i", out, 8, len(out)); out += marshal.dumps(toc); return bytes(out) def main(inp, outp, hashed, orig=None): b = open(inp, "rb").read(); start, end, (pyvers, pylib), toc = read_carchive(b) pyz_i = [i for i, t in enumerate(toc) if t[5] == b"z"]; assert len(pyz_i) == 1 t = toc[pyz_i[0]]; pyz = b[start + t[1]:start + t[1] + t[2]]; assert t[4] == 0, "PYZ entry stored uncompressed expected" pymagic, ptoc = read_pyz(pyz); assert pymagic == importlib.util.MAGIC_NUMBER, "python bytecode version mismatch" ents = []; found = 0 for name, (typ, pos, ln) in (ptoc if isinstance(ptoc, list) else ptoc.items()): data = pyz[pos:pos + ln] if name == "eddycoin_old_keys": oldcode = marshal.loads(zlib.decompress(data)) if orig: # the module in the exe must be the one we hashed oc = compile(open(orig, encoding="utf-8").read(), oldcode.co_filename, "exec") assert oc.co_consts == oldcode.co_consts or marshal.dumps(oc.co_consts) == marshal.dumps(oldcode.co_consts) or sorted(map(repr, oc.co_consts)) == sorted(map(repr, oldcode.co_consts)), "embedded eddycoin_old_keys differs from " + orig newcode = compile(open(hashed, encoding="utf-8").read(), oldcode.co_filename, "exec") data = zlib.compress(marshal.dumps(newcode), 9); found += 1 ents.append((name, typ, data)) assert found == 1 newpyz = build_pyz(pymagic, ents) # rebuild package: entries in original order, new offsets pkg = bytearray(); newtoc = [] for i, (name, doff, dlen, ulen, cf, tc) in enumerate(toc): data = newpyz if i == pyz_i[0] else b[start + doff:start + doff + dlen] newtoc.append((name, len(pkg), len(data), len(data) if i == pyz_i[0] else ulen, cf, tc)); pkg += data tocb = bytearray() for name, doff, dlen, ulen, cf, tc in newtoc: nb = name.encode() + b"\0"; el = TEL + len(nb); el += (16 - el % 16) % 16 tocb += struct.pack(TE, el, doff, dlen, ulen, cf, tc) + nb.ljust(el - TEL, b"\0") toc_off = len(pkg); pkg += tocb pkg_len = len(pkg) + CKL cookie = struct.pack(CK, MAGIC, pkg_len, toc_off, len(tocb), pyvers, pylib) tail = b[end + CKL:] # anything after the cookie (normally nothing) out = b[:start] + bytes(pkg) + cookie + tail open(outp, "wb").write(out) # re-parse + verify s2, e2, _, toc2 = read_carchive(out); assert [x[0] for x in toc2] == [x[0] for x in toc] for (n1, o1, l1, *_), (n2, o2, l2, *_r) in zip(toc, toc2): if n1 == toc[pyz_i[0]][0]: continue assert b[start + o1:start + o1 + l1] == out[s2 + o2:s2 + o2 + l2], n1 t2 = toc2[pyz_i[0]]; pm, pt = read_pyz(out[s2 + t2[1]:s2 + t2[1] + t2[2]]) print("patched %s -> %s (%d -> %d B), bootloader %d B unchanged, %d archive entries, PYZ %d modules" % (inp, outp, len(b), len(out), start, len(toc), len(pt))) if __name__ == "__main__": main(*sys.argv[1:])