reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
36
reverse-skill-repo/scripts/idat_mcp_serve.py
Normal file
36
reverse-skill-repo/scripts/idat_mcp_serve.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""IDA startup script (-S) that boots the ida-pro-mcp HTTP server headless.
|
||||
|
||||
Launched via:
|
||||
idat.exe -A -S"idat_mcp_serve.py" <target>
|
||||
|
||||
-A = autonomous/batch (no dialogs). This script starts the MCP plugin's
|
||||
HTTP JSON-RPC server on 127.0.0.1:13337 and keeps the idat process alive so the
|
||||
already-wired `xd://mcp__ida_pro_mcp_*` tools can drive analysis. idat runs the
|
||||
startup script after initial auto-analysis of the input DB.
|
||||
"""
|
||||
import idaapi
|
||||
import ida_auto
|
||||
import ida_loader
|
||||
|
||||
# Ensure auto-analysis has settled before serving requests.
|
||||
ida_auto.auto_wait()
|
||||
|
||||
# Load and start the MCP plugin server.
|
||||
# The plugin file registers PLUGIN_ENTRY()->MCP(); run(0) starts the HTTP server.
|
||||
_plugin_path = idaapi.idadir("plugins") # noqa: F841 (informational)
|
||||
mod = ida_loader.load_plugin("mcp-plugin.py")
|
||||
if mod:
|
||||
ok = ida_loader.run_plugin(mod, 0)
|
||||
print(f"[idat-mcp] run_plugin -> {ok}")
|
||||
else:
|
||||
# Fallback: import the plugin module directly and start its Server.
|
||||
import importlib.util, os
|
||||
pp = os.path.join(idaapi.get_user_idadir(), "plugins", "mcp-plugin.py")
|
||||
spec = importlib.util.spec_from_file_location("mcp_plugin", pp)
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
_srv = m.Server()
|
||||
_srv.start()
|
||||
print("[idat-mcp] started server via direct import")
|
||||
|
||||
print("[idat-mcp] MCP HTTP server should be live at http://127.0.0.1:13337")
|
||||
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()
|
||||
103
reverse-skill-repo/scripts/pe_deep.py
Normal file
103
reverse-skill-repo/scripts/pe_deep.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/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())
|
||||
37
reverse-skill-repo/scripts/refresh-tool-index.ps1
Normal file
37
reverse-skill-repo/scripts/refresh-tool-index.ps1
Normal file
@@ -0,0 +1,37 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Refresh the local reverse-skill tool index.
|
||||
|
||||
.DESCRIPTION
|
||||
Root-level compatibility wrapper. The real implementation lives at:
|
||||
skills/scripts/refresh-tool-index.ps1
|
||||
|
||||
This wrapper keeps older README commands working while preserving the canonical
|
||||
script location under skills/scripts/.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$OutputMarkdown,
|
||||
[string]$OutputJson
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$target = Join-Path $repoRoot 'skills\scripts\refresh-tool-index.ps1'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $target)) {
|
||||
throw "Missing canonical refresh script: $target"
|
||||
}
|
||||
|
||||
$argsToForward = @()
|
||||
if (-not [string]::IsNullOrWhiteSpace($OutputMarkdown)) {
|
||||
$argsToForward += @('-OutputMarkdown', $OutputMarkdown)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($OutputJson)) {
|
||||
$argsToForward += @('-OutputJson', $OutputJson)
|
||||
}
|
||||
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $target @argsToForward
|
||||
exit $LASTEXITCODE
|
||||
Reference in New Issue
Block a user