SE2Revival/tools/pe_rename_exports.py
2026-10-23 20:47:44 -04:00

79 lines
3.9 KiB
Python

"""Rename a DLL's exports after linking: python -I tools/pe_rename_exports.py IN.dll OUT.dll MAP.txt
MAP.txt: lines "placeholder<TAB>new name". MSVC's linker cuts an aliased export name at its first '@' (in a .def file
and in /EXPORT alike), so decorated C++ names cannot be exported under another symbol's code directly. Instead the DLL
is linked with placeholder export names, and this script writes a new export directory - the same functions (export
address table and ordinals unchanged), names replaced and sorted (the loader and GetProcAddress search the name table
by binary search) - into a new section, and points the export data directory at it.
"""
import struct, sys
def main():
src, dst, mapfile = sys.argv[1:4]
rename = dict(line.rstrip('\n').split('\t') for line in open(mapfile, encoding='latin1') if '\t' in line)
d = bytearray(open(src, 'rb').read())
pe = struct.unpack_from('<I', d, 0x3c)[0]
nsec = struct.unpack_from('<H', d, pe + 6)[0]
opt_size = struct.unpack_from('<H', d, pe + 20)[0]
opt = pe + 24
sec_align, file_align = struct.unpack_from('<II', d, opt + 32)
dd = opt + 96 # data directories (PE32)
exp_rva, exp_size = struct.unpack_from('<II', d, dd)
shdr = opt + opt_size
secs = [struct.unpack_from('<8sIIIIIIHHI', d, shdr + 40 * i) for i in range(nsec)]
def off(rva):
for s in secs:
if s[2] <= rva < s[2] + max(s[1], s[3]):
return s[4] + rva - s[2]
raise ValueError(hex(rva))
e = off(exp_rva)
(_, _, _, _, name_rva, base, n_funcs, n_names, eat_rva, names_rva, ords_rva) = struct.unpack_from('<IIHHIIIIIII', d, e)
dll_name = d[off(name_rva):d.index(b'\0', off(name_rva))]
entries, old_names = [], set()
for i in range(n_names):
nr = struct.unpack_from('<I', d, off(names_rva) + 4 * i)[0]
old = d[off(nr):d.index(b'\0', off(nr))].decode('latin1')
old_names.add(old)
ordinal = struct.unpack_from('<H', d, off(ords_rva) + 2 * i)[0]
entries.append((rename.get(old, old), ordinal))
missing = [p for p in rename if p not in old_names]
if missing:
sys.exit('placeholders not exported: %s' % missing[:5])
entries.sort(key=lambda e: e[0].encode('latin1')) # byte order, as the loader compares
# new section layout: directory (40) | name pointers | ordinals | dll name | names
new_rva = (secs[-1][2] + max(secs[-1][1], secs[-1][3]) + sec_align - 1) // sec_align * sec_align
blob = bytearray(40)
ptrs_at = len(blob)
blob += b'\0' * (4 * len(entries))
ords_at = len(blob)
blob += b'\0' * (2 * len(entries))
name_at = len(blob)
blob += dll_name + b'\0'
for i, (name, ordinal) in enumerate(entries):
struct.pack_into('<I', blob, ptrs_at + 4 * i, new_rva + len(blob))
struct.pack_into('<H', blob, ords_at + 2 * i, ordinal)
blob += name.encode('latin1') + b'\0'
struct.pack_into('<IIHHIIIIIII', blob, 0, 0, 0, 0, 0, new_rva + name_at, base, n_funcs, len(entries), eat_rva,
new_rva + ptrs_at, new_rva + ords_at)
vsize = len(blob)
raw_size = (vsize + file_align - 1) // file_align * file_align
raw_ptr = (len(d) + file_align - 1) // file_align * file_align
if shdr + 40 * (nsec + 1) > secs[0][4]:
sys.exit('no room for another section header')
struct.pack_into('<8sIIIIIIHHI', d, shdr + 40 * nsec, b'.edata2', vsize, new_rva, raw_size, raw_ptr, 0, 0, 0, 0,
0x40000040) # initialized data, readable
struct.pack_into('<H', d, pe + 6, nsec + 1)
struct.pack_into('<I', d, opt + 56, (new_rva + vsize + sec_align - 1) // sec_align * sec_align) # SizeOfImage
struct.pack_into('<II', d, dd, new_rva, vsize)
d += b'\0' * (raw_ptr - len(d))
d += blob + b'\0' * (raw_size - vsize)
open(dst, 'wb').write(bytes(d))
print('%s: %d exports renamed into section .edata2 (%d bytes)' % (dst, len(entries), vsize))
if __name__ == '__main__':
main()