83 lines
3.2 KiB
Python
Executable File
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()
|