reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
82
reverse-skill-repo/scripts/pe_behavior.py
Normal file
82
reverse-skill-repo/scripts/pe_behavior.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user