Files
reverse-skills-max/reverse-skill-repo/scripts/pe_behavior.py

83 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Behavior-focused static reverse: resolve the real main from the CRT stub,
map every import call-site in .text to its API, dump resource directory + carve
embedded PE/streams. No IDA. x64 only for call-site RIP resolution.
Usage: python pe_behavior.py <path.exe>
"""
from __future__ import annotations
import sys, struct
import pefile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from capstone.x86 import X86_OP_MEM, X86_OP_IMM, X86_REG_RIP
def main():
path = sys.argv[1]
pe = pefile.PE(path, fast_load=False)
base = pe.OPTIONAL_HEADER.ImageBase
md = Cs(CS_ARCH_X86, CS_MODE_64); md.detail = True
# IAT slot VA -> name
iat = {}
for entry in getattr(pe, "DIRECTORY_ENTRY_IMPORT", []) or []:
dll = entry.dll.decode("latin1","replace")
for imp in entry.imports:
nm = imp.name.decode("latin1","replace") if imp.name else f"ord{imp.ordinal}"
if imp.address: iat[imp.address] = f"{dll.split('.')[0]}!{nm}"
text = [s for s in pe.sections if s.Name.rstrip(b'\x00')==b'.text'][0]
code = text.get_data()
text_va = base + text.VirtualAddress
# Count import call-sites across whole .text (linear sweep).
from collections import Counter
calls = Counter()
callsites = {} # name -> [addrs]
for insn in md.disasm(code, text_va):
if insn.mnemonic in ("call","jmp") and insn.operands:
op = insn.operands[-1]
tgt = None
if op.type == X86_OP_MEM and op.mem.base in (0, X86_REG_RIP):
tgt = insn.address + insn.size + op.mem.disp
elif op.type == X86_OP_IMM:
tgt = op.imm
if tgt in iat:
nm = iat[tgt]; calls[nm]+=1
callsites.setdefault(nm, []).append(insn.address)
print(f"# {path}")
print(f"imagebase={hex(base)} .text_va={hex(text_va)} size={len(code)}")
print("\n== import call-site frequency (behavior surface) ==")
for nm,c in calls.most_common():
addrs = ",".join(hex(a) for a in callsites[nm][:4])
print(f" {c:3}x {nm:<32} @ {addrs}")
# Resource directory dump + carve
print("\n== resources ==")
if hasattr(pe,"DIRECTORY_ENTRY_RESOURCE"):
for t in pe.DIRECTORY_ENTRY_RESOURCE.entries:
tn = pefile.RESOURCE_TYPE.get(t.id, t.id)
for r in getattr(t,"directory",{}).entries if hasattr(t,"directory") else []:
for l in getattr(r,"directory",{}).entries if hasattr(r,"directory") else []:
d = l.data.struct
print(f" type={tn} id={r.id} size={d.Size} rva={hex(d.OffsetToData)}")
# Whole-file embedded PE scan (extractable child?)
b = open(path,"rb").read()
print("\n== embedded PE scan ==")
cnt=0
for i in range(0, len(b)-0x100):
if b[i:i+2]==b'MZ':
e = i + int.from_bytes(b[i+0x3c:i+0x40],'little')
if 0<e<len(b)-4 and b[e:e+4]==b'PE\x00\x00':
mach = int.from_bytes(b[e+4:e+6],'little')
if mach in (0x8664,0x14c):
cnt+=1
if cnt<=8: print(f" child PE @ file+{hex(i)} machine={hex(mach)}")
print(f" total embedded PE headers: {cnt}")
if __name__ == "__main__":
main()