89 lines
4.9 KiB
Python
89 lines
4.9 KiB
Python
"""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()
|