reverse-engineering-agent / multi_agent.py
Cultxre's picture
Restore full multi_agent.py
fd526b3 verified
Raw History Blame Contribute Delete
14.7 kB
"""
Multi-Agent Reverse Engineering System
====================================
A team of specialized agents, each with its own strength, coordinated by an
orchestrator. Instead of one monolithic agent, tasks are routed to the agent
best suited for them, and their findings are fused into a single report.
Agents:
- BinaryAgent native code (ELF/PE/Mach-O): disassembly, decompilation, CFG
- WebAgent websites / JS bundles / SPAs: endpoints, secrets, source maps
- MobileAgent APK/IPA: manifests, permissions, components, native libs
- FirmwareAgent raw blobs: entropy, partitions, strings, arch guessing
- MalwareAgent IOCs, anti-analysis, persistence, C2, crypto misuse
- ReconstructorAgent rebuilds a project scaffold from the fused analysis
- ReporterAgent synthesizes the final report
Usage:
python multi_agent.py analyze ./target.bin
python multi_agent.py analyze https://example.com --mode web
python multi_agent.py analyze app.apk --mode apk
"""
from __future__ import annotations
import os, json, hashlib, shutil, tempfile, zipfile, subprocess
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Any, Optional
import agent
@dataclass
class AgentResult:
agent: str
strength: str
findings: Dict[str, Any] = field(default_factory=dict)
errors: List[str] = field(default_factory=list)
confidence: float = 0.0
@dataclass
class FusedReport:
target: str
mode: str
agents_run: List[str] = field(default_factory=list)
results: List[AgentResult] = field(default_factory=list)
summary: str = ""
iocs: List[str] = field(default_factory=list)
risks: List[str] = field(default_factory=list)
rebuild_path: Optional[str] = None
report: str = ""
class BaseAgent:
name: str = "base"
strength: str = "general"
def can_handle(self, ctx: agent.AnalysisContext) -> bool:
return True
def run(self, ctx: agent.AnalysisContext) -> AgentResult:
raise NotImplementedError
def _result(self, findings, errors=None, confidence=0.5):
return AgentResult(agent=self.name, strength=self.strength,
findings=findings, errors=errors or [], confidence=confidence)
class BinaryAgent(BaseAgent):
name = "BinaryAgent"
strength = "native code: disassembly, decompilation, CFG, symbols"
def can_handle(self, ctx):
return ctx.mode == "binary"
def run(self, ctx):
errs = []
ctx = agent.FileIdentificationStage().run(ctx)
ctx = agent.StringExtractionStage(min_len=4).run(ctx)
ctx = agent.DisassemblyStage().run(ctx)
findings = {
"format": ctx.format, "arch": ctx.arch, "bits": ctx.bits,
"sections": len(ctx.sections), "imports": len(ctx.imports),
"exports": len(ctx.exports), "strings": len(ctx.strings),
"disassembled_sections": len(ctx.disassembly),
}
if shutil.which("analyzeHeadless") or shutil.which("r2"):
agent.DecompilationStage().run(ctx)
findings["decompiled"] = len(ctx.decompiled)
return self._result(findings, errs, 0.9)
class WebAgent(BaseAgent):
name = "WebAgent"
strength = "web apps: endpoints, JS, secrets, source maps, forms"
def can_handle(self, ctx):
return ctx.mode == "web"
def run(self, ctx):
ctx = agent.WebFetchStage().run(ctx)
ctx = agent.WebAnalyzeStage().run(ctx)
findings = {
"endpoints": len(ctx.endpoints),
"secrets": len(ctx.secrets),
"strings": len(ctx.strings),
"forms": len(ctx.metadata.get("forms", [])),
"scripts": len(ctx.metadata.get("scripts", [])),
}
return self._result(findings, ctx.errors, 0.85)
class MobileAgent(BaseAgent):
name = "MobileAgent"
strength = "mobile apps: manifests, permissions, components, native libs"
def can_handle(self, ctx):
return ctx.mode in ("apk", "ipa")
def run(self, ctx):
ctx = agent.ApkUnzipStage().run(ctx)
ctx = agent.ApkAnalyzeStage().run(ctx)
findings = {
"assets": len(ctx.assets),
"permissions": len(ctx.permissions),
"components": len(ctx.components),
"secrets": len(ctx.secrets),
"endpoints": len(ctx.endpoints),
}
return self._result(findings, ctx.errors, 0.85)
class FirmwareAgent(BaseAgent):
name = "FirmwareAgent"
strength = "raw firmware: entropy, partition hints, arch guessing, strings"
def can_handle(self, ctx):
return ctx.mode == "binary" and ctx.format == "raw"
def run(self, ctx):
data = ctx.target.read_bytes()
import math
from collections import Counter
def entropy(b):
if not b: return 0.0
c = Counter(b); n = len(b)
return -sum((v/n)*math.log2(v/n) for v in c.values())
blocks = [data[i:i+4096] for i in range(0, len(data), 4096)]
ent = [round(entropy(b), 3) for b in blocks]
high = [i for i, e in enumerate(ent) if e > 7.5]
ctx = agent.StringExtractionStage(min_len=6).run(ctx)
findings = {
"size": len(data),
"blocks": len(blocks),
"avg_entropy": round(sum(ent)/max(1,len(ent)), 3),
"high_entropy_blocks": len(high),
"strings": len(ctx.strings),
"urls": len([s for s in ctx.strings if "url" in s.get("tags", [])]),
}
ctx.metadata["entropy"] = ent
return self._result(findings, ctx.errors, 0.7)
class MalwareAgent(BaseAgent):
name = "MalwareAgent"
strength = "threat intel: IOCs, C2, persistence, anti-analysis, crypto misuse"
def can_handle(self, ctx):
return True
SUSPICIOUS_IMPORTS = {"system", "popen", "execve", "VirtualAlloc", "WriteProcessMemory",
"CreateRemoteThread", "RegSetValueEx", "connect", "socket", "send"}
PERSIST_PATHS = ["/etc/cron", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
"LaunchAgents", "systemd", "init.d"]
def run(self, ctx):
iocs = []
risks = []
for imp in ctx.imports:
if imp.get("name") in self.SUSPICIOUS_IMPORTS:
iocs.append(f"suspicious import: {imp['name']}")
for s in ctx.strings:
v = s.get("value", "")
if "http://" in v or "https://" in v:
iocs.append(f"url: {v}")
for p in self.PERSIST_PATHS:
if p in v:
risks.append(f"persistence path: {v}")
if any(k in v.lower() for k in ("password", "secret", "token", "api_key")):
risks.append(f"credential-like string: {v[:60]}")
for s in ctx.strings:
v = s.get("value", "").lower()
if any(k in v for k in ("isdebuggerpresent", "ptrace", "sandbox", "vmware", "virtualbox")):
risks.append(f"anti-analysis: {s['value']}")
findings = {"iocs": len(iocs), "risks": len(risks),
"sample_iocs": iocs[:20], "sample_risks": risks[:20]}
ctx.metadata["malware"] = findings
return self._result(findings, ctx.errors, 0.75)
class ReconstructorAgent(BaseAgent):
name = "ReconstructorAgent"
strength = "rebuilds a project scaffold from fused analysis"
def can_handle(self, ctx):
return True
def run(self, ctx):
out = Path(tempfile.mkdtemp()) / "rebuilt"
out.mkdir()
if ctx.mode == "binary":
(out / "src").mkdir()
(out / "src" / "main.c").write_text(self._c_scaffold(ctx))
(out / "Makefile").write_text("all:\n\tgcc src/main.c -o app\n")
elif ctx.mode == "web":
(out / "index.html").write_text(ctx.metadata.get("html", "<!-- captured html -->"))
(out / "endpoints.txt").write_text("\n".join(ctx.endpoints))
(out / "secrets.txt").write_text("\n".join(f"{s.get('type')}: {s.get('value')}" for s in ctx.secrets))
else:
extracted = ctx.metadata.get("extracted")
if extracted and Path(extracted).exists():
shutil.copytree(extracted, out / "extracted", dirs_exist_ok=True)
(out / "permissions.txt").write_text("\n".join(ctx.permissions))
(out / "components.json").write_text(json.dumps(ctx.components, indent=2))
(out / "secrets.txt").write_text("\n".join(f"{s.get('type')}: {s.get('value')}" for s in ctx.secrets))
(out / "README.md").write_text(f"# Rebuilt from {ctx.target}\n\nMode: {ctx.mode}\n")
zip_path = str(out.parent / "rebuilt.zip")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
for f in out.rglob("*"):
if f.is_file():
z.write(f, f.relative_to(out.parent))
return self._result({"rebuild_path": zip_path}, ctx.errors, 0.6)
def _c_scaffold(self, ctx):
lines = ["#include <stdio.h>", "#include <stdlib.h>", "", "/* Reconstructed from reverse engineering. Fill in bodies. */", ""]
for f in getattr(ctx, "functions", []) or []:
lines.append(f"/* {f.name}: {f.summary} */")
lines.append(f"void {f.name}(void) {{ /* TODO */ }}")
lines.append("")
if not getattr(ctx, "functions", []):
lines.append("int main(int argc, char** argv) {")
lines.append(" printf(\"rebuilt\\n\");")
lines.append(" return 0;")
lines.append("}")
return "\n".join(lines)
class ReporterAgent(BaseAgent):
name = "ReporterAgent"
strength = "synthesis: final report, risk summary, IOC list"
def can_handle(self, ctx):
return True
def run(self, ctx):
iocs = ctx.metadata.get("malware", {}).get("sample_iocs", [])
risks = ctx.metadata.get("malware", {}).get("sample_risks", [])
lines = ["# Multi-Agent Reverse Engineering Report", "", f"- Target: `{ctx.target}`", f"- Mode: {ctx.mode}", ""]
lines.append("## Agent findings")
lines.append(f"- strings: {len(ctx.strings)}")
lines.append(f"- endpoints: {len(ctx.endpoints)}")
lines.append(f"- secrets: {len(ctx.secrets)}")
lines.append(f"- permissions: {len(ctx.permissions)}")
lines.append(f"- components: {len(ctx.components)}")
if iocs:
lines.append("")
lines.append("## IOCs")
lines += [f"- {i}" for i in iocs]
if risks:
lines.append("")
lines.append("## Risks")
lines += [f"- {r}" for r in risks]
if ctx.strings:
lines.append("")
lines.append("## Strings (sample)")
lines += [f"- {s.get('value','')[:120]}" for s in ctx.strings[:60]]
ctx.report = "\n".join(lines)
return self._result({"report_len": len(ctx.report), "iocs": len(iocs), "risks": len(risks)}, ctx.errors, 0.9)
AGENTS = [BinaryAgent(), WebAgent(), MobileAgent(), FirmwareAgent(), MalwareAgent(),
ReconstructorAgent(), ReporterAgent()]
class Orchestrator:
def __init__(self, mode="binary", model="gpt-4o-mini", rebuild=True):
self.mode = mode
self.model = model
self.rebuild = rebuild
def run(self, target) -> FusedReport:
ctx = agent.AnalysisContext(target=Path(target) if not str(target).startswith("http") else Path(target),
mode=self.mode)
if self.mode == "web":
ctx.target = Path(target)
fr = FusedReport(target=str(target), mode=self.mode)
for ag in AGENTS:
if not ag.can_handle(ctx):
continue
if ag.name in ("ReconstructorAgent", "ReporterAgent"):
continue
try:
res = ag.run(ctx)
fr.results.append(res)
fr.agents_run.append(ag.name)
except Exception as e:
fr.results.append(AgentResult(agent=ag.name, strength=ag.strength, errors=[str(e)]))
if "MalwareAgent" not in fr.agents_run:
try:
res = MalwareAgent().run(ctx)
fr.results.append(res); fr.agents_run.append("MalwareAgent")
except Exception as e:
fr.results.append(AgentResult(agent="MalwareAgent", strength="", errors=[str(e)]))
agent.AIAugmentationStage(model=self.model).run(ctx)
if self.rebuild:
try:
res = ReconstructorAgent().run(ctx)
fr.results.append(res); fr.agents_run.append("ReconstructorAgent")
fr.rebuild_path = res.findings.get("rebuild_path")
except Exception as e:
fr.results.append(AgentResult(agent="ReconstructorAgent", strength="", errors=[str(e)]))
try:
ReporterAgent().run(ctx)
fr.agents_run.append("ReporterAgent")
except Exception as e:
fr.results.append(AgentResult(agent="ReporterAgent", strength="", errors=[str(e)]))
fr.summary = f"Ran {len(fr.agents_run)} agents on {target}"
fr.iocs = ctx.metadata.get("malware", {}).get("sample_iocs", [])
fr.risks = ctx.metadata.get("malware", {}).get("sample_risks", [])
fr.report = ctx.report
return fr
def to_dict(self, fr):
return {
"target": fr.target, "mode": fr.mode,
"agents_run": fr.agents_run,
"results": [asdict(r) for r in fr.results],
"iocs": fr.iocs, "risks": fr.risks,
"rebuild_path": fr.rebuild_path,
}
def main():
import argparse
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
a = sub.add_parser("analyze")
a.add_argument("target")
a.add_argument("--mode", default="binary", choices=["binary", "web", "apk", "ipa"])
a.add_argument("--out", default="report.md")
a.add_argument("--json", default=None)
a.add_argument("--no-rebuild", action="store_true")
args = ap.parse_args()
if args.cmd == "analyze":
orch = Orchestrator(mode=args.mode, rebuild=not args.no_rebuild)
fr = orch.run(args.target)
Path(args.out).write_text(fr.report)
if args.json:
Path(args.json).write_text(json.dumps(orch.to_dict(fr), indent=2, default=str))
print(f"agents: {', '.join(fr.agents_run)}")
print(f"report -> {args.out}")
if fr.rebuild_path:
print(f"rebuild -> {fr.rebuild_path}")
if __name__ == "__main__":
from pathlib import Path
main()