1133 lines
48 KiB
Python
1133 lines
48 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
seep - AI-Native Reverse Engineering & Security Analysis MCP Server
|
||
整合 Radare2 跨架构二进制逆向、JADX/Apktool/Smali 安卓逆向、
|
||
动态 Hook 生成以及 ReverseLab 183+ 攻防知识库与 CTF 战术模板。
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import re
|
||
import glob
|
||
import shutil
|
||
import zipfile
|
||
import subprocess
|
||
from pathlib import Path
|
||
from typing import Optional, List, Dict, Any
|
||
|
||
try:
|
||
from mcp.server.mcpserver import MCPServer
|
||
except ImportError:
|
||
from mcp.server.fastmcp import FastMCP as MCPServer
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 基础路径定义与环境初始化
|
||
# ---------------------------------------------------------------------------
|
||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
TOOL_DIR = os.path.join(BASE_DIR, "Tool")
|
||
SAFE_DIR = os.path.join(TOOL_DIR, "safe")
|
||
R2_BIN_DIR = os.path.join(SAFE_DIR, "radare2", "bin")
|
||
R2_EXE = os.path.join(R2_BIN_DIR, "radare2.exe")
|
||
RABIN2_EXE = os.path.join(R2_BIN_DIR, "rabin2.exe")
|
||
RADIFF2_EXE = os.path.join(R2_BIN_DIR, "radiff2.exe")
|
||
RASM2_EXE = os.path.join(R2_BIN_DIR, "rasm2.exe")
|
||
RAHASH2_EXE = os.path.join(R2_BIN_DIR, "rahash2.exe")
|
||
|
||
JADX_BAT = os.path.join(SAFE_DIR, "jadx", "bin", "jadx.bat")
|
||
APKTOOL_JAR = os.path.join(SAFE_DIR, "apktool", "apktool.jar")
|
||
APKTOOL_BAT = os.path.join(SAFE_DIR, "apktool", "apktool.bat")
|
||
|
||
KB_DIR = os.path.join(TOOL_DIR, "reverselab", "kb")
|
||
LLMS_TXT = os.path.join(TOOL_DIR, "reverselab", "docs", "llms.txt")
|
||
HOOK_TEMPLATES_DIR = os.path.join(SAFE_DIR, "hook-mcp", "templates")
|
||
|
||
# 将 r2 bin 目录添加到 PATH 中,确保子进程加载依赖 dll
|
||
if os.path.isdir(R2_BIN_DIR) and R2_BIN_DIR not in os.environ.get("PATH", ""):
|
||
os.environ["PATH"] = R2_BIN_DIR + os.pathsep + os.environ.get("PATH", "")
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# MCP Server 实例
|
||
# ---------------------------------------------------------------------------
|
||
server = MCPServer(
|
||
"seep",
|
||
instructions="""seep 是一个专为安全分析师与 AI Agent 设计的全能逆向工程与 CTF 战术 MCP Server。
|
||
能力包含:
|
||
1. 二进制分析与逆向 (Radare2 / Rabin2 / Radiff2 / Rasm2)
|
||
2. Android APK / DEX 反编译、解包与 Smali 检索 (JADX, Apktool)
|
||
3. 动态 Hook 插件自动生成 (Frida JS, LSPilot / BeanShell)
|
||
4. ReverseLab 183+ 逆向与 Web/PE/CVE 攻防战术知识库检索与即用 Payload
|
||
"""
|
||
)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 辅助函数
|
||
# ---------------------------------------------------------------------------
|
||
def _run_cmd(cmd: List[str], cwd: Optional[str] = None, timeout: int = 60) -> str:
|
||
"""运行外部命令并返回标准输出或捕获错误"""
|
||
try:
|
||
proc = subprocess.run(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
cwd=cwd,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
timeout=timeout
|
||
)
|
||
out = (proc.stdout or "").strip()
|
||
err = (proc.stderr or "").strip()
|
||
if proc.returncode != 0 and not out:
|
||
return f"[ERROR code {proc.returncode}]: {err}"
|
||
if out and err:
|
||
return f"{out}\n[STDERR]: {err}"
|
||
return out or err or "[Success, no output]"
|
||
except subprocess.TimeoutExpired:
|
||
return f"[ERROR]: Command timed out after {timeout} seconds."
|
||
except Exception as e:
|
||
return f"[ERROR]: Exception executing command: {str(e)}"
|
||
|
||
def _clean_r2_output(text: str) -> str:
|
||
"""移除 ANSI 颜色控制字符"""
|
||
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
||
return ansi_escape.sub('', text)
|
||
|
||
def _find_java() -> Optional[str]:
|
||
"""寻找 Java 运行时可执行路径"""
|
||
if shutil.which("java"):
|
||
return "java"
|
||
candidates = [
|
||
r"D:\Tool\JDK\bin\java.exe",
|
||
r"D:\Tool\Android Studio\jbr\bin\java.exe",
|
||
r"C:\Program Files\Java",
|
||
r"C:\Program Files\Eclipse Adoptium",
|
||
r"C:\Program Files\Microsoft"
|
||
]
|
||
for c in candidates:
|
||
if os.path.isfile(c):
|
||
return c
|
||
if os.path.isdir(c):
|
||
for root, _, files in os.walk(c):
|
||
if "java.exe" in files:
|
||
return os.path.join(root, "java.exe")
|
||
return None
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. 状态与环境工具
|
||
# ---------------------------------------------------------------------------
|
||
@server.tool()
|
||
def seep_status() -> str:
|
||
"""检查 seep 逆向工具链状态、内置组件就绪度及系统环境配置。"""
|
||
java_path = _find_java()
|
||
java_ver = "NOT FOUND (JDK 17 LTS recommended)"
|
||
if java_path:
|
||
out = _run_cmd([java_path, "-version"])
|
||
first_line = out.splitlines()[0] if out else "Found"
|
||
java_ver = f"{java_path} ({first_line})"
|
||
|
||
r2_ver = "NOT FOUND"
|
||
if os.path.isfile(R2_EXE):
|
||
out = _run_cmd([R2_EXE, "-v"])
|
||
r2_ver = out.splitlines()[0] if out else "Ready"
|
||
|
||
jadx_status = "Ready (Batch/Jar present)" if os.path.isfile(JADX_BAT) else "Missing"
|
||
apktool_status = "Ready (Jar present)" if os.path.isfile(APKTOOL_JAR) else "Missing"
|
||
kb_status = f"Ready ({len(glob.glob(os.path.join(KB_DIR, '**', '*.md'), recursive=True))} documents)" if os.path.isdir(KB_DIR) else "Missing"
|
||
|
||
status = {
|
||
"name": "seep",
|
||
"description": "AI-Native Reverse Engineering Toolkit MCP Server",
|
||
"base_directory": BASE_DIR,
|
||
"components": {
|
||
"radare2_suite": {
|
||
"status": "Ready" if os.path.isfile(R2_EXE) else "Missing",
|
||
"binary": R2_EXE,
|
||
"version": r2_ver
|
||
},
|
||
"jadx_decompiler": {
|
||
"status": jadx_status,
|
||
"path": JADX_BAT
|
||
},
|
||
"apktool": {
|
||
"status": apktool_status,
|
||
"path": APKTOOL_JAR
|
||
},
|
||
"java_runtime": {
|
||
"status": "Ready" if java_path else "Not Installed / Not in PATH",
|
||
"detail": java_ver
|
||
},
|
||
"reverselab_knowledge_base": {
|
||
"status": kb_status,
|
||
"path": KB_DIR
|
||
},
|
||
"hook_templates": {
|
||
"status": "Ready" if os.path.isdir(HOOK_TEMPLATES_DIR) else "Missing",
|
||
"path": HOOK_TEMPLATES_DIR
|
||
}
|
||
}
|
||
}
|
||
return json.dumps(status, indent=2, ensure_ascii=False)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. 二进制逆向分析工具 (Radare2 引擎)
|
||
# ---------------------------------------------------------------------------
|
||
@server.tool()
|
||
def seep_r2_info(binary_path: str) -> str:
|
||
"""提取二进制文件 (PE/ELF/Mach-O) 的关键元信息:架构、位数、大小、大小端、编译器、语言以及关键安全防御机制 (Canary, NX, PIE, RELRO, ASLR)。
|
||
|
||
Args:
|
||
binary_path: 目标二进制文件的绝对路径或相对路径
|
||
"""
|
||
if not os.path.isfile(binary_path):
|
||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||
if not os.path.isfile(RABIN2_EXE):
|
||
return f"[ERROR]: rabin2.exe not found at {RABIN2_EXE}"
|
||
|
||
out = _run_cmd([RABIN2_EXE, "-I", os.path.abspath(binary_path)])
|
||
return _clean_r2_output(out)
|
||
|
||
@server.tool()
|
||
def seep_r2_cmd(binary_path: str, command: str, analyze: bool = True) -> str:
|
||
"""在目标二进制文件上执行任意 radare2 命令。
|
||
|
||
Args:
|
||
binary_path: 目标二进制文件路径
|
||
command: radare2 命令字符串(例如 'afl', 'pdf @ main', 'iz', 'px 64 @ 0x1000',多条命令用分号分隔)
|
||
analyze: 是否在执行命令前先执行基本代码分析 (aaa)。默认 True。
|
||
"""
|
||
if not os.path.isfile(binary_path):
|
||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||
if not os.path.isfile(R2_EXE):
|
||
return f"[ERROR]: radare2.exe not found at {R2_EXE}"
|
||
|
||
full_cmd = f"aaa; {command}" if analyze else command
|
||
cmd_args = [
|
||
R2_EXE,
|
||
"-q",
|
||
"-e", "scr.color=0",
|
||
"-c", full_cmd,
|
||
os.path.abspath(binary_path)
|
||
]
|
||
out = _run_cmd(cmd_args, timeout=90)
|
||
return _clean_r2_output(out)
|
||
|
||
@server.tool()
|
||
def seep_r2_strings(binary_path: str, filter_text: Optional[str] = None, all_sections: bool = False, limit: int = 100) -> str:
|
||
"""从二进制文件中提取字符串(包含数据段或全段字符串),支持关键词过滤。
|
||
|
||
Args:
|
||
binary_path: 目标二进制文件路径
|
||
filter_text: 可选过滤关键词(不区分大小写)
|
||
all_sections: 是否检索全段字符串 (-zz),默认 False 只检索数据段 (-z)
|
||
limit: 返回最大结果行数,默认 100
|
||
"""
|
||
if not os.path.isfile(binary_path):
|
||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||
|
||
flag = "-zz" if all_sections else "-z"
|
||
out = _run_cmd([RABIN2_EXE, flag, os.path.abspath(binary_path)])
|
||
lines = out.splitlines()
|
||
|
||
if filter_text:
|
||
pat = filter_text.lower()
|
||
header = lines[:2] if len(lines) >= 2 else []
|
||
matched = [l for l in lines[2:] if pat in l.lower()]
|
||
result = header + matched[:limit]
|
||
if len(matched) > limit:
|
||
result.append(f"... [Truncated: showing {limit} of {len(matched)} matched strings]")
|
||
return "\n".join(result)
|
||
else:
|
||
result = lines[:limit]
|
||
if len(lines) > limit:
|
||
result.append(f"... [Truncated: showing {limit} of {len(lines)} strings]")
|
||
return "\n".join(result)
|
||
|
||
@server.tool()
|
||
def seep_r2_functions(binary_path: str, filter_name: Optional[str] = None, limit: int = 100) -> str:
|
||
"""列出二进制文件的函数列表、入口点与导入导出符号。
|
||
|
||
Args:
|
||
binary_path: 目标二进制文件路径
|
||
filter_name: 过滤函数名或符号名(不区分大小写)
|
||
limit: 最大返回行数,默认 100
|
||
"""
|
||
if not os.path.isfile(binary_path):
|
||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||
|
||
cmd = "aaa; afl"
|
||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmd, os.path.abspath(binary_path)], timeout=60)
|
||
lines = _clean_r2_output(out).splitlines()
|
||
|
||
# 剔除分析阶段日志
|
||
clean_lines = [l for l in lines if not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||
|
||
if filter_name:
|
||
pat = filter_name.lower()
|
||
filtered = [l for l in clean_lines if pat in l.lower()]
|
||
res = filtered[:limit]
|
||
if len(filtered) > limit:
|
||
res.append(f"... [Truncated: showing {limit} of {len(filtered)} functions]")
|
||
return "\n".join(res) or "[No matching functions found]"
|
||
else:
|
||
res = clean_lines[:limit]
|
||
if len(clean_lines) > limit:
|
||
res.append(f"... [Truncated: showing {limit} of {len(clean_lines)} functions]")
|
||
return "\n".join(res)
|
||
|
||
@server.tool()
|
||
def seep_r2_disasm(
|
||
binary_path: str,
|
||
target: str = "main",
|
||
lines: int = 60,
|
||
output_mode: str = "full",
|
||
limit_tokens: int = 0,
|
||
) -> str:
|
||
"""反汇编指定函数或地址,支持三种输出模式以控制上下文消耗(Context Budget Control)。
|
||
|
||
Args:
|
||
binary_path: 目标二进制文件路径
|
||
target: 目标函数名、符号或虚拟地址(例如 'main', 'sym.imp.puts', '0x1013ef0')
|
||
lines: 当以地址为目标时反汇编的最大指令行数,默认 60
|
||
output_mode: 输出模式:
|
||
'full' = 完整原始反汇编(默认,适合精细分析)
|
||
'branch' = 仅保留条件跳转与调用指令(cmp/test/jcc/call/ret),聚焦控制流骨架
|
||
'summary'= 不输出指令,只统计:函数大小、块数、调用链、导入函数列表(最省 Token)
|
||
limit_tokens: 最大输出字符数(0=不限制),超出时尾部截断并附注 Truncated 标记
|
||
"""
|
||
if not os.path.isfile(binary_path):
|
||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||
|
||
if output_mode == "summary":
|
||
# 仅抽取函数级摘要统计,不输出原始指令
|
||
info_cmd = f"aaa; afi @ {target}"
|
||
xref_cmd = f"axt @ {target}"
|
||
call_cmd = f"axf @ {target}"
|
||
info_out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", info_cmd, os.path.abspath(binary_path)], timeout=60)
|
||
xref_out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", xref_cmd, os.path.abspath(binary_path)], timeout=60)
|
||
call_out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", call_cmd, os.path.abspath(binary_path)], timeout=60)
|
||
summary = (
|
||
f"[DISASM SUMMARY MODE — 上下文降噪 Token 节省模式]\n"
|
||
f"Target: {target}\n"
|
||
f"--- Function Info (afi) ---\n{_clean_r2_output(info_out)[:800]}\n"
|
||
f"--- Callers (axt / xrefs-to) ---\n{_clean_r2_output(xref_out)[:600]}\n"
|
||
f"--- Callees (axf / xrefs-from) ---\n{_clean_r2_output(call_out)[:600]}\n"
|
||
f"提示: 使用 output_mode='branch' 或 'full' 获取指令级详情。"
|
||
)
|
||
return summary
|
||
|
||
# 获取完整反汇编
|
||
cmd = f"aaa; pdf @ {target}"
|
||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmd, os.path.abspath(binary_path)], timeout=60)
|
||
cleaned = _clean_r2_output(out)
|
||
output_lines = [l for l in cleaned.splitlines() if not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||
|
||
if not output_lines or all(not l.strip() for l in output_lines):
|
||
cmd = f"pd {lines} @ {target}"
|
||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmd, os.path.abspath(binary_path)], timeout=60)
|
||
cleaned = _clean_r2_output(out)
|
||
output_lines = [l for l in cleaned.splitlines() if not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||
|
||
if output_mode == "branch":
|
||
# 过滤:仅保留含有控制流关键词的行(条件跳转、调用、返回)
|
||
branch_keywords = ("cmp ", "test ", "je ", "jne ", "jz ", "jnz ", "jb ", "jbe ",
|
||
"ja ", "jae ", "jl ", "jle ", "jg ", "jge ", "jmp ", "call ",
|
||
"ret", "jcc", "cbz ", "cbnz ", "bl ", "bne ", "beq ")
|
||
filtered = [l for l in output_lines if any(kw in l.lower() for kw in branch_keywords)]
|
||
total = len(output_lines)
|
||
kept = len(filtered)
|
||
header = (
|
||
f"[DISASM BRANCH MODE — 控制流骨架截面,已过滤 {total - kept}/{total} 行纯数据指令]\n"
|
||
f"Target: {target} | 保留行: {kept} | 完整输出请用 output_mode='full'\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
)
|
||
output_lines = filtered
|
||
result = header + "\n".join(output_lines)
|
||
else:
|
||
result = "\n".join(output_lines)
|
||
|
||
if limit_tokens > 0 and len(result) > limit_tokens:
|
||
result = result[:limit_tokens] + f"\n... [TRUNCATED at {limit_tokens} chars — use offset/target narrowing for full output]"
|
||
|
||
return result or "[Disassembly empty or symbol not found]"
|
||
|
||
@server.tool()
|
||
def seep_r2_decompile(
|
||
binary_path: str,
|
||
target: str = "main",
|
||
output_mode: str = "full",
|
||
limit_tokens: int = 8000,
|
||
) -> str:
|
||
"""将目标函数反编译为类 C 伪代码,支持三档输出模式以控制 Token 消耗(Context Budget Control)。
|
||
|
||
Args:
|
||
binary_path: 目标二进制文件路径
|
||
target: 目标函数名称或虚拟地址(例如 'main', 'sym.verify_key')
|
||
output_mode: 输出控制模式(默认 full):
|
||
'full' = 完整伪代码(适合深度分析,默认)
|
||
'fold' = 折叠纯计算/赋值语句,仅保留 if/else/switch/return/API调用行(减少 ~60% Token)
|
||
'summary' = 不输出伪代码,仅返回:函数签名、入参、出参、直接调用的 API/函数名列表(最省 Token)
|
||
limit_tokens: 最大输出字符数(默认 8000),超出则截断附 Truncated 标记;设 0 表示不限制
|
||
"""
|
||
if not os.path.isfile(binary_path):
|
||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||
|
||
if output_mode == "summary":
|
||
# 获取函数摘要信息(不输出函数体)
|
||
cmds = f"aaa; afi @ {target}; axf @ {target}"
|
||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmds, os.path.abspath(binary_path)], timeout=60)
|
||
clean = _clean_r2_output(out)
|
||
return (
|
||
f"[DECOMPILE SUMMARY MODE — 最省 Token 函数摘要]\n"
|
||
f"Target: {target}\n"
|
||
f"{clean[:1200]}\n"
|
||
f"提示: 使用 output_mode='fold' 或 'full' 获取伪代码详情。"
|
||
)
|
||
|
||
cmd = f"aaa; pdc @ {target}"
|
||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmd, os.path.abspath(binary_path)], timeout=60)
|
||
cleaned = _clean_r2_output(out)
|
||
output_lines = [l for l in cleaned.splitlines() if not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||
|
||
if output_mode == "fold":
|
||
# 折叠模式:去除纯赋值/加减乘除/临时变量行,仅保留控制流与 API 调用
|
||
def _is_control_or_call(line: str) -> bool:
|
||
stripped = line.strip()
|
||
keywords = ("if ", "else", "switch", "case ", "return", "while ", "for ",
|
||
"do {", "goto ", "break", "continue",
|
||
"call ", "(", "->")
|
||
# 包含括号 = 可能是函数调用,包含关键词 = 控制流
|
||
return any(k in stripped for k in keywords) or stripped.startswith("//")
|
||
|
||
folded = [l for l in output_lines if _is_control_or_call(l)]
|
||
total = len(output_lines)
|
||
kept = len(folded)
|
||
header = (
|
||
f"[DECOMPILE FOLD MODE — 已折叠 {total - kept}/{total} 行纯赋值/计算语句,上下文降噪约 {int((total-kept)/max(total,1)*100)}%]\n"
|
||
f"Target: {target} | 保留行: {kept} | 完整输出请用 output_mode='full'\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
)
|
||
result = header + "\n".join(folded)
|
||
else:
|
||
result = "\n".join(output_lines)
|
||
|
||
if limit_tokens > 0 and len(result) > limit_tokens:
|
||
result = result[:limit_tokens] + f"\n... [TRUNCATED at {limit_tokens} chars — 使用 target 缩小范围或 output_mode='fold'/'summary' 降低消耗]"
|
||
|
||
return result or "[Decompile output empty]"
|
||
|
||
@server.tool()
|
||
def seep_r2_diff(file_a: str, file_b: str, mode: str = "code") -> str:
|
||
"""使用 radiff2 比对两个二进制文件的差异。
|
||
|
||
Args:
|
||
file_a: 第一个二进制文件路径
|
||
file_b: 第二个二进制文件路径
|
||
mode: 比对模式,支持 'code' (函数代码比对 -C), 'hex' (十六进制比对 -x), 'stats' (差异摘要统计 -s)
|
||
"""
|
||
if not os.path.isfile(file_a) or not os.path.isfile(file_b):
|
||
return f"[ERROR]: One or both files do not exist: '{file_a}', '{file_b}'"
|
||
|
||
flag_map = {
|
||
"code": "-C",
|
||
"hex": "-x",
|
||
"stats": "-s"
|
||
}
|
||
flag = flag_map.get(mode.lower(), "-C")
|
||
out = _run_cmd([RADIFF2_EXE, flag, os.path.abspath(file_a), os.path.abspath(file_b)], timeout=60)
|
||
return _clean_r2_output(out)
|
||
|
||
@server.tool()
|
||
def seep_r2_asm(code_or_hex: str, arch: str = "x86", bits: int = 64, disasm: bool = False) -> str:
|
||
"""使用 rasm2 进行机器码组装 (汇编) 或逆向反汇编。
|
||
|
||
Args:
|
||
code_or_hex: 汇编指令字符串 (如 'xor eax, eax; ret') 或 十六进制机器码 (如 '31c0c3')
|
||
arch: 目标架构 (x86, arm, mips, riscv 等),默认 x86
|
||
bits: 位数 (16, 32, 64),默认 64
|
||
disasm: 为 True 时将 hex 转换为汇编代码;为 False 时将汇编代码转换为 hex 机器码。
|
||
"""
|
||
if not os.path.isfile(RASM2_EXE):
|
||
return f"[ERROR]: rasm2.exe not found at {RASM2_EXE}"
|
||
|
||
cmd = [RASM2_EXE, "-a", arch, "-b", str(bits)]
|
||
if disasm:
|
||
cmd.extend(["-d", code_or_hex.strip()])
|
||
else:
|
||
cmd.append(code_or_hex.strip())
|
||
|
||
out = _run_cmd(cmd)
|
||
return out.strip()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. Android APK 逆向与动态 Hook 生成
|
||
# ---------------------------------------------------------------------------
|
||
@server.tool()
|
||
def seep_r2_xrefs(
|
||
binary_path: str,
|
||
target: str,
|
||
direction: str = "to",
|
||
limit: int = 10,
|
||
) -> str:
|
||
"""获取指定地址/函数的交叉引用关系,内置分页与统计摘要避免大量 xref 结果内爆上下文。
|
||
|
||
Args:
|
||
binary_path: 目标二进制文件路径
|
||
target: 目标函数名或地址(如 'sym.check_license', '0x401234')
|
||
direction: 方向— 'to'=谁引用了它(调用方), 'from'=它引用了谁(被调用方)
|
||
limit: 最多返回多少条引用(默认 10),超出部分附加统计摘要
|
||
"""
|
||
if not os.path.isfile(binary_path):
|
||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||
|
||
r2_cmd = f"aaa; {'axt' if direction == 'to' else 'axf'} @ {target}"
|
||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", r2_cmd, os.path.abspath(binary_path)], timeout=60)
|
||
cleaned = _clean_r2_output(out)
|
||
lines = [l for l in cleaned.splitlines() if l.strip() and not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||
|
||
total = len(lines)
|
||
shown = lines[:limit]
|
||
result_lines = [
|
||
f"[XREFS {direction.upper()} — {target} | 共 {total} 条引用,显示前 {min(limit, total)} 条]",
|
||
f"注意: 若 total 远大于 limit,考虑缩小 limit 或先查 summary 模式确定目标函数。",
|
||
"━" * 60,
|
||
] + shown
|
||
|
||
if total > limit:
|
||
result_lines.append(f"... [已截断,剩余 {total - limit} 条未展示 | 增大 limit 或用 filter_name 缩小目标范围]"
|
||
f"\n模块来源分布小计: {dict(list({l.split()[2] if len(l.split())>2 else '?' for l in lines}))[:3]}"
|
||
)
|
||
|
||
return "\n".join(result_lines)
|
||
@server.tool()
|
||
def seep_apk_info(apk_path: str) -> str:
|
||
"""原生解析 APK 压缩包结构:统计 DEX 文件、Native .so 架构动态库、资源分布,并提取 AndroidManifest.xml 关键字符串(免 Java 环境)。
|
||
|
||
Args:
|
||
apk_path: 目标 APK 文件路径
|
||
"""
|
||
if not os.path.isfile(apk_path):
|
||
return f"[ERROR]: APK file not found: {apk_path}"
|
||
|
||
try:
|
||
with zipfile.ZipFile(apk_path, 'r') as z:
|
||
namelist = z.namelist()
|
||
dex_files = [f for f in namelist if f.endswith(".dex")]
|
||
so_files = [f for f in namelist if f.endswith(".so")]
|
||
meta_files = [f for f in namelist if f.startswith("META-INF/")]
|
||
|
||
# 统计原生架构
|
||
arches = set()
|
||
for s in so_files:
|
||
parts = s.split('/')
|
||
if len(parts) >= 2 and parts[0] == "lib":
|
||
arches.add(parts[1])
|
||
|
||
# 提取 Manifest 二进制中的可读 ASCII/UTF-8 字符串
|
||
manifest_strings = []
|
||
if "AndroidManifest.xml" in namelist:
|
||
raw = z.read("AndroidManifest.xml")
|
||
# 过滤出 4 字符以上的可打印字符串
|
||
found = re.findall(rb'[\x20-\x7e]{4,}', raw)
|
||
for item in found:
|
||
s = item.decode('ascii', errors='ignore')
|
||
if any(k in s.lower() for k in ["android", "permission", "activity", "service", "receiver", "provider", "package"]):
|
||
manifest_strings.append(s)
|
||
|
||
summary = {
|
||
"file_size": os.path.getsize(apk_path),
|
||
"dex_count": len(dex_files),
|
||
"dex_files": dex_files,
|
||
"native_architectures": sorted(list(arches)),
|
||
"native_libraries_count": len(so_files),
|
||
"signed_certificates": meta_files[:5],
|
||
"manifest_key_markers": manifest_strings[:30]
|
||
}
|
||
return json.dumps(summary, indent=2, ensure_ascii=False)
|
||
except Exception as e:
|
||
return f"[ERROR]: Failed to parse APK zip: {str(e)}"
|
||
|
||
@server.tool()
|
||
def seep_apk_decompile(apk_path: str, output_dir: Optional[str] = None, deobfuscate: bool = True) -> str:
|
||
"""使用内置 JADX 反编译器将 APK 或 DEX 文件全量反编译为 Java 源码工程。
|
||
|
||
Args:
|
||
apk_path: 目标 APK 或 DEX 文件路径
|
||
output_dir: 源码输出目录(若不指定则在 APK 旁生成 <apk_name>_src 目录)
|
||
deobfuscate: 是否启用混淆名还原 (-r/--deobf),默认 True
|
||
"""
|
||
if not os.path.isfile(apk_path):
|
||
return f"[ERROR]: Target APK does not exist: {apk_path}"
|
||
|
||
java_path = _find_java()
|
||
if not java_path:
|
||
return "[ERROR]: Java runtime (JDK 17 LTS) is required to run JADX. Please install JDK or set JAVA_HOME."
|
||
|
||
if not output_dir:
|
||
stem = Path(apk_path).stem
|
||
output_dir = os.path.join(os.path.dirname(os.path.abspath(apk_path)), f"{stem}_jadx_src")
|
||
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
cmd = [JADX_BAT, "-d", output_dir]
|
||
if deobfuscate:
|
||
cmd.append("--deobf")
|
||
cmd.append(os.path.abspath(apk_path))
|
||
|
||
out = _run_cmd(cmd, timeout=300)
|
||
return f"Decompilation finished.\nOutput directory: {output_dir}\nLog: {out[:500]}..."
|
||
|
||
@server.tool()
|
||
def seep_apk_unpack(apk_path: str, output_dir: Optional[str] = None, no_src: bool = False) -> str:
|
||
"""使用内置 Apktool 解包 APK 资源文件、清晰明文 AndroidManifest.xml 以及 Smali 汇编代码。
|
||
|
||
Args:
|
||
apk_path: 目标 APK 文件路径
|
||
output_dir: 解包输出目录(若不指定则在 APK 旁生成 <apk_name>_apktool 目录)
|
||
no_src: 是否跳过 smali 反编译 (-s),若只需提取资源可设为 True
|
||
"""
|
||
if not os.path.isfile(apk_path):
|
||
return f"[ERROR]: Target APK does not exist: {apk_path}"
|
||
|
||
java_path = _find_java()
|
||
if not java_path:
|
||
return "[ERROR]: Java runtime (JDK 17 LTS) is required to run Apktool. Please install JDK or set JAVA_HOME."
|
||
|
||
if not output_dir:
|
||
stem = Path(apk_path).stem
|
||
output_dir = os.path.join(os.path.dirname(os.path.abspath(apk_path)), f"{stem}_apktool")
|
||
|
||
cmd = [java_path, "-jar", APKTOOL_JAR, "d", "-f"]
|
||
if no_src:
|
||
cmd.append("-s")
|
||
cmd.extend(["-o", output_dir, os.path.abspath(apk_path)])
|
||
|
||
out = _run_cmd(cmd, timeout=300)
|
||
return f"Apktool unpack finished.\nOutput directory: {output_dir}\nLog: {out[:500]}..."
|
||
|
||
@server.tool()
|
||
def seep_apk_smali_search(directory: str, pattern: str, case_sensitive: bool = False, limit: int = 50) -> str:
|
||
"""在解包后的 Smali 代码或源码目录中进行敏感词/签名/加解密密钥快速排查。
|
||
|
||
Args:
|
||
directory: 解包后的 smali 或源码根目录
|
||
pattern: 正则表达式或搜索文本(例如 'AES', 'SecretKeySpec', 'token', 'http://', 'firebase')
|
||
case_sensitive: 是否大小写敏感,默认 False
|
||
limit: 最大匹配结果条数,默认 50
|
||
"""
|
||
if not os.path.isdir(directory):
|
||
return f"[ERROR]: Directory not found: {directory}"
|
||
|
||
flags = 0 if case_sensitive else re.IGNORECASE
|
||
try:
|
||
regex = re.compile(pattern, flags)
|
||
except re.error as e:
|
||
return f"[ERROR]: Invalid regex pattern: {str(e)}"
|
||
|
||
matches = []
|
||
for root, _, files in os.walk(directory):
|
||
for f in files:
|
||
if f.endswith((".smali", ".java", ".xml", ".json", ".txt")):
|
||
fpath = os.path.join(root, f)
|
||
try:
|
||
with open(fpath, "r", encoding="utf-8", errors="ignore") as fh:
|
||
for idx, line in enumerate(fh, 1):
|
||
if regex.search(line):
|
||
rel = os.path.relpath(fpath, directory)
|
||
matches.append(f"{rel}:{idx}: {line.strip()[:200]}")
|
||
if len(matches) >= limit:
|
||
break
|
||
except Exception:
|
||
pass
|
||
if len(matches) >= limit:
|
||
break
|
||
|
||
if not matches:
|
||
return f"No matches found for '{pattern}' in {directory}"
|
||
res = matches[:limit]
|
||
if len(matches) >= limit:
|
||
res.append(f"... [Truncated: reached limit of {limit} matches]")
|
||
return "\n".join(res)
|
||
|
||
@server.tool()
|
||
def seep_apk_gen_hook(
|
||
framework: str = "frida",
|
||
class_name: str = "com.example.app.MainActivity",
|
||
method_name: str = "checkFlag",
|
||
param_types: Optional[List[str]] = None,
|
||
return_type: str = "boolean",
|
||
hook_timing: str = "after"
|
||
) -> str:
|
||
"""根据类名与方法签名自动生成动态 Hook 脚本(支持 Frida JavaScript 与 LSPilot/BeanShell 两种框架)。
|
||
|
||
Args:
|
||
framework: Hook 框架,'frida' (默认) 或 'bsh' / 'lspilot'
|
||
class_name: 目标完整类名(如 com.example.app.SecurityHelper)
|
||
method_name: 目标方法名(如 verifyLicense)
|
||
param_types: 方法形参类型列表,例如 ['java.lang.String', 'int'],若重载方法需指定
|
||
return_type: 返回值类型,默认 'boolean'
|
||
hook_timing: Hook 时机,'before', 'after', 或 'replace'
|
||
"""
|
||
params_str = ", ".join(param_types) if param_types else ""
|
||
|
||
if framework.lower() == "frida":
|
||
# 生成 Frida 脚本
|
||
joined_types = "', '".join(param_types) if param_types else ""
|
||
overload_args = f"'{joined_types}'" if param_types else ""
|
||
js_code = f"""// Frida Hook Script for {class_name}.{method_name}
|
||
// Generated by seep MCP Toolkit
|
||
Java.perform(function () {{
|
||
var target = Java.use("{class_name}");
|
||
var method = target["{method_name}"];
|
||
if ({'true' if param_types else 'false'}) {{
|
||
method = method.overload({overload_args});
|
||
}}
|
||
|
||
method.implementation = function () {{
|
||
console.log("[*] Hooked {class_name}.{method_name} called");
|
||
for (var i = 0; i < arguments.length; i++) {{
|
||
console.log(" arg[" + i + "]: " + arguments[i]);
|
||
}}
|
||
"""
|
||
if hook_timing.lower() == "replace":
|
||
default_ret = "true" if return_type == "boolean" else "null" if return_type == "void" else "0"
|
||
js_code += f""" console.log("[*] Return value hijacked to: {default_ret}");
|
||
return {default_ret};
|
||
}};
|
||
}});"""
|
||
else:
|
||
js_code += f""" var ret = this.{method_name}.apply(this, arguments);
|
||
console.log("[*] Original return value: " + ret);
|
||
return ret;
|
||
}};
|
||
}});"""
|
||
return js_code
|
||
|
||
else:
|
||
# 生成 LSPilot / BeanShell 脚本
|
||
template_file = os.path.join(HOOK_TEMPLATES_DIR, f"hook_{hook_timing.lower()}.bsh")
|
||
if not os.path.isfile(template_file):
|
||
template_file = os.path.join(HOOK_TEMPLATES_DIR, "hook_after.bsh")
|
||
|
||
with open(template_file, "r", encoding="utf-8", errors="replace") as fh:
|
||
content = fh.read()
|
||
|
||
pre_find = ""
|
||
if param_types:
|
||
param_classes = ", ".join([f'ReflectUtils.findClass("{p}", hostLoader)' for p in param_types])
|
||
pre_find = f'Class[] paramTypes = new Class[] {{ {param_classes} }};\n java.lang.reflect.Method targetMethod = ReflectUtils.findMethod(targetClass, "{method_name}", paramTypes);'
|
||
else:
|
||
pre_find = f'java.lang.reflect.Method targetMethod = ReflectUtils.findMethod(targetClass, "{method_name}", null);'
|
||
|
||
content = content.replace("${GEN_TIME}", "Now")
|
||
content = content.replace("${PLUGIN_NAME}", f"hook_{method_name}")
|
||
content = content.replace("${TARGET_CLASS}", class_name)
|
||
content = content.replace("${TARGET_METHOD}", method_name)
|
||
content = content.replace("${TARGET_DESC}", f"({params_str})")
|
||
content = content.replace("${PRE_FIND_METHOD}", pre_find)
|
||
content = content.replace("${CALLBACK_CODE}", f'// TODO: Add custom manipulation for {method_name}')
|
||
return content
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. ReverseLab 攻防战术知识库与 CTF Payload 库
|
||
# ---------------------------------------------------------------------------
|
||
@server.tool()
|
||
def seep_kb_search(query: str, category: Optional[str] = None, limit: int = 10) -> str:
|
||
"""在 ReverseLab 183+ 逆向与 Web/PE/CVE 攻防知识库中进行全文搜索,快速定位战术指南。
|
||
|
||
Args:
|
||
query: 检索关键词(例如 'JWT', 'SSRF', 'vmp', 'proto pollution', 'heap', 'yara')
|
||
category: 可选限定目录分类(例如 'ctf-website', 'pe-reverse', 'apk-reverse', 'general')
|
||
limit: 最大返回文章数,默认 10
|
||
"""
|
||
search_root = os.path.join(KB_DIR, category) if category else KB_DIR
|
||
if not os.path.isdir(search_root):
|
||
search_root = KB_DIR
|
||
|
||
md_files = glob.glob(os.path.join(search_root, "**", "*.md"), recursive=True)
|
||
pat = query.lower()
|
||
|
||
hits = []
|
||
for fpath in md_files:
|
||
try:
|
||
with open(fpath, "r", encoding="utf-8", errors="ignore") as fh:
|
||
content = fh.read()
|
||
title = ""
|
||
for line in content.splitlines()[:5]:
|
||
if line.startswith("# "):
|
||
title = line.lstrip("# ").strip()
|
||
break
|
||
|
||
score = 0
|
||
if pat in Path(fpath).name.lower():
|
||
score += 10
|
||
if pat in title.lower():
|
||
score += 8
|
||
if pat in content.lower():
|
||
score += content.lower().count(pat)
|
||
|
||
if score > 0:
|
||
rel = os.path.relpath(fpath, KB_DIR)
|
||
hits.append({
|
||
"path": rel.replace("\\", "/"),
|
||
"title": title or Path(fpath).stem,
|
||
"score": score,
|
||
"snippet": content[:300].strip().replace("\n", " ")
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
hits.sort(key=lambda x: x["score"], reverse=True)
|
||
top = hits[:limit]
|
||
if not top:
|
||
return f"No articles found matching '{query}' in ReverseLab knowledge base."
|
||
|
||
return json.dumps(top, indent=2, ensure_ascii=False)
|
||
|
||
@server.tool()
|
||
def seep_kb_read(article_path: str, max_chars: int = 6000) -> str:
|
||
"""读取 ReverseLab 知识库中某篇战术文档或技巧手册的全文内容。
|
||
|
||
Args:
|
||
article_path: 相对 KB 根目录的路径(可通过 seep_kb_search 获得,例如 'ctf-website/checklists/web-ctf-first-30-min.md')
|
||
max_chars: 最大返回字符数,默认 6000
|
||
"""
|
||
target = os.path.join(KB_DIR, article_path)
|
||
if not os.path.isfile(target):
|
||
# 尝试模糊匹配
|
||
matches = glob.glob(os.path.join(KB_DIR, "**", f"*{article_path}*"), recursive=True)
|
||
if matches and os.path.isfile(matches[0]):
|
||
target = matches[0]
|
||
else:
|
||
return f"[ERROR]: Article not found: {article_path}"
|
||
|
||
with open(target, "r", encoding="utf-8", errors="replace") as fh:
|
||
text = fh.read()
|
||
if len(text) > max_chars:
|
||
return text[:max_chars] + f"\n\n... [Truncated: Total {len(text)} characters, showing first {max_chars}]"
|
||
return text
|
||
|
||
@server.tool()
|
||
def seep_kb_checklist(checklist_type: str = "web_first_30_min") -> str:
|
||
"""获取精炼的 CTF 竞技战术排查清单与工作流。
|
||
|
||
Args:
|
||
checklist_type: 清单类型,支持:
|
||
- 'web_first_30_min': Web CTF 前 30 分钟应急排查指南
|
||
- 'attack_matrix': 攻击面全景矩阵
|
||
- 'evidence': 证据链归档标准模板
|
||
- 'pe_ioc': PE 逆向 IOC 提取指南
|
||
- 'pe_persistence': PE 启动持久化链路排查
|
||
- 'yara_rules': YARA 规则编写指南
|
||
"""
|
||
map_dict = {
|
||
"web_first_30_min": "ctf-website/checklists/web-ctf-first-30-min.md",
|
||
"attack_matrix": "ctf-website/checklists/attack-matrix.md",
|
||
"evidence": "ctf-website/checklists/evidence.md",
|
||
"pe_ioc": "pe-reverse/techniques/06-ioc-extraction/01-ioc-extraction.md",
|
||
"pe_persistence": "pe-reverse/techniques/06-ioc-extraction/02-persistence-startup-chain.md",
|
||
"yara_rules": "pe-reverse/techniques/07-yara-sigma/01-yara-rule-writing.md"
|
||
}
|
||
rel_path = map_dict.get(checklist_type.lower(), "ctf-website/checklists/web-ctf-first-30-min.md")
|
||
return seep_kb_read(rel_path)
|
||
|
||
@server.tool()
|
||
def seep_kb_payloads(category: str = "all") -> str:
|
||
"""检索 CTF 常用高危漏洞 Exploit 种子与 Payload 集合(含 SQLi, SSTI, Prototype Pollution, JWT, SSRF, Deserialization 等)。
|
||
|
||
Args:
|
||
category: 类别过滤('all', 'jwt', 'ssrf', 'sqli', 'ssti', 'prototype'),默认 'all'
|
||
"""
|
||
payload_file = os.path.join(KB_DIR, "ctf-website", "payloads", "web-payload-seeds.md")
|
||
if not os.path.isfile(payload_file):
|
||
return "[ERROR]: Payload seeds file not found."
|
||
|
||
with open(payload_file, "r", encoding="utf-8", errors="replace") as fh:
|
||
content = fh.read()
|
||
|
||
if category.lower() == "all":
|
||
return content
|
||
|
||
# 提取对应章节
|
||
lines = content.splitlines()
|
||
selected_lines = []
|
||
capturing = False
|
||
target = category.lower()
|
||
|
||
for line in lines:
|
||
if line.startswith("#"):
|
||
if target in line.lower():
|
||
capturing = True
|
||
elif capturing and (line.startswith("## ") or line.startswith("# ")):
|
||
break
|
||
if capturing:
|
||
selected_lines.append(line)
|
||
|
||
if selected_lines:
|
||
return "\n".join(selected_lines)
|
||
return f"No section matching '{category}' in payloads file. Returning full document header:\n" + "\n".join(lines[:60])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. 战术助手与安全评估增强工具 (Task Isolation, Triage, Report & IDA Bridge)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@server.tool()
|
||
def seep_task_init(task_name: str, description: str = "") -> str:
|
||
"""
|
||
【工作空间铁律保障】为新任务一键在根目录下初始化独立的任务沙箱工作区。
|
||
自动创建规范子目录:samples/, exports/, patches/, reports/,
|
||
杜绝文件乱堆乱放和根目录污染。
|
||
"""
|
||
clean_name = re.sub(r'[^\w\-]', '_', task_name.strip())
|
||
if not clean_name:
|
||
return "[ERROR] task_name 不能为空"
|
||
|
||
task_path = os.path.join(BASE_DIR, clean_name)
|
||
subdirs = ["samples", "exports", "patches", "reports"]
|
||
|
||
try:
|
||
os.makedirs(task_path, exist_ok=True)
|
||
created_dirs = [clean_name]
|
||
for sub in subdirs:
|
||
p = os.path.join(task_path, sub)
|
||
os.makedirs(p, exist_ok=True)
|
||
created_dirs.append(f"{clean_name}/{sub}")
|
||
|
||
readme_path = os.path.join(task_path, "README.md")
|
||
if not os.path.exists(readme_path):
|
||
with open(readme_path, "w", encoding="utf-8") as f:
|
||
f.write(f"# 任务工作区: {clean_name}\n\n")
|
||
f.write(f"- 创建时间: {subprocess.getoutput('date /t') if sys.platform == 'win32' else subprocess.getoutput('date')}\n")
|
||
f.write(f"- 任务描述: {description or '授权软件安全检测与白盒鉴权脆弱性走查任务'}\n\n")
|
||
f.write("## 目录约定\n")
|
||
f.write("- `samples/`: 原始目标程序与待测样本\n")
|
||
f.write("- `exports/`: 反编译伪代码、字符串导出、日志与分析证据\n")
|
||
f.write("- `patches/`: 验证 PoC 代码、代理 DLL 跳板工程、热补丁\n")
|
||
f.write("- `reports/`: 最终安全评估与漏洞复现报告\n")
|
||
|
||
return json.dumps({
|
||
"status": "Success",
|
||
"task_directory": task_path,
|
||
"created_directories": created_dirs,
|
||
"message": f"任务工作区 [{clean_name}] 已成功创建!后续所有产物请严格保存于该目录内部。"
|
||
}, indent=2, ensure_ascii=False)
|
||
except Exception as e:
|
||
return f"[ERROR] 初始化任务工作区失败: {str(e)}"
|
||
|
||
|
||
@server.tool()
|
||
def seep_auto_triage(target_path: str, task_name: Optional[str] = None) -> str:
|
||
"""
|
||
一键全自动靶向侦察 (Auto Triage):
|
||
对给定目标(PE/EXE/DLL/APK/ELF)进行 360° 综合体检:
|
||
1. 计算 MD5 / SHA256 文件哈希
|
||
2. 探测文件类型与架构位数 (32/64 bit, x86/ARM)
|
||
3. 枚举关键安全缓解机制 (Canary/NX/PIE/ASLR)
|
||
4. 提取高价值鉴权/密钥/网络特征字符串
|
||
5. 若提供 task_name,自动将完整报告输出到该任务的 reports/ 目录
|
||
"""
|
||
if not os.path.exists(target_path):
|
||
return f"[ERROR] Target path not found: {target_path}"
|
||
|
||
t_path = Path(target_path).resolve()
|
||
file_size = os.path.getsize(t_path)
|
||
|
||
# 1. 计算哈希
|
||
md5_cmd = [RABIN2_EXE, "-k", "md5", str(t_path)] if os.path.exists(RABIN2_EXE) else None
|
||
sha256_cmd = [RABIN2_EXE, "-k", "sha256", str(t_path)] if os.path.exists(RABIN2_EXE) else None
|
||
|
||
import hashlib
|
||
with open(t_path, "rb") as f:
|
||
data = f.read(1024 * 1024 * 50) # 最大前 50MB 计算
|
||
md5_val = hashlib.md5(data).hexdigest()
|
||
sha256_val = hashlib.sha256(data).hexdigest()
|
||
|
||
# 2. 调用 r2 获取架构和安全缓解
|
||
sec_info = "Unknown"
|
||
bin_info = {}
|
||
if os.path.exists(RABIN2_EXE):
|
||
raw_info = _run_cmd([RABIN2_EXE, "-Ij", str(t_path)])
|
||
try:
|
||
bin_info = json.loads(raw_info).get("info", {})
|
||
except Exception:
|
||
pass
|
||
|
||
# 3. 提取高价值安全关键词(License / VIP / Auth / Expire 等)
|
||
sensitive_keywords = ["vip", "license", "auth", "expire", "token", "subscri", "register", "check", "trial", "valid"]
|
||
matched_strings = []
|
||
|
||
if os.path.exists(RABIN2_EXE):
|
||
raw_str = _run_cmd([RABIN2_EXE, "-zj", str(t_path)])
|
||
try:
|
||
str_list = json.loads(raw_str).get("strings", [])
|
||
for s in str_list:
|
||
val = s.get("string", "")
|
||
v_lower = val.lower()
|
||
if any(k in v_lower for k in sensitive_keywords):
|
||
matched_strings.append({
|
||
"vaddr": hex(s.get("vaddr", 0)),
|
||
"string": val[:80]
|
||
})
|
||
if len(matched_strings) >= 20:
|
||
break
|
||
except Exception:
|
||
pass
|
||
|
||
triage_result = {
|
||
"file_name": t_path.name,
|
||
"file_size": f"{file_size / (1024 * 1024):.2f} MB",
|
||
"hashes": {
|
||
"md5": md5_val,
|
||
"sha256": sha256_val
|
||
},
|
||
"binary_profile": {
|
||
"arch": bin_info.get("arch", "unknown"),
|
||
"bits": bin_info.get("bits", "unknown"),
|
||
"format": bin_info.get("bintype", "unknown"),
|
||
"canary": bin_info.get("canary", False),
|
||
"nx": bin_info.get("nx", False),
|
||
"pic": bin_info.get("pic", False),
|
||
"stripped": bin_info.get("stripped", False),
|
||
"compiler": bin_info.get("compiler", "unknown")
|
||
},
|
||
"auth_related_strings_sampled": matched_strings[:15],
|
||
"recommended_action": (
|
||
"检测到高价值鉴权关键词与符号,建议下一步:"
|
||
"1. 若存在 IDA 环境,直接使用 mcp_ida_get_function_by_name 或地址枚举决策分支;"
|
||
"2. 若为本地逻辑判定,使用 seep_apk_gen_hook 或生成免杀热补丁跳板验证;"
|
||
"3. 报告与验证代码请归档入对应任务的 reports/ 与 patches/。"
|
||
)
|
||
}
|
||
|
||
if task_name:
|
||
clean_task = re.sub(r'[^\w\-]', '_', task_name.strip())
|
||
report_dir = os.path.join(BASE_DIR, clean_task, "reports")
|
||
if os.path.exists(report_dir):
|
||
out_file = os.path.join(report_dir, f"{t_path.stem}_triage.json")
|
||
with open(out_file, "w", encoding="utf-8") as f:
|
||
json.dump(triage_result, f, indent=2, ensure_ascii=False)
|
||
triage_result["saved_report"] = out_file
|
||
|
||
return json.dumps(triage_result, indent=2, ensure_ascii=False)
|
||
|
||
|
||
@server.tool()
|
||
def seep_ida_status() -> str:
|
||
"""
|
||
检查本地 IDA Pro 逆向分析服务与 MCP 桥接器是否在线就绪。
|
||
探测端口 127.0.0.1:13337。
|
||
"""
|
||
import urllib.request
|
||
url = "http://127.0.0.1:13337"
|
||
try:
|
||
req = urllib.request.Request(url, method="GET")
|
||
with urllib.request.urlopen(req, timeout=2) as resp:
|
||
return json.dumps({
|
||
"status": "Online",
|
||
"code": resp.status,
|
||
"message": "IDA Pro MCP 服务正在稳定运行!可直接调用 mcp_ida_* 工具链展开反编译与交叉引用深度分析。"
|
||
}, indent=2, ensure_ascii=False)
|
||
except Exception as e:
|
||
return json.dumps({
|
||
"status": "Offline / Unreachable",
|
||
"detail": str(e),
|
||
"hint": "若需使用 IDA 深度分析,请在 IDA Pro GUI 中确认插件已加载,或按下 Ctrl+Alt+M 唤醒。"
|
||
}, indent=2, ensure_ascii=False)
|
||
|
||
|
||
@server.tool()
|
||
def seep_gen_security_report(
|
||
task_name: str,
|
||
target_name: str,
|
||
vuln_type: str = "CWE-602: 客户端鉴权与状态判定缺陷",
|
||
branch_location: str = "0x0",
|
||
description: str = "",
|
||
poc_summary: str = "",
|
||
defense_recommendation: str = ""
|
||
) -> str:
|
||
"""
|
||
自动生成专业级白盒安全漏洞评估报告。
|
||
按照正规合规标准排版,包含漏洞成因、位点、复现 PoC 与企业级防护加固方案。
|
||
报告自动落入任务文件夹的 reports/ 目录。
|
||
"""
|
||
clean_task = re.sub(r'[^\w\-]', '_', task_name.strip())
|
||
report_dir = os.path.join(BASE_DIR, clean_task, "reports")
|
||
os.makedirs(report_dir, exist_ok=True)
|
||
|
||
report_file = os.path.join(report_dir, f"{target_name}_security_assessment_report.md")
|
||
|
||
now_str = subprocess.getoutput('date /t') if sys.platform == 'win32' else subprocess.getoutput('date')
|
||
|
||
default_remediation = (
|
||
"1. **服务端统一鉴权 (Server-side Enforcement)**:将核心特权逻辑迁移至云端,客户端仅作为渲染终端,所有受限资源由服务端颁发带有时间戳和数字签名的 Token 换取。\n"
|
||
"2. **非对称公钥防篡改**:若需支持离线场景,必须使用 RSA/ECC 非对称签名,客户端仅保留公钥验签,私钥由授权服务器持有。\n"
|
||
"3. **内存完整性保护**:启用 Windows `ProcessDynamicCodePolicy` 抑制动态内存写入,并在核心敏感模块增加代码段自校验哈希。"
|
||
)
|
||
|
||
remediation_text = defense_recommendation or default_remediation
|
||
|
||
content = f"""# 软件安全评估与鉴权脆弱性走查报告
|
||
|
||
- **目标资产**: `{target_name}`
|
||
- **评估类型**: 白盒代码走查 / 客户端决策逻辑安全性验证
|
||
- **缺陷分类**: `{vuln_type}`
|
||
- **评估时间**: {now_str}
|
||
- **报告归属**: `{clean_task}/reports/`
|
||
|
||
---
|
||
|
||
## 1. 风险概述与脆弱性成因 (Vulnerability Summary)
|
||
{description or "在白盒审计过程中,发现该客户端采用本地离线决策逻辑,缺乏服务端强鉴权与非对称加密验签。客户端程序完全依据本地存储状态或单一标志位分支进行权限放行,构成 CWE-602 本地判定信任缺陷。"}
|
||
|
||
---
|
||
|
||
## 2. 关键判定决策位点 (Decision Branch Location)
|
||
- **判定函数 / 偏移位点**: `{branch_location}`
|
||
- **汇编指令与分支模式**:
|
||
```text
|
||
; 典型本地鉴权判断分支
|
||
TEST EAX, EAX
|
||
JZ loc_restricted_feature ; 本地跳转决定受限功能
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 验证 PoC 与复现过程 (Proof of Concept)
|
||
{poc_summary or "通过动态 Hook / 内存标志位修正,成功验证在不经过服务端鉴权的前提下,本地逻辑可以直接放行相关受限功能。验证代码已归档至 patches/ 目录。"}
|
||
|
||
---
|
||
|
||
## 4. 纵深防护与企业级加固修复建议 (Remediation)
|
||
{remediation_text}
|
||
"""
|
||
|
||
with open(report_file, "w", encoding="utf-8") as f:
|
||
f.write(content)
|
||
|
||
return json.dumps({
|
||
"status": "Success",
|
||
"report_path": report_file,
|
||
"message": f"安全评估报告已正式生成并归档至: {clean_task}/reports/{os.path.basename(report_file)}"
|
||
}, indent=2, ensure_ascii=False)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 主入口
|
||
# ---------------------------------------------------------------------------
|
||
def main():
|
||
"""MCP Server 启动入口,使用 stdio 协议与 AI Client 通信"""
|
||
if sys.platform == "win32":
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
sys.stderr.reconfigure(encoding="utf-8")
|
||
sys.stdin.reconfigure(encoding="utf-8")
|
||
except Exception:
|
||
pass
|
||
|
||
server.run(transport="stdio")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|