Files
GaCy-GO/tools/cw_map_getter_layout.py
T

88 lines
4.9 KiB
Python
Raw Normal View History

"""Inspect explicit runtime-resolved getter RVAs, never search an assembly's types."""
import argparse
import hashlib
import json
import pathlib
import struct
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CS_OP_MEM, CS_OP_REG
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--binary', type=pathlib.Path, required=True)
p.add_argument('--handles', type=pathlib.Path, required=True)
p.add_argument('--output', type=pathlib.Path, required=True)
args = p.parse_args()
data = args.binary.read_bytes()
pe = struct.unpack_from('<I', data, 0x3c)[0]
if data[:2] != b'MZ' or data[pe:pe+4] != b'PE\0\0':
raise SystemExit('Expected PE image')
count = struct.unpack_from('<H', data, pe+6)[0]
optional_size = struct.unpack_from('<H', data, pe+20)[0]
sections = [struct.unpack_from('<IIII', data, pe+24+optional_size+i*40+8) for i in range(count)]
def offset(rva, size):
for _, start, length, raw in sections:
if start <= rva and rva+size <= start+length:
return raw+rva-start
raise ValueError('RVA range outside raw PE sections')
exception_rva, exception_size = struct.unpack_from('<II', data, pe+24+112+3*8)
base = offset(exception_rva, exception_size)
bounds = [struct.unpack_from('<III', data, off)[:2] for off in range(base, base+exception_size, 12)]
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
decoder.detail = True
rows = []
for item in json.loads(args.handles.read_text(encoding='utf-8-sig')):
rva = int(item['rva'], 0)
matching = [(b, e) for b, e in bounds if b == rva and e-b <= 65536]
if not item['executable']:
raise SystemExit('Getter handle is not executable')
if len(matching) == 1:
_, end = matching[0]
boundary = 'AMD64 exception table'
else:
# AMD64 leaf getters can omit unwind entries. Admit only a tiny
# straight-line load/return sequence, without calls or branches.
leaf = list(decoder.disasm(data[offset(rva,64):offset(rva,64)+64],rva))
first_ret = next((i for i,x in enumerate(leaf) if x.mnemonic=='ret'),None)
if first_ret is None or first_ret>8 or any(x.mnemonic not in ('mov','movzx','movss','ret','nop','endbr64') for x in leaf[:first_ret+1]):
rows.append({**item,'direct_return_loads':[],'limitation':'No exception entry or proven straight-line leaf getter'})
continue
end = leaf[first_ret].address+leaf[first_ret].size
boundary = 'Runtime method entry, straight-line leaf return'
instructions = list(decoder.disasm(data[offset(rva,end-rva):offset(rva,end-rva)+end-rva],rva))
loads = []
aliases = {'rcx'}
for i, ins in enumerate(instructions):
# A later dereference into _NodeParam must not be mistaken for
# an offset on this. Track overwritten registers and call clobbers.
before = set(aliases)
if ins.mnemonic == 'call':
aliases.difference_update({'rax','rcx','rdx','r8','r9','r10','r11'})
if ins.operands and ins.operands[0].type == CS_OP_REG:
dest = ins.reg_name(ins.operands[0].reg)
if ins.mnemonic == 'mov' and len(ins.operands)==2 and ins.operands[1].type==CS_OP_REG and ins.reg_name(ins.operands[1].reg) in before:
aliases.add(dest)
elif ins.mnemonic not in ('cmp','test','push'):
aliases.discard(dest)
if ins.mnemonic not in ('mov', 'movzx', 'movss') or len(ins.operands) != 2:
continue
src = ins.operands[1]
if src.type != CS_OP_MEM or src.mem.index or ins.reg_name(src.mem.base) not in before:
continue
if ins.reg_name(ins.operands[0].reg) not in ('eax', 'rax', 'xmm0'):
continue
after = instructions[i+1:i+7]
epilogue = []
for following in after:
epilogue.append(following)
if following.mnemonic == 'ret':
break
if not epilogue or epilogue[-1].mnemonic != 'ret' or any(x.mnemonic not in ('add','pop','ret','nop') for x in epilogue):
continue
loads.append({'offset':src.mem.disp,'size':src.size,'instruction':f'{ins.address:x}: {ins.mnemonic} {ins.op_str}'})
rows.append({**item,'boundary':boundary,'direct_return_loads':loads})
args.output.write_text(json.dumps({'assembly_sha256':hashlib.sha256(data).hexdigest(),
'scope':'Explicit named runtime getter handles; direct baseline return loads only, not computed getter semantics',
'getters':rows},indent=2)+'\n',encoding='utf-8')
print(f'Inspected {len(rows)} named getters; {sum(len(x["direct_return_loads"])==1 for x in rows)} have a unique direct return load')
if __name__ == '__main__': main()