104 lines
4.0 KiB
Python
Executable File
104 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Deep PE static reverse without IDA: capstone linear+entry disasm, IAT thunk
|
|
resolution, call-site mapping to imports, and payload/container carving.
|
|
|
|
Usage: python pe_deep.py <path.exe> [--disasm-entry N] [--carve]
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse, sys
|
|
import pefile
|
|
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CS_MODE_32
|
|
|
|
|
|
def rva_to_off(pe, rva):
|
|
return pe.get_offset_from_rva(rva)
|
|
|
|
|
|
def build_iat_map(pe):
|
|
"""Map IAT slot VA -> imported name (for call [rip+x] / call [iat] resolution)."""
|
|
m = {}
|
|
if not hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
|
|
return m
|
|
base = pe.OPTIONAL_HEADER.ImageBase
|
|
for entry in pe.DIRECTORY_ENTRY_IMPORT:
|
|
dll = entry.dll.decode("latin1", "replace")
|
|
for imp in entry.imports:
|
|
nm = imp.name.decode("latin1", "replace") if imp.name else f"{dll}#ord{imp.ordinal}"
|
|
if imp.address is not None:
|
|
m[imp.address] = f"{dll}!{nm}" # absolute VA of the IAT slot user
|
|
# imp.address is the VA where the pointer is used; also map the thunk RVA
|
|
return m
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("path")
|
|
ap.add_argument("--disasm-entry", type=int, default=120, help="instructions from entrypoint")
|
|
ap.add_argument("--carve", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
pe = pefile.PE(args.path, fast_load=False)
|
|
is64 = hex(pe.OPTIONAL_HEADER.Magic) == "0x20b"
|
|
md = Cs(CS_ARCH_X86, CS_MODE_64 if is64 else CS_MODE_32)
|
|
md.detail = True
|
|
base = pe.OPTIONAL_HEADER.ImageBase
|
|
ep_rva = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
|
ep_va = base + ep_rva
|
|
print(f"# {args.path}")
|
|
print(f"imagebase={hex(base)} entry_va={hex(ep_va)} bits={'64' if is64 else '32'}")
|
|
|
|
# Build a name map for IAT slots keyed by the pointer VA the code dereferences.
|
|
# pefile: imp.address = VA of the IAT entry (where the resolved pointer lives).
|
|
iat = {}
|
|
if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
|
|
for entry in pe.DIRECTORY_ENTRY_IMPORT:
|
|
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}!{nm}"
|
|
|
|
# Disassemble from entrypoint.
|
|
ep_off = rva_to_off(pe, ep_rva)
|
|
code = pe.get_memory_mapped_image()[ep_rva: ep_rva + args.disasm_entry * 16]
|
|
print(f"\n== entrypoint disasm ({args.disasm_entry} insns) ==")
|
|
n = 0
|
|
for insn in md.disasm(code, ep_va):
|
|
annot = ""
|
|
# resolve call/jmp [rip+disp] against IAT
|
|
if insn.mnemonic in ("call", "jmp") and insn.operands:
|
|
op = insn.operands[-1]
|
|
try:
|
|
from capstone.x86 import X86_OP_MEM, X86_OP_IMM
|
|
if op.type == X86_OP_MEM and op.mem.base in (0,) and is64:
|
|
# rip-relative: target = next_insn + disp
|
|
tgt = insn.address + insn.size + op.mem.disp
|
|
if tgt in iat:
|
|
annot = f" ; -> {iat[tgt]}"
|
|
elif op.type == X86_OP_MEM:
|
|
if op.mem.disp in iat:
|
|
annot = f" ; -> {iat[op.mem.disp]}"
|
|
elif op.type == X86_OP_IMM and op.imm in iat:
|
|
annot = f" ; -> {iat[op.imm]}"
|
|
except Exception:
|
|
pass
|
|
print(f" {insn.address:#0x}: {insn.mnemonic:<7}{insn.op_str}{annot}")
|
|
n += 1
|
|
if n >= args.disasm_entry:
|
|
break
|
|
|
|
if args.carve:
|
|
print("\n== carve scan ==")
|
|
rd = [s for s in pe.sections if s.Name.rstrip(b'\x00') == b'.rdata']
|
|
if rd:
|
|
data = rd[0].get_data()
|
|
print(f".rdata off={hex(rd[0].PointerToRawData)} size={len(data)}")
|
|
# leading dword table
|
|
import struct
|
|
vals = struct.unpack_from("<8I", data, 0)
|
|
print("leading dwords:", [hex(v) for v in vals])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|