130 lines
6.1 KiB
Python
130 lines
6.1 KiB
Python
"""Generate mods/brokenworld/cpuid_sites.h: the CPUID sites in Broken World's crack-rebuilt .rld section
|
|
(SecuROM virtual-machine dispatchers; see mods/brokenworld/brokenworld.cpp, FastCpuid).
|
|
|
|
usage: python -I tools/bw_cpuid_sites.py [BW_EXE] [--check PID]
|
|
A site is a CPUID followed, within 14 instructions (unconditional jumps followed), by "mov eax, 0xf41" and
|
|
"pop ebx" (the dispatcher shape). To make room for a 5-byte jump the straight-line instructions after it are
|
|
copied to the stub, or an unconditional jump right after it (whose skipped bytes cover the patch) becomes the
|
|
stub's exit. Each entry has a CRC32 of the overwritten bytes, checked before patching (other builds are left
|
|
alone). --check PID samples a running game and reports which
|
|
CPUID sites its main thread is waiting on and whether they are in the table.
|
|
"""
|
|
import re, struct, sys, zlib
|
|
from capstone import Cs, CS_ARCH_X86, CS_MODE_32, CS_GRP_JUMP, CS_GRP_CALL, CS_GRP_RET
|
|
|
|
EXE = 'DungeonSiege2.exe' # Broken World v2.30.0.4277 (or pass its path as the first argument)
|
|
|
|
|
|
def follow(md, d, off_of, va, limit=14):
|
|
"""Instructions from va, following unconditional direct jumps (junk is hidden behind them)."""
|
|
out = []
|
|
while len(out) < limit:
|
|
o = off_of(va)
|
|
ins = list(md.disasm(d[o:o + 16], va))[:1]
|
|
if not ins:
|
|
break
|
|
i = ins[0]
|
|
out.append(i)
|
|
if i.mnemonic == 'jmp' and i.op_str.startswith('0x'):
|
|
va = int(i.op_str, 16)
|
|
elif i.group(CS_GRP_JUMP) or i.group(CS_GRP_CALL) or i.group(CS_GRP_RET):
|
|
break
|
|
else:
|
|
va = i.address + i.size
|
|
return out
|
|
|
|
|
|
def sites(exe):
|
|
d = open(exe, 'rb').read()
|
|
pe = struct.unpack_from('<I', d, 0x3c)[0]
|
|
n = struct.unpack_from('<H', d, pe + 6)[0]
|
|
so = struct.unpack_from('<H', d, pe + 20)[0]
|
|
secs = {s[0].rstrip(b'\0').decode(): s for s in (struct.unpack_from('<8sIIII', d, pe + 24 + so + i * 40) for i in range(n))}
|
|
_, vs, v, rs, ra = secs['.rld']
|
|
off_of = lambda a: ra + a - 0x400000 - v
|
|
md = Cs(CS_ARCH_X86, CS_MODE_32)
|
|
md.detail = True
|
|
out, skipped = [], []
|
|
for m in re.finditer(rb'\x0f\xa2', d[ra:ra + rs]):
|
|
off = ra + m.start()
|
|
va = 0x400000 + v + m.start()
|
|
flow = follow(md, d, off_of, va + 2)
|
|
txt = ['%s %s' % (i.mnemonic, i.op_str) for i in flow]
|
|
if not ('mov eax, 0xf41' in txt and 'pop ebx' in txt):
|
|
continue
|
|
# Make room for a 5-byte jump: copy the straight-line instructions after the CPUID (>= 3 bytes), or stop at
|
|
# an unconditional jump whose target lies beyond the patch (the bytes it skips are never executed).
|
|
copy, target, ok = 0, 0, False
|
|
a = va + 2
|
|
while True:
|
|
i = list(md.disasm(d[off_of(a):off_of(a) + 16], a))[:1]
|
|
if not i:
|
|
break
|
|
i = i[0]
|
|
if i.mnemonic == 'jmp' and i.op_str.startswith('0x'):
|
|
target = int(i.op_str, 16)
|
|
ok = target >= va + 5 and target >= i.address + i.size
|
|
end = va + 2 + copy + i.size
|
|
break
|
|
if i.group(CS_GRP_JUMP) or i.group(CS_GRP_CALL) or i.group(CS_GRP_RET) or i.mnemonic.startswith('loop'):
|
|
break
|
|
copy += i.size
|
|
a += i.size
|
|
if copy >= 3:
|
|
ok, end = True, va + 2 + copy
|
|
break
|
|
if not ok:
|
|
skipped.append(va)
|
|
continue
|
|
span = max(5, end - va)
|
|
out.append((va, copy, span, target, zlib.crc32(d[off:off + span]) & 0xffffffff))
|
|
return out, skipped
|
|
|
|
|
|
def main():
|
|
a = [x for x in sys.argv[1:] if not x.startswith('--')]
|
|
exe = a[0] if a and not a[0].isdigit() else EXE
|
|
table, skipped = sites(exe)
|
|
if '--check' in sys.argv:
|
|
import collections, ctypes, time
|
|
sys.path.insert(0, 'tools')
|
|
import cpuprof as cp
|
|
pid = int(sys.argv[sys.argv.index('--check') + 1])
|
|
proc = cp.k32.OpenProcess(cp.PROCESS_ACCESS, False, pid)
|
|
best, mh = -1, None
|
|
for tid in cp.thread_ids(pid):
|
|
h = cp.k32.OpenThread(cp.THREAD_ACCESS, False, tid)
|
|
t = cp.cpu_time(h)
|
|
if t > best:
|
|
best, mh = t, h
|
|
hits, total, ctx = collections.Counter(), 0, cp.WOW64_CONTEXT()
|
|
for i in range(400):
|
|
cp.k32.Wow64SuspendThread(mh)
|
|
ctx.ContextFlags = 0x10001
|
|
cp.k32.Wow64GetThreadContext(mh, ctypes.byref(ctx))
|
|
cp.k32.ResumeThread(mh)
|
|
total += 1
|
|
if cp.read(proc, ctx.Eip - 2, 2) == b'\x0f\xa2':
|
|
hits[ctx.Eip - 2] += 1
|
|
time.sleep(0.01)
|
|
known = {e[0] for e in table}
|
|
print('%d of %d samples right after a CPUID' % (sum(hits.values()), total))
|
|
for va, c in hits.most_common():
|
|
print(' %08X %3d %s' % (va, c, 'in table' if va in known else ('SKIPPED (branch)' if va in skipped else 'NOT IN TABLE')))
|
|
return
|
|
lines = ['// Generated by tools/bw_cpuid_sites.py from Broken World DungeonSiege2.exe v2.30.0.4277 (PE timestamp',
|
|
'// 0x449B02B3): CPUID sites in the crack-rebuilt .rld section. {VA of the CPUID, bytes after it copied to',
|
|
'// the stub, bytes overwritten by the patch, where the stub continues (0 = right after the copied bytes),',
|
|
'// CRC32 of the overwritten bytes}. No code bytes are stored; they are copied from the running exe.',
|
|
'#pragma once', '', 'struct CpuidSite {', ' unsigned long va;', ' unsigned char copy;',
|
|
' unsigned char span;', ' unsigned long next;', ' unsigned long crc;', '};', '',
|
|
'constexpr CpuidSite kCpuidSites[] = {']
|
|
for va, copy, span, target, crc in table:
|
|
lines.append(' {0x%08X, %d, %d, 0x%08X, 0x%08X},' % (va, copy, span, target, crc))
|
|
lines.append('};')
|
|
open('mods/brokenworld/cpuid_sites.h', 'w', newline='\r\n').write('\n'.join(lines) + '\n')
|
|
print('%d sites written, %d skipped (branch right after the CPUID): %s' % (len(table), len(skipped), ' '.join('%08X' % s for s in skipped)))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|