delphi-mcp
by tzhtdx
README.md
# delphi-mcp
Delphi 逆向符号恢复 MCP 服务:把多个 Delphi 反编译引擎统一到一个 MCP 接口后面,为 AI agent 提供静态符号知识,并与 IDA MCP(ida-mcp)协同工作。
在真实恶意样本上验证:Delphi 2 ~ 13 Florence 全版本、32/64 位,纯 Python,零编译依赖。
> 想了解"做了什么、为什么能做、二进制变成了什么"——见 [docs/TECHNICAL.md](docs/TECHNICAL.md)。
> 想知道"能干什么、输出长什么样、怎么用"——见 [docs/USAGE.md](docs/USAGE.md)(使用手册,含真实输出样例)。
> 性能演进与卡死复盘——见 [docs/PERFORMANCE.md](docs/PERFORMANCE.md)。
> 文件职责速查——见 [docs/FILES.md](docs/FILES.md)。
## 为什么
老 Delphi 样本(尤其恶意软件)在 IDA/Ghidra 里函数名全是 `sub_xxxx`,分析极度依赖符号恢复。现有工具要么只有 GUI(IDR)、要么绑定特定平台(DelphiReSym 依赖 Ghidra)、要么恢复能力弱。delphi-mcp 把它们的分析能力抽取为统一的、可脚本化的、给 agent 用的服务。
## 特性
- **三引擎自动路由**:按 Delphi 版本选择最合适的分析引擎,输出统一的符号模型
- **12 个 MCP 工具**:从版本判定到符号查询、行为情报、模型生命周期、IDA 协同
- **纯 Python 引擎**:`classic` / `modern` 两个引擎无编译依赖(仅需 `pefile`、`numpy`、`capstone`)
- **IDR 知识库匹配**:直接解析 `kb*.bin`(4 万+ RTL/VCL 函数字节模式),命名虚表槽与调用目标
- **行为情报**:字符串×引用、敏感 API 指纹、DFM 表单→事件、XRef 调用图
- **为 agent 设计**:stdout=JSON、结果按需分页、大产物落盘只回传路径、磁盘缓存
## 架构
```
delphi_detect ─── 版本 / 架构判定
│
┌─────────────────┼──────────────────┐
▼ ▼ ▼
Delphi 2..2007 Delphi 2009+ (idr-cli 强制)
classic 引擎 modern 引擎 legacy 引擎
(纯 Python) (纯 Python) (C++ 子进程)
RTTI+KB 匹配 VMT/MDT/RTTI syskb 19 步流水线
│ │ │
└─────────────────┴──────────────────┘
▼
统一符号模型 SymbolModel (JSON)
▼
query / strings / imports / forms / xrefs / renames / export
▼
ida-mcp 深挖(可选)
```
三个引擎都产出同一套 `SymbolModel`(类/方法/参数/字段/单元/接口/变量),后端与 agent 无需关心用的是哪个引擎。
### 引擎职责
| 引擎 | 版本范围 | 实现 | 恢复内容 |
|------|----------|------|----------|
| `classic` | Delphi 2~2007(老格式,负偏移 VMT) | 纯 Python | 类/RTTI、虚表槽(KB 命名)、动态消息方法、接口、字段、调用目标发现、单元 |
| `modern` | Delphi 2009~13(新格式,正偏移 VMT) | 纯 Python(DelphiReSym 衍生) | 类/VMT/MDT、方法名+参数+返回类型、RTTI 命名空间 |
| `legacy` | Delphi 2~2007 | C++ `idr-cli` 子进程 | IDR 完整 19 步流水线(可选深度增强/兜底) |
## 安装
依赖(Python ≥ 3.11):
```bash
pip install mcp pefile numpy capstone
```
`legacy` 引擎需要 `idr-cli.exe`(可选,缺失时 classic 引擎仍然工作):
```bash
# 见 D:\Code\IDR\idr-cli,或设 IDR_CLI_EXE
```
IDR 知识库(可选,启用 KB 匹配):从 IDR 安装目录取得 `kb2.bin` … `kb2014.bin`,放入 `IDR_KB_DIR`。
运行:
```bash
PYTHONPATH=src python -m delphi_mcp.server
```
## MCP 工具
| 工具 | 作用 |
|------|------|
| `delphi_detect(file)` | 秒级判定版本/架构/引擎路由 |
| `delphi_analyze(file, version?, refresh?, section?, offset?, limit?, with_methods?)` | 全量分析 → 符号模型;**按 section 分页**(summary/classes/methods/units/forms/vars/full,默认 summary,避免大 JSON) |
| `delphi_query(file, class_name?/addr?/addrs?)` | 查符号:类名子串 / 单地址 / **地址批量查表(pull,给 IDA 的地址回名)** |
| `delphi_list()` | 列出当前常驻内存的模型(类比 ida-mcp `list_databases`) |
| `delphi_close(file, drop_sidecar?)` | 卸载该样本的缓存模型(释放内存),可选删 `.delphi_model.json` |
| `delphi_triage(file, include_noise?)` | **一键初筛**:版本+规模+信号串(**表驱动分类** persistence/creds_path/network/exec/mutex,去重;噪声默认隐藏)+ 敏感 API + DLL 导出 |
| `delphi_strings(file, pattern?, with_refs?)` | 字符串(带 Delphi 头分类)+ 引用函数归因 |
| `delphi_imports(file, only_sensitive?)` | 导入表 + 行为分类(process/persist/net/crypto/evasion) |
| `delphi_forms(file, with_addresses?)` | DFM 表单控件树 + 事件处理函数→地址 |
| `delphi_xrefs(file, addr)` | 调用者/跳转者/数据读者(静态调用图查询) |
| `delphi_renames(file, base?, class_filter?, limit?, offset?, save_idc?, addrs?, overwrite?, min_confidence?)` | 生成 `[{addr,name,confidence}]` / `.idc`;`addrs`=只改你选的地址;默认**不覆盖导出**、`min_confidence` 可过滤低可信名 |
| `delphi_export(file, fmt?)` | 导出 idc/lst/pas/idp |
## 与 IDA / ida-mcp 协同
核心资产是**地址 → 符号名映射**。两侧地址对齐:Delphi PE `image_base` 由 `delphi_detect` 输出(32-bit 通常 `0x400000`),与 ida-mcp `survey_binary` 的 `base_address` 核对即可(rebased 时用 `delphi_renames(base=…)`)。
### 两种模式
- **批量灌入(plan)**:`delphi_renames(file, min_confidence="medium")` → 落盘 `.idc` → 在 IDA 里 `File > Script file` 执行一次。
- **按需查表(pull,推荐)**:让 IDA 先看,遇到关注的地址再问 delphi——不把全量改名拉进上下文,也不会误灌低可信名。
### pull 闭环(推荐流程)
```
1. IDA 定位 (ida-mcp) survey_binary / decompile / xrefs → 关注地址集合
2. 查表 delphi_query(file, addrs=[...]) → 每个地址的名字(含 kind/confidence)
模型未命中 → 回退 PE 导出表 (export:<name>)
3. 筛选 按 confidence 取舍 (high=MDT/RTTI, medium=KB虚表, low=调用图)
4. 出计划 delphi_renames(file, addrs=[...], min_confidence="medium")
→ 只含你选的地址:小 .idc + [{addr,name,confidence}](默认不动导出)
5. 回灌 IDA 执行小 .idc / 对 [{addr,name}] 逐条 ida_mcp_rename
```
**名字来源与可信度**:`high`=编译器元数据(MDT/RTTI)、`medium`=KB 字节匹配(虚表槽)、`low`=调用图发现。`min_confidence` 只回灌高可信名;改名默认**跳过 PE 导出**(IDA 已命名),`overwrite=true` 才强制覆盖。
### 把改名应用到 IDA(当前无原生 batch rename)
`.idc` 落盘后两种落地方式:
- **IDA GUI**:`File > Script file` 选该 `.idc`(一次性;小 `.idc` 即 pull 模式产物)。
- **ida-mcp 会话内**:用 `execute_python` 解析后 `set_name`(用 `SN_NOWARN`,不覆盖 IDA 已在名):
```python
import re, ida_name
txt = open(r"...\sample.renames.idc", encoding="utf-8").read()
for a, n in re.findall(r'MakeNameEx\((0x[0-9A-Fa-f]+),\s*"(.*?)"', txt):
ida_name.set_name(int(a,16), n.replace('\\\\','\\').replace('\\"','"'), ida_name.SN_NOWARN)
```
### 实测(GRenam,Delphi 2007)
- `delphi_renames(min_confidence="medium")` → **1522** 条;IDA 应用 **1510**。
- 3834 函数中 **1185 个**由 `sub_` 变为真实符号;反编译 `uMain.TfrmMain.FormCreate` 可见 `ExtCtrls.TTimer.Create(...)`。
- 病毒自写函数无 Delphi 元数据 → 仍为 `sub_`:**delphi 给“地图”,ida 给“地形”**。
> 详见 `docs/TECHNICAL.md` §5.1。
## 配置
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `IDR_CLI_EXE` | `D:\Code\IDR\idr-cli\modern\build\Release\idr-cli.exe` | legacy 引擎二进制 |
| `IDR_KB_DIR` | `<IDR_DIR>` | `kb*.bin` 知识库目录 |
| `DELPHI_LEGACY_ENGINE` | 空 | 设为 `idr-cli` 强制老版本用 C++ 引擎 |
| `DELPHI_NO_KB_CACHE` | 空 | 设为 `1` 禁用 KB 进程内缓存 |
| `DELPHI_TRACE` | 空 | 设为 `1` 打印分析各阶段耗时 |
| `DELPHI_MODEL_CACHE_MAX` | `3` | 内存中常驻符号模型数上限(LRU 淘汰) |
| `DELPHI_GC_ON_EVICT` | `1` | 设为 `0` 时淘汰/关闭模型不触发 `gc.collect()` |
| `DELPHI_IDRCLI_TIMEOUT` | `900` | idr-cli `analyze`/`export` 子进程超时(秒),用于兜底 C++ 路径 |
| `DELPHI_IDRCLI_FALLBACK` | `1` | 设为 `0` 关闭 classic→idr-cli 兜底(classic 0 类时直接返回,不跑 C++) |
| `DELPHI_SIGNAL_RULES` | 空 | `delphi_triage` 信号规则**覆盖表**(JSON 路径,含 `signal`/`noise`/`framework_prefixes`,与内置表合并) |
opencode.json(stdio 本地,每会话各起一份进程):
```json
"delphi-mcp": {
"type": "local",
"command": ["python", "-m", "delphi_mcp.server"],
"environment": { "PYTHONPATH": "D:\\Code\\IDR\\delphi-mcp\\src" },
"enabled": true
}
```
### 常驻远程模式(推荐,避免“每会话一份 python”)
`type: local` 下 opencode 会**按会话/窗口各起一份**进程(多窗口 = 多份 python)。改成**一个常驻服务 + `remote` 连接**,所有会话共用一份(模型 LRU 也共享):
```bash
# 启动一次并保持运行(Windows 可用 scripts/serve-delphi-mcp.bat)
python -m delphi_mcp.serve
# 默认 127.0.0.1:8910 (SSE);可用环境变量调整:
# DELPHI_MCP_TRANSPORT=sse|streamable-http DELPHI_MCP_HOST DELPHI_MCP_PORT
```
```json
"delphi-mcp": { "type": "remote", "url": "http://127.0.0.1:8910/sse", "enabled": true }
```
开机自启:用任务计划程序在登录时运行 `scripts/serve-delphi-mcp.bat`。SSE 与 streamable-http 两种传输都支持(后者路径为 `/mcp`)。
## 性能
实测(本机,含 KB 索引一次性构建):
| 样本 | 版本 | 类/方法 | 耗时 |
|------|------|---------|------|
| GRenam | 2006 | 109 / 3148 | 3.0s |
| Rekram | 2006 | 144 / 3761 | 3.1s |
| DanaBot | 10.2 | 300 / 973 | 3.5s |
| Culebra | XE2-4 | 738 / 3864 | 4.2s |
| libcef.dll | 2014 | 1854 / 12190 | 9.4s |
### 卡死问题与修复(开发笔记)
早期版本的 `classic` 引擎在较大样本(如 Culebra)上会**假死**(CPU 100%、长时间无输出)。根因是三处缺陷叠加:
1. **主因**:`build_anchor_index` 只构建单字节索引 `_anchor`,从不构建两级索引 `_anchor2`,且**无幂等守卫**——`match_slot` 每次因 `_anchor2 is None` 触发全量 4.8 万条 KB 记录索引重建。Culebra 场景 7554 次匹配 × 4.8 万解析 ≈ **3.6 亿次操作**。
2. **掩码 bug**:匹配掩码用 `1 << (k*8)`(每字节只 1 bit)而非 `0xFF << (k*8)`,导致一级预筛恒假、虚表命名静默归零。
3. **低效扫描**:类名回溯对每个类名做一次全 `.text` `find`;索引构建用重型 `proc_info`(解码函数名/参数)。
**修复**(见 `prepare_matching` / `match_slot_fast`):
- 单遍构建两级锚点索引 + 头 8 字节掩码 + 全长掩码,用 `_proc_dump`(仅解析 dump 段)替代重型解析
- `match_slot_fast` 用 numpy 对候选桶做**向量化头字节预筛**,仅对幸存者做全长比对
- 类名回溯改为**单遍 numpy u32 扫描**收集引用
- IntfTable 解析延后到虚表命名之后(槽名查询命中缓存)
- 结果:Culebra **56s → 4.2s(13×)**,结果不变
> 注意:`cProfile` 对千万级 Python 调用有 10~50× 放大,用它剖析此类热点会放大"卡死"观感——用 `DELPHI_TRACE=1` 的分阶段计时更合适。
### MCP 调用超时与修复(开发笔记)
早期版本首次调用 `delphi_triage`(或任何触发冷分析的 `delphi_*`)会在 MCP 客户端侧报 `-32001: Request timed out`。
- **根因**:`classic` 引擎的 KB 匹配器(`engines/kb.py` 的 `prepare_matching`)**懒加载 `numpy`**。FastMCP 把同步工具放到 **anyio 工作线程**执行、且此时事件循环已在运行——在该上下文里加载 numpy 的 C 扩展 `_multiarray_umath` 会**死锁**,工具永不返回,客户端超时。
- **修复**:在**主线程、事件循环启动之前**预加载 numpy(`delphi_mcp/__init__.py` 包导入时 + `server.py` `main()` 里兜底)。缺 numpy 时仍优雅降级(仅失去 KB 快速匹配)。
### idr-cli 兜底耗时
`classic` 引擎若解析出 **0 个类**或抛异常,`router` 会在**确认是 Delphi 二进制**(Delphi 指纹:`.itext` 段 / `.rsrc` 的 `TPF0` DFM / RTL 标记串)后,才回退到 `legacy`(C++ `idr-cli`)——这是正常兜底设计,不是 Python 瓶颈。子进程超时由 `DELPHI_IDRCLI_TIMEOUT`(默认 900s)限定;可用 `DELPHI_IDRCLI_FALLBACK=0` 完全关闭兜底。回退时 `model.warnings` 会带 `classic parser yielded 0 classes; fell back to idr-cli (C++, X.Xs)`。
### 非 Delphi 二进制的误报与快速返回
idr-cli 的快速版本判定对**非 Delphi 二进制**(MSVC/MinGW/Qt 等)可能误报 `version=2`。此时 `detect` 用 Delphi 指纹预检,若**无任何 Delphi 标记**则返回 `engine=not-delphi`、`triage` 直接回报 `delphi:false`、`analyze` 立即报错返回——**不再触发 idr-cli 兜底**(例:Qt 5.1.0 的 UninstallTool.exe 由 ~12s 降至 ~0.3s)。若样本确为加壳 Delphi(元数据被抹除),会被一并判为 not-delphi——加壳样本本就在能力边界之外。
## 能力边界
- **加壳样本**需先脱壳(所有引擎相同)
- **字段类型名**:用户自定义类型可解析,导入/外部类型为 `None`(typesTab 语义限制)
- **老格式 MDT** 不含参数/返回类型信息(`classic` 通过 KB 原型补全命中的函数)
- **64 位**:`modern` 引擎完整支持(符号 + **RIP-relative 的字符串/导入引用归因**,`lea/mov/call [rip+disp32]`、`movabs`);`classic`/`legacy` 仅 32 位
- **idr-cli 弱判定**:对非 Delphi(MSVC/MinGW/Qt)可能误报 `version=2`。`detect` 会做 Delphi 指纹预检,无可信标记则返回 `engine=not-delphi`(快速返回,不跑分析)
- 非 PE / 截断文件:`detect` 返回 `engine=not-pe`,`analyze` 给出明确错误
## 测试
```bash
cd tests
python smoke_all.py # 工具 × 双引擎 + MCP 协议层
python classic_test.py # 老格式语料
python verify_64bit.py # 64 位路径
python download_paper_corpus.py # 从 VT 拉取论文语料
```
`tests/regression_corpus.json` 记录了回归用的本地样本、论文 hash 与拒绝模式清单。
## 目录结构
```
src/delphi_mcp/
├── server.py MCP 组装(12 工具注册)
├── router.py 版本判定 + 引擎路由
├── model.py SymbolModel 统一模型
├── shared.py 缓存 / 路径 / idc 写出
├── xref.py 统一交叉引用收集
├── intel.py 情报 API 再导出
├── strings_api.py 字符串提取 + 分类 + VA 归属
├── imports_api.py IAT 指纹 + 敏感 API 分类
├── dfm.py DFM 资源提取 + TPF0 二进制解析
├── engines/
│ ├── classic.py 老格式引擎(Delphi 2~2007)
│ ├── modern.py 新格式引擎(Delphi 2009+,DelphiReSym 衍生)
│ ├── legacy.py idr-cli 子进程封装
│ ├── kb.py IDR 知识库解析 + 匹配
│ ├── units.py 单元边界/初始化表
│ ├── pe_memory.py PE 文件内存视图
│ ├── vmt_scan.py VMT 扫描
│ ├── mdt.py MDT/方法/参数遍历
│ ├── rtti.py RTTI 对象遍历
│ └── fqn.py 模板感知 FQN 解析
└── tools/ MCP 工具模块(按域拆分)
```
## 致谢
- [crypto2011/IDR](https://github.com/crypto2011/IDR) — Interactive Delphi Reconstructor (MIT)。VMT 偏移表、FieldTable/MethodTable/DynamicTable/IntfTable 解析、单元/初始化表算法均移植自其源码。
- [ajsb85/idr-cli](https://github.com/ajsb85/idr-cli) — IDR 的 VCL-free CLI 移植(知识库格式与匹配器参考)。
- [WenzWenzWenz/DelphiReSym](https://github.com/WenzWenzWenz/DelphiReSym) — 新格式(VMT/MDT/RTTI)解析与论文《Towards Ghidra Reverse Engineering Tools for Delphi Executables》(BSD-3)。`modern` 引擎为其衍生作品。
## License
MIT(本项目代码)。衍生组件见 [LICENSE](LICENSE):`modern` 引擎为 DelphiReSym (BSD-3) 衍生作品,`legacy` 引擎运行时依赖 idr-cli (MIT)。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues