项目地址:github.com/magiclf-ai/agentic-ida-pro
版本:IDA Pro 9.3
核心理念:LLM 主导策略决策,工具执行具体动作,全流程可追溯、可验证、可审计
Agentic IDA Pro 核心思想是:
- LLM 主导策略
- 工具执行动作
- 任务板跟踪状态
- 证据链驱动结论
- 前后快照做验收
二、为什么是 Agent,而不是更多脚本
逆向分析的典型路径是:
1 | 入口定位 -> 调用链展开 -> 函数语义分析 -> 数据结构恢复 -> 类型传播 -> 验证
|
这个路径的关键难点是:它不是固定流程图,而是"观察-决策-执行-再观察"的循环过程。
脚本擅长执行固定步骤,但不擅长在证据变化时动态改计划。而 LLM 正好擅长"弱结构决策"——也就是在不完全信息下做下一步规划。
所以我们把角色拆开:
| 层级 |
负责方 |
职责 |
| 决策层 |
LLM |
观察证据、规划下一步、选择工具 |
| 执行层 |
Tool |
做确定性动作(搜索、反编译、创建结构体) |
| 状态层 |
Task Board / Knowledge |
沉淀结论、管理进度 |
| 审计层 |
Observability / Acceptance |
记录日志、验收结果 |
这就是 Agentic 化真正的价值:不是"替代分析师",而是把分析师的方法论做成能复盘、能审计、能扩展的系统。
三、总体架构:主控 Agent + 子 Agent + IDA 服务
先看主路径:
1 2 3 4 5 6 7 8 9 10 11 | 用户请求
↓
ReverseExpertAgentCore(主策略循环)
↓
Tool Layer(search/xref/decompile/create_structure/...)
↓
IDAClient(HTTP 通信层)
↓
ida_service.daemon(IDA 进程内串行执行)
↓
IDB 变更 + 反编译结果
|
再看横向能力:
| 组件 |
作用 |
| 任务与知识 |
TaskBoard + WorkingKnowledge,管理进度与沉淀认知 |
| 子任务并行 |
spawn_subagent 异步派生,结果自动回流 |
| 上下文压缩 |
prune_context_messages + compress_context_8block |
| 运行验收 |
备份、快照、结构体 diff、失败判定 |
| 可观测性 |
会话、turn、tool、事件入库 SQLite,可视化回放 |
从工程角度看,这个架构有两个关键取舍:
- Python 代码保持"薄控制层",避免把业务逻辑硬编码进 if/else
- 把策略约束写进 prompt + tool docstring,让 Agent 行为可进化
四、多 Agent 分工:不是"多模型炫技",而是角色隔离
很多多 Agent 系统失败,原因是"每个 Agent 都在做所有事"。我们反过来做:每个 Agent 只做自己最擅长、最可验证的一段。
1)主 Agent:ReverseExpertAgentCore
职责:
- 维护主循环(观察 -> 规划 -> 调工具 -> 更新状态)
- 管理任务板和知识库
- 决定是否派生子 Agent
- 最终调用
submit_output 完成收敛
约束:
- 关键任务未关闭,禁止
submit_output
- 有运行中 subagent,禁止
submit_output
- 每轮结论必须引用工具证据
2)DocumentAgent
检索本地 artifacts 中的可复用模板与历史片段,为主 Agent 提供"路径 + 片段 + 使用建议"。
3)Evidence Reviewer
审核证据链是否闭环,识别"字段命名看起来对了但证据不足"的风险点,给出最小补证动作。
4)Reverse Planner
给主 Agent 输出"下一轮唯一关键动作",把动作描述成可执行、可验证、可回退的形式。
5)Context Pruner
对历史消息做高密度压缩,保留可执行记忆,删除噪音上下文。
6)IDAPython Coder / Executor
- Coder 负责写出最小修改脚本
- Executor 负责"执行-报错-修复-再执行"的闭环
这两者共同构成了我们最有特色的一段:CodeAct 式执行环。
五、实战演示:一次完整的结构体恢复流程
下面是一个真实的结构体恢复任务执行日志。我们将逐轮剖析 Agent 的决策过程、工具调用和验证闭环。
任务目标
分析关键函数并恢复结构体定义,给出证据链
这是一个典型的逆向分析需求:不仅要找出程序的核心逻辑,还要把散落在代码中的数据结构还原出来,并且每个结论都要有迹可循。
第一轮:任务规划与入口定位
Agent 首先创建任务列表,把大目标拆成可追踪的子任务:
1 2 3 4 5 6 7 8 9 | [Tool Calls]
- create_task: {
"tasks": [
{"title": "定位关键函数入口/热点函数", "priority": "high"},
{"title": "采集关键函数伪代码与符号使用证据", "priority": "high"},
{"title": "创建/迭代结构体定义并应用类型验证", "priority": "high"},
{"title": "形成证据链报告", "priority": "normal"}
]
}
|
为什么要先创建任务板?
在长时间的分析过程中,LLM 容易"迷失方向"——忘记自己已经做了什么、还需要做什么。任务板把这些状态外化,每完成一个子任务就标记为 done,形成清晰的可审计路径。
任务创建成功后,Agent 开始执行第一个任务:定位入口函数。
1 2 3 4 5 | [Tool Calls]
- search: {
"pattern": "(?i)\\b(main|wmain|winmain|dllmain|start|entry)\\b",
"target_type": "symbol"
}
|
Tool 返回:
1 2 3 4 5 6 | - total_count: 2
1. [symbol/function] main @ 0x1140
2. [symbol/function] start @ 0x14c0
|
找到了两个关键符号:main 和 start。接下来需要确认它们的调用关系。
1 2 | [Tool Calls]
- xref: {"target": "main", "target_type": "symbol", "direction": "to"}
|
Tool 返回:
1 2 3 4 5 6 7 8 9 | - total_count: 5
1. ea=0x3fd8 -> target=0x4060 [__libc_start_main]
2. start+0x1f -> target=0x4060 [__libc_start_main]
3. start+0x18 -> target=0x1140 [main] ← 关键证据
4. start+0x1f -> target=0x3fd8 [__libc_start_main_ptr]
5. ea=0x420 -> target=0x56e [aLibcStartMain]
|
证据解读:
xref 的结果清晰地展示了调用链:start 函数在偏移 +0x18 处调用了 main(地址 0x1140)。这是标准的 C 程序启动流程:start 作为真正的程序入口,通过 __libc_start_main 最终调用用户定义的 main。
Agent 立即更新任务状态,记录这一关键证据:
1 2 3 4 5 6 | [Tool Calls]
- set_task_status: {
"task_ref": "t001",
"status": "in_progress",
"note": "已定位入口符号: main@0x1140, start@0x14C0;xref 证据: start+0x18 -> main"
}
|
第二轮:关键函数反编译与证据采集
入口定位完成,接下来深入分析 main 函数。
1 2 | [Tool Calls]
- decompile_function: {"function_name": "main"}
|
Tool 返回的伪代码(节选):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | __int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 ptr;
ptr = (__int64)calloc(1u, 0x28u);
if ( ptr )
{
*(_DWORD *)(ptr + 8) = 256;
*(_QWORD *)ptr = 0x5244485F544E4F43LL;
*(_QWORD *)(ptr + 32) = sub_15B0;
v6 = calloc(1u, 0x38u);
v10 = calloc(v7, 0x38u);
}
}
|
关键发现:
ptr = calloc(1, 0x28) —— 分配 40 字节,很可能是某个结构体
*(ptr + 8) = 256 —— offset 8 可能是版本号字段
*ptr = 0x5244485F544E4F43 —— offset 0 写入一个 8 字节常量,可能是 magic
*(ptr + 32) = sub_15B0 —— offset 32 写入函数指针
- 后续还有
calloc(1, 0x38) 和 calloc(v7, 0x38) —— 说明还有另外两种大小为 56 字节的结构体
但仅凭 main 的代码还不够,Agent 决定同时反编译 main 中调用的子函数,获取更完整的字段使用证据:
1 2 3 4 5 | [Tool Calls]
- decompile_function: {"function_name": "sub_1780"}
- decompile_function: {"function_name": "sub_15B0"}
- decompile_function: {"function_name": "sub_15F0"}
- decompile_function: {"function_name": "sub_16C0"}
|
sub_1780 的反编译结果(应用类型前):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | __int64 __fastcall sub_1780(__int64 a1, int a2)
{
__int64 v2;
if ( !a1 )
return v13 - __readfsqword(0x28u);
v2 = *(_QWORD *)(a1 + 16);
if ( !v2 )
goto LABEL_13;
while ( *(_DWORD *)v2 != a2 )
{
v2 = *(_QWORD *)(v2 + 32);
if ( !v2 )
goto LABEL_13;
}
v4 = (*(__int64 (**)(void))(a1 + 32))(v2, 16, 0);
}
|
证据解读:
a1 + 16 被解引用作为链表头,说明 offset 16 是 head 指针
v2 + 32 是 next 指针,说明 node 结构体的 offset 32 是 next
a1 + 32 被当作函数指针调用,验证了 main 中 *(ptr + 32) = sub_15B0 的推测
sub_15F0 的反编译结果(应用类型前):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | unsigned __int64 __fastcall sub_15F0(__int64 a1)
{
__int64 v1;
v1 = *(_QWORD *)(a1 + 16);
if ( v1 )
{
v4 = *(unsigned int *)(a1 + 24);
if ( (_DWORD)v4 )
{
v5 = *(_QWORD *)(a1 + 8) & 0xFFFLL;
v6 = v1 + 56 * v4;
do
{
*(_DWORD *)(v1 + 52) |= 1u;
*(_QWORD *)(v1 + 40) += v5;
v1 += 56;
}
while ( v1 != v6 );
}
}
}
|
证据解读:
a1 + 16 指向的内存以 56 字节为步长遍历,且每个元素有 offset 52 和 40 的访问
- 这与
main 中 calloc(v7, 0x38)(0x38 = 56)对应,说明 v1 指向的是 field_desc 数组
Agent 更新任务状态,记录已采集的证据:
1 2 3 4 5 6 | [Tool Calls]
- set_task_status: {
"task_ref": "t002",
"status": "in_progress",
"note": "已反编译 main@0x1140 获得容器与节点构建逻辑;已提取符号使用:calloc(0x28) 容器、calloc(0x38) 节点、calloc(v7,0x38) 字段数组"
}
|
第三轮:结构体建模与创建
基于前面收集的证据,Agent 开始构建结构体定义。
container 结构体推导:
| 偏移 |
大小 |
证据来源 |
语义推断 |
| +0x00 |
8 |
*ptr = 0x5244485F544E4F43 |
magic 字段 |
| +0x08 |
4 |
*(ptr + 8) = 256 |
version |
| +0x0C |
4 |
++*(ptr + 12) |
node_count |
| +0x10 |
8 |
*(ptr + 16) 作为链表头 |
head 指针 |
| +0x18 |
8 |
*(ptr + 24) 指向尾节点 |
tail 指针 |
| +0x20 |
8 |
*(ptr + 32) = sub_15B0 |
hash 函数指针 |
总大小:0x28(40 字节),与 calloc(1, 0x28) 吻合。
node 结构体推导:
| 偏移 |
大小 |
证据来源 |
语义推断 |
| +0x00 |
4 |
*v6 = v33 |
id |
| +0x04 |
4 |
v6[1] = v4 |
flags |
| +0x08 |
8 |
*(v8 + 8) = time(0) |
ts(时间戳) |
| +0x10 |
8 |
*(v8 + 16) = v11 |
fields 指针 |
| +0x18 |
4 |
v8[6] = v7 |
field_count |
| +0x1C |
4 |
未观察到访问 |
unk1C(占位) |
| +0x20 |
8 |
遍历 v2 + 32 |
next 指针 |
| +0x28 |
8 |
双向链表逻辑 |
prev 指针 |
| +0x30 |
8 |
*(v8 + 48) = v20 |
process 函数指针 |
总大小:0x38(56 字节),与 calloc(1, 0x38) 吻合。
field_desc 结构体推导:
| 偏移 |
大小 |
证据来源 |
语义推断 |
| +0x00 |
32 |
snprintf(..., 32, ...) 写入 name |
name[32] |
| +0x20 |
4 |
*(v14 - 6) = v12 & 3 |
kind |
| +0x24 |
4 |
未观察到访问 |
unk24 |
| +0x28 |
8 |
*(v14 - 2) = 8 * v12 |
value |
| +0x30 |
4 |
*(v14 - 1) = 4 << (v12 % 3) |
size |
| +0x34 |
4 |
sub_15F0 中置位操作 |
flags |
Agent 使用 create_structure 创建这三个结构体:
1 2 3 4 | [Tool Calls]
- create_structure: {"name": "field_desc", "c_decl": "struct field_desc { char name[32]; unsigned int kind; unsigned int unk24; unsigned __int64 value; unsigned int size; unsigned int flags; };"}
- create_structure: {"name": "node", "c_decl": "struct node { unsigned int id; unsigned int flags; unsigned __int64 ts; struct field_desc *fields; unsigned int field_count; struct node *next; struct node *prev; void (*process)(struct node*); };"}
- create_structure: {"name": "container", "c_decl": "struct container { unsigned __int64 magic; unsigned int version; unsigned int node_count; struct node *head; struct node *tail; __int64 (*hash)(void *buf, __int64 len, __int64 unused); };"}
|
Tool 返回(确认结构体已落地):
1 2 3 | struct field_desc { ... }; // size=0x38 (56), mutation_effective=true
struct node { ... }; // size=0x38 (56), mutation_effective=true
struct container { ... }; // size=0x28 (40), mutation_effective=true
|
mutation_effective=true 表示结构体已成功写入 IDA 数据库。这是关键一步:结构体不再是"纸面分析",而是真正落地到了 IDB 中。
第四轮:类型应用与验证闭环
结构体创建完成,但这只是开始。接下来必须验证:应用这些类型后,反编译代码是否真的变得更易读了?
Agent 在 main 函数中应用类型:
1 2 3 4 5 6 7 8 9 | [Tool Calls]
- set_identifier_type: {
"function_name": "main",
"operations": [
{"kind": "local", "name": "ptr", "c_type": "struct container *"},
{"kind": "local", "name": "v8", "c_type": "struct node *"}
],
"redecompile": true
}
|
Tool 返回(重反编译后的 main 函数):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | __int64 __fastcall main(int a1, char **a2, char **a3)
{
struct container *ptr;
ptr = (struct container *)calloc(1u, 0x28u);
if ( ptr )
{
ptr->version = 256;
ptr->magic = 0x5244485F544E4F43LL;
ptr->hash = (__int64 (*)(void *, __int64, __int64))sub_15B0;
v8->fields = v11;
v8->process = v20;
if ( ptr->head )
{
tail = ptr->tail;
ptr->tail = v8;
v8->prev = tail;
tail->next = v8;
}
head = ptr->head;
for ( i = head; i; i = i->next )
{
process = i->process;
if ( process )
process(i);
}
}
}
|
验证效果:
ptr->version = 256 替代了 *(_DWORD *)(ptr + 8) = 256
ptr->magic 替代了直接赋值常量
v8->fields、v8->process、v8->prev、v8->next 全部变成了有意义的字段名
- 链表遍历
i = i->next 清晰可读
- 函数调用
process(i) 不再是一堆强制类型转换
同样的验证在子函数中进行:
重反编译后的 sub_1780:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | __int64 __fastcall sub_1780(struct container *a1, unsigned int a2)
{
struct node *head;
head = a1->head;
while ( head->id != a2 )
{
head = head->next;
if ( !head )
goto LABEL_13;
}
v4 = a1->hash(head, 16, 0);
field_count = head->field_count;
if ( (_DWORD)field_count )
{
fields = head->fields;
while ( 1 )
{
field_count = fields->size + field_count;
if ( fields->value > 0x10000 || (fields->flags & 4) != 0 )
break;
if ( ++fields == v8 )
return;
}
}
}
|
重反编译后的 sub_15F0:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | unsigned __int64 __fastcall sub_15F0(struct node *a1)
{
struct field_desc *fields;
fields = a1->fields;
if ( fields )
{
field_count = a1->field_count;
if ( (_DWORD)field_count )
{
v5 = a1->ts & 0xFFF;
v6 = &fields[field_count];
do
{
fields->flags |= 1u;
fields->value += v5;
++fields;
}
while ( fields != v6 );
}
}
}
|
重反编译后的 sub_16C0:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | unsigned __int64 __fastcall sub_16C0(struct node *a1)
{
struct field_desc *fields;
fields = a1->fields;
if ( fields )
{
field_count = a1->field_count;
if ( (_DWORD)field_count )
{
v5 = &fields[field_count];
do
{
fields->flags |= 2u;
if ( fields->kind == 1 )
fields->size *= 2;
++fields;
}
while ( v5 != fields );
}
}
}
|
闭环验证总结:
| 函数 |
应用前 |
应用后 |
改进 |
| main |
*(_DWORD *)(ptr + 8) = 256 |
ptr->version = 256 |
字段语义清晰 |
| main |
v2 = *(_QWORD *)(a1 + 16) |
head = a1->head |
链表操作可读 |
| sub_1780 |
*(_QWORD *)(a1 + 32))(v2, 16, 0) |
a1->hash(head, 16, 0) |
函数指针调用明确 |
| sub_15F0 |
`*(_DWORD *)(v1 + 52) |
= 1` |
`fields->flags |
| sub_16C0 |
v1[8] == 1 |
fields->kind == 1 |
条件判断可读 |
Agent 更新任务状态,标记结构体恢复任务完成:
1 2 3 4 5 6 | [Tool Calls]
- set_task_status: {
"task_ref": "t003",
"status": "done",
"note": "结构体已创建并通过类型应用验证闭环:create_structure 确认大小;set_identifier_type 后伪代码字段访问全部替换为有名字段"
}
|
第五轮:知识沉淀与任务闭环
所有子任务完成后,Agent 将关键发现沉淀到知识区:
1 2 3 4 5 6 7 8 | [Tool Calls]
- knowledge_write: {
"confirmed_facts": "main 使用 calloc(1,0x28) 分配容器对象...",
"evidence": "search: main@0x1140; xref: start+0x18 -> main; decompile main...",
"hypotheses": "container::hash 的第三参数目前固定为 0,可能是保留参数",
"open_questions": "field_desc::unk24 未见读写证据,需在其它函数中找交叉引用",
"do_not_repeat": "不在无偏移访问证据时猜测字段含义"
}
|
这种知识沉淀机制使得分析过程可持续:如果后续发现新的证据,可以回到知识区更新假设;如果另一个子 Agent 在别处发现了 unk24 的访问,可以补充到这里。
最终的任务板状态:
1 2 3 4 5 6 7 | - summary: total=4, active=0, done=4, blocked=0
- [x] t001 (high) 定位关键函数入口/热点函数 [status=done]
- [x] t002 (high) 采集关键函数伪代码与符号使用证据 [status=done]
- [x] t003 (high) 创建/迭代结构体定义并应用类型验证 [status=done]
- [x] t004 (normal) 形成证据链报告 [status=done]
|
所有任务都已闭环,Agent 提交最终输出:
1 2 3 4 5 6 | [Tool Calls]
- submit_output: {
"summary": "已完成关键函数分析并恢复 3 个核心结构体(container/node/field_desc)",
"key_findings": "入口: main@0x1140; container 大小 0x28; node 大小 0x38; field_desc 大小 0x38",
"artifacts": "IDA Structures: container/node/field_desc; 类型应用验证: main/sub_1780/sub_15F0/sub_16C0"
}
|
六、Tool Call 逻辑:为什么强调 docstring 而不是 JSON Schema 堆叠
项目里 tool 调用遵循几条硬规则:
bind_tools + docstring 是参数语义基线
- 参数要显式且最小,避免盲调
- Tool 返回
ERROR: 时先做最小修复再重试
- 有依赖关系的工具必须分轮调用
- 每轮结论必须引用工具证据文本
从实现上看,我们采用了"动态绑定工具集":
- 主循环动态注入运行时工具(任务板、知识库、上下文压缩、submit)
- 子循环使用子 Agent 对应工具集
- 工具执行支持受控并发(有并发上限和错误收集)
这套设计的优势是:策略灵活,但工具边界清晰。
七、IDAPython Agent 的 CodeAct 设计:核心差异化
很多系统把"执行 IDAPython"当成一个黑盒工具:脚本失败了就失败了。我们做的是"脚本失败也能自修复"的可循环执行器。
1)入口:先执行,再决定是否进入修复环
execute_idapython 的路径大致是:
- 先跑一次脚本
- 如果成功,直接返回
- 如果失败,进入 IDAPython repair 子循环
2)修复环:观察错误 -> 最小修改 -> 提交执行 -> 再观察
系统会自动生成子任务板,例如:
- 已执行初始脚本并收集错误
- 待最小修改并提交
- 若知识不足则检索 KB(
search/read_file)
- 成功后退出
这正是 CodeAct 思路在工程里的落地:
- 行动是"可执行代码"
- 观察是"运行输出和报错"
- 下一步动作由上一轮执行结果驱动
3)安全与边界:防破坏优先
我们明确阻断了破坏性结构操作,例如 idc.del_struc。理由很简单:一旦删错结构体,证据链和验收基线都会被污染。
4)IDA 9.3 兼容策略
我们在提示词和执行器里都写了"高风险 API 规避策略":
- 避免盲用兼容性差的接口
- 优先
idc/idautils/ida_hexrays 等稳定模块
- 每次修复只改报错相关行,拒绝大改
这套机制让"会写脚本"升级成"可控地修脚本并完成任务"。
八、任务板与知识库:让 Agent 不是"边跑边忘"
我们把任务管理做成了第一等公民,而不是日志附属品。
任务板支持:
create_task(单条/批量)
set_task_status(todo/in_progress/blocked/done/cancelled)
edit_task
get_task_board
知识库支持:
confirmed_facts —— 已验证的事实
hypotheses —— 待验证的假设
open_questions —— 尚未解答的问题
evidence —— 证据来源
next_actions —— 下一步建议
do_not_repeat —— 避免重复踩坑
为什么这很关键?因为它把"模型短期上下文"转换成"可持续记忆"。尤其在长链路逆向任务里,没有这层结构化记忆,系统几乎一定会重复劳动。
九、上下文压缩:长任务场景里必须有"记忆管理"
长周期分析常见问题是上下文污染:历史消息越多,模型越容易掉焦点。我们设计了两层机制:
prune_context_messages:按 Message ID 定点折叠
compress_context_8block:触发 8-block 蒸馏快照
蒸馏不是"摘要文学",而是保留可执行记忆:
- 当前目标
- 关键证据
- 任务状态
- 禁止重复动作
- 下一步建议
这让系统可以在长会话中保持"方向稳定 + 成本可控"。
十、可观测性与验收
1)可观测性
所有会话进入 SQLite,包含:
sessions —— 会话级别元数据
turns —— 轮次级别信息
messages —— 完整消息链
turn_tools —— 工具调用记录
session_events —— 事件日志
并配有前端时间线,可回看"某一轮为什么调用了这个工具、失败点是什么、后续如何修复"。
2)验收
run_reverse_expert_agent.py 在执行期自动完成:
- IDB 备份
- before/after 结构体快照
- struct diff
- acceptance summary(md/json)
而且定义了明确失败条件:
- 无 tool call
- 无有效 mutation
- before/after 无差异
- 运行异常中断
这意味着:系统不是"跑完就算成功",而是"满足质量门槛才算成功"。
十一、关键设计解析
1. 为什么坚持"类型应用后必须重反编译验证"
很多自动化工具止步于"输出结构体定义",但 Agentic IDA Pro 强制要求验证闭环。这是因为:
- 模型可能猜错:LLM 根据偏移推断的字段语义,可能与实际不符。只有看到
ptr->version = 256 这样的代码,才能确认 offset 8 真的是版本号。
- 结构体定义可能不完整:如果漏掉了某个字段,后续访问会出现错位。重反编译能立即暴露这种问题。
- 可读性是硬指标:结构体恢复的目标是让代码更易读。如果应用类型后代码仍然满屏强制类型转换,说明恢复工作还没到位。
2. 为什么用"任务板"管理状态
长链任务中,LLM 容易"遗忘"之前做过什么。任务板的价值在于:
- 外化记忆:不依赖 LLM 的上下文记忆,而是把状态写在持久化的任务板里
- 可审计:每个任务的完成都有明确的证据引用
- 阻断提交:如果还有未完成的任务,
submit_output 会被拒绝,防止"半成品"输出
3. 为什么需要"知识区"
与任务板不同,知识区沉淀的是跨任务的长期认知。这使得多个子 Agent 可以协作分析:一个 Agent 发现了线索,写入知识区;另一个 Agent 在别处找到了答案,回来补充。
十二、与学术/行业思路的关系:借鉴与工程化
从研究脉络看,我们受到两类工作启发:
- ReAct:交错的推理与行动
- CodeAct:把可执行代码作为统一行动空间
但项目不是直接照搬论文,而是做了工程化改造:
- 将"动作空间"映射为可控工具集(含逆向专用工具)
- 用任务板和知识区约束长链路决策
- 用子 Agent 做角色隔离,降低上下文污染
- 用验收机制保证"可交付"的底线
简单说:论文解决"能不能做",工程系统解决"能不能长期稳定地做"。
十三、风险控制与局限
风险控制
| 风险 |
对策 |
| 模型幻觉 |
每步操作必须有工具证据,不能只凭"我觉得" |
| 破坏性操作 |
显式阻断删除结构体等高危 API |
| 失败不可追溯 |
所有操作写入 SQLite,支持轮级回放 |
| 伪成功 |
必须有实际变更(结构体 diff)才算完成 |
| 无限循环 |
最大迭代次数 + 子任务超时机制 |
当前局限
- 仅支持 IDA Pro 9.3
- 当前优化重点是结构体恢复,不代表覆盖所有逆向场景
- 字段命名质量取决于证据密度
路线图
- 增加结构体恢复质量的自动化评测
- 扩展更多逆向技能(字符串解密、协议字段恢复、语义级的代码分析)
- 更细粒度的变更审计(字段变更原因链)
- RAG 增强逆向workflow, 和 IDAPython 脚本开发
十四、如何快速上手
1)环境要求
- IDA Pro 9.3
- Python 3.10+
- OpenAI API Key(或其他兼容 OpenAI 格式的端点)
2)配置模型环境
1 2 3 | export OPENAI_API_KEY='your-api-key'
export OPENAI_BASE_URL='http://your-llm-endpoint/v1'
export OPENAI_MODEL='gpt-5.2'
|
3)启动 IDA Service(Windows)
.\src\scripts\run_windows_bridge.bat
4)启动 Agent(WSL/Linux)
1 2 3 4 | PYTHONPATH=src python src/scripts/run_reverse_expert_agent.py \
--ida-url http://127.0.0.1:5000 \
--request "分析关键函数并恢复结构体定义,给出证据链" \
--max-iterations 40
|
5)运行前检查清单
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 19小时前
被xxxxxxx_798269编辑
,原因: