1. 从"AI 会写代码"到"AI 会拆代码":逆向技能路由到底解决什么问题
大多数人现在用 AI Agent 的姿势,还停留在"帮我写个函数""帮我改个 bug""帮我解释这段报错"。这些场景有个共同点:输入是干净的、结构化的、有明确意图的代码或文本,AI 只需要顺着正向逻辑往下推就行。但真实世界里有一大类任务,输入是"别人编译好的东西"——一个二进制文件、一段混淆过的脚本、一个没有源码的库。这时候你让通用 Agent 直接上,它大概率会给你一堆看起来合理、实际上完全跑偏的猜测。
这就是 reverse-skill 这类项目想解决的核心矛盾:通用 AI Agent 的默认技能树里,逆向工程这一支几乎是空的。不是模型不够聪明,而是它没有被"路由"到正确的工具链和思维框架上。你问它"这个二进制在干什么",它会用自然语言给你编一个故事;你问它"这个函数用了什么加密",它会根据函数名猜一个算法。这些回答的置信度极低,但在没有技能路由的情况下,Agent 自己意识不到这一点。
所谓"技能路由包",本质上是一层意图识别 + 工具编排 + 知识注入的中间层。它做的事情可以拆成三步:第一,判断用户当前的任务属于逆向工程的哪个子领域(静态分析、动态调试、协议还原、固件提取、恶意样本行为分析等);第二,为该子领域加载对应的工具集和操作规范;第三,把逆向工程特有的"证据链思维"注入到 Agent 的推理过程中,让它每一步结论都必须有可验证的支撑。
我之所以觉得这个方向值得认真聊,是因为它代表了一种更普适的趋势:AI Agent 的能力边界,越来越不取决于基座模型,而取决于你给它挂载了什么技能包。逆向工程只是一个切口,它把"技能路由"这件事的难度和必要性都放大了——因为逆向任务的容错率极低,一个错误的假设会导致后面几十步全废。
这篇文章适合三类人看:一是正在做 AI Agent 应用、想理解技能路由怎么落地的人;二是对逆向工程有兴趣、想看看 AI 能帮上什么忙的人;三是纯粹好奇"给 AI 装技能包"这件事到底怎么实现的人。我会尽量把原理、结构、实操和踩坑都讲透,不堆术语。
2. 逆向任务的特殊性:为什么通用 Agent 直接上必然翻车
2.1 逆向工程和正向开发在思维方向上的根本差异
正向开发是"从意图到实现":我先想清楚要做什么,然后选语言、选框架、写代码。逆向工程是反过来的——"从实现反推意图"。你手里只有一个编译产物,没有注释、没有文档、没有变量名,甚至连函数边界都是模糊的。这个方向上的反转,导致逆向工程的每一步都建立在假设-验证的循环上,而不是"直接构造"。
通用 Agent 的训练数据里,正向代码占了绝大多数。它习惯了"给定需求生成代码"的模式,一旦进入逆向场景,它会不自觉地用正向思维去填补信息空白——也就是"编"。比如你给它一个 stripped 的二进制,问它某个函数的作用,它可能会根据调用关系编一个听起来很合理的功能描述,但它不会主动告诉你"这个结论的置信度只有 30%,需要进一步验证"。
reverse-skill 这类路由包要做的第一件事,就是把逆向工程的证据链规范强制注入到 Agent 的工作流里。具体来说,就是要求 Agent 在给出任何结论之前,必须先列出支撑该结论的证据(字符串引用、交叉引用、系统调用序列、内存访问模式等),并标注每条证据的可靠程度。这个规范听起来简单,但它直接改变了 Agent 的输出结构,从"给答案"变成"给推理过程 + 答案 + 置信度"。
2.2 逆向工具链的碎片化:一个任务要跨五六个工具
正向开发里,一个任务通常在一个 IDE 里就能完成。逆向工程完全不是这样。举个典型场景:你要分析一个未知的二进制样本,完整流程可能涉及——
- 用文件识别工具判断格式和架构
- 用反汇编器(如 objdump、radare2、Ghidra 的无头模式)做静态反汇编
- 用字符串提取工具找线索
- 用调试器(如 gdb)做动态跟踪
- 用网络抓包工具分析通信行为
- 用脚本(Python + pwntools 之类)做自动化验证
这五六个工具各有各的输入输出格式、各有各的调用方式、各有各的坑。通用 Agent 就算知道这些工具的存在,也很难在正确的时机调用正确的工具,更别说处理工具之间的数据传递了。
技能路由包在这里的价值就体现出来了:它把"什么情况下该用哪个工具、怎么用、输出怎么解析"这套知识固化下来,Agent 只需要做意图判断,具体的工具编排由路由层负责。这就像给一个刚入行的分析师配了一个熟悉所有工具的老手在旁边指导。
2.3 容错率极低:一步错,步步错
正向开发里,你写错一行代码,编译器会报错,测试会失败,反馈很快。逆向工程里,你如果一开始把架构判断错了(比如把 ARM 当成 x86),后面所有的反汇编结果都是垃圾,但工具不会告诉你"你架构选错了",它只会给你一堆看起来像指令的乱码。等你发现的时候,可能已经浪费了几个小时。
这种低容错率意味着,逆向技能路由包必须包含前置校验环节。在正式进入分析之前,先做一轮基础信息确认:文件格式、架构、位数、是否加壳、是否混淆、入口点特征等。这些信息决定了后续所有工具的参数配置。reverse-skill 这类项目通常会把这一轮校验做成一个独立的技能节点,强制 Agent 在开始任何深度分析前先跑一遍。
提示:很多新手用 AI 做逆向时,最大的问题不是工具不会用,而是跳过了前置校验直接问"这个二进制在干什么"。Agent 在没有基础信息的情况下给出的回答,基本等于随机猜测。
3. reverse-skill 的技能路由架构:从意图识别到工具编排的完整链路
3.1 技能注册表:把逆向知识拆成可路由的原子单元
reverse-skill 这类项目的核心数据结构,是一张技能注册表。每个技能是一个独立的原子单元,包含几个关键字段:技能名称、适用场景描述、所需工具列表、操作步骤模板、常见陷阱提示、输出格式规范。
我拿"静态字符串分析"这个技能举例,它的注册信息大概长这样:
| 字段 | 内容 |
|---|---|
| 技能名称 | static_string_analysis |
| 适用场景 | 二进制无源码、需要快速定位关键线索 |
| 所需工具 | strings、rabin2、自定义正则过滤器 |
| 操作步骤 | 提取全部字符串 → 按编码分类 → 过滤系统库噪声 → 标记可疑项 |
| 常见陷阱 | 编码混淆导致漏提、宽字符未处理、压缩段误读 |
| 输出格式 | 字符串列表 + 地址 + 可疑度评分 + 关联函数 |
这张表看起来朴素,但它是整个路由系统的基础。Agent 在接到任务后,先做意图分类,然后从注册表里查出对应的技能,按步骤模板执行。这样做的好处是行为可预测、可审计——你知道 Agent 在什么阶段会调用什么工具,出了问题也能定位到具体环节。
技能注册表的粒度设计是个关键决策点。粒度太粗(比如"静态分析"作为一个技能),Agent 在里面还是不知道先干什么后干什么;粒度太细(比如"提取 ASCII 字符串"和"提取宽字符字符串"分成两个技能),路由层会变得极其臃肿,意图识别的准确率反而下降。我的经验是,按"一个完整的小目标"来划分粒度——比如"定位关键字符串"是一个技能,"分析字符串引用关系"是另一个技能,每个技能对应一个可以独立验证的输出。
3.2 意图识别层:怎么判断用户到底想干什么
意图识别是路由的第一道关卡,也是最容易出错的地方。用户说"帮我看看这个文件",这句话本身不包含任何意图信息,Agent 必须结合上下文(有没有上传文件、文件类型是什么、之前的对话历史)来推断。
reverse-skill 通常采用多信号融合的策略来做意图识别,而不是单纯依赖语言模型分类。具体来说,它会综合以下几类信号:
- 文件类型信号:上传的是 PE、ELF、Mach-O、APK、固件镜像还是脚本?不同文件类型直接对应不同的技能子集。
- 关键词信号:用户描述里有没有出现"加密""通信""崩溃""提取""对比"等词?这些词各自指向不同的分析方向。
- 历史上下文信号:如果之前已经做过静态分析,现在用户说"再看看",那大概率是要进入动态分析阶段。
- 显式指令信号:用户直接说"用调试器跟一下",那就跳过意图推断,直接路由到动态调试技能。
这四类信号加权融合之后,得出一个意图分布,取置信度最高的作为路由目标。如果最高置信度低于某个阈值,路由层会主动向用户提问澄清,而不是硬猜。这个"主动澄清"的机制非常重要,它避免了 Agent 在意图不明的情况下乱调工具、浪费资源。
3.3 工具编排层:让多个工具像流水线一样协同
意图确定之后,就进入工具编排阶段。这里要解决的核心问题是:多个工具之间怎么传递数据、怎么处理中间结果、怎么在某个工具失败时降级。
我拿一个实际的分析流程来说明。假设用户上传了一个未知的 ELF 文件,意图是"搞清楚它的网络行为"。路由层会编排这样一条流水线:
- 文件识别技能:调用 file 命令和 readelf,确认架构、位数、动态链接情况。
- 字符串提取技能:调用 strings 和 rabin2,提取所有可读字符串,过滤出疑似 URL、IP、端口、协议关键词。
- 符号与导入表分析技能:调用 nm 和 objdump,列出动态导入的函数,重点标记网络相关调用(socket、connect、send、recv 等)。
- 交叉引用分析技能:在反汇编结果里,找到网络相关函数的调用点,回溯调用链。
- 行为推断技能:综合以上证据,给出网络行为的描述和置信度。
这条流水线里,每一步的输出都是下一步的输入,格式必须对齐。比如字符串提取技能输出的可疑 URL 列表,要能被交叉引用分析技能直接消费,去反汇编里搜索这些字符串的引用地址。如果格式不对齐,中间就需要一个转换层,这会增加出错概率。
reverse-skill 的做法是为每个技能定义标准化的输入输出 Schema,技能之间通过 Schema 对接,而不是通过自由文本。这个设计决策看起来工程化,但它直接决定了整个系统能不能稳定运行。我见过太多项目在 Demo 阶段用自然语言传递中间结果,一到复杂场景就崩了。
3.4 证据链管理:让每一步结论都可追溯
这是我认为 reverse-skill 最有价值的部分。逆向工程里,一个结论如果没有证据支撑,它就是猜测。技能路由包在编排工具的同时,会自动收集每一步产生的证据,并建立证据与结论之间的关联。
具体实现上,通常是一个证据池(evidence pool)加上一个结论图(conclusion graph)。每个工具调用产生的原始输出,经过解析后作为证据存入证据池,带上来源标记(哪个工具、哪个文件、哪个地址)。Agent 在生成结论时,必须从证据池里引用至少一条证据,否则结论会被标记为"无支撑"。
这个机制的实际效果是:当你问 Agent"这个函数是不是在做加密",它不会直接说"是的",而是会说"根据地址 0x4012a0 处的常量表(证据 1)和该函数对 0x401500 的调用(证据 2),推断该函数可能实现了 AES 密钥扩展,置信度 75%,建议用动态调试验证"。这种输出对逆向工程师来说才是真正有用的。
4. 把 reverse-skill 跑起来:环境搭建与第一个分析任务
4.1 基础环境准备:工具链的版本坑比想象中多
reverse-skill 本身是一个路由层,它依赖底层的逆向工具链。所以第一步是把工具装齐。这里我要重点说的是版本兼容性问题,这是实际部署时最容易翻车的地方。
以反汇编工具为例,不同版本对同一条指令的解析可能不同,尤其是在处理边界情况(比如非法指令、数据段误入代码段)时。如果你的路由包在技能注册表里写死了某个工具的调用参数,但实际环境里装的是另一个版本,参数可能不兼容,导致工具报错或者输出格式变化。
我的建议是:在技能注册表里为每个工具声明版本范围,并在启动时做一次环境自检。自检内容包括:工具是否存在、版本是否在支持范围内、基本功能是否正常(比如跑一个已知样本,看输出是否符合预期)。这个自检环节花不了几分钟,但能避免后面大量的调试时间。
环境准备清单大致如下:
- 文件识别类:file、readelf、objdump、rabin2
- 反汇编类:radare2 或 Ghidra 无头模式、capstone(Python 库)
- 调试类:gdb(配 pwndbg 或 gef 插件)
- 脚本类:Python 3.8+、pwntools、capstone、lief
- 网络分析类:tcpdump、scapy(Python 库)
- 路由框架本身:reverse-skill 主程序及其依赖
注意:不要在一个环境里混装多个版本的反汇编工具。我见过有人同时装了系统包管理器的 radare2 和源码编译的 radare2,结果路由层调用时路径混乱,排查了半天才发现是版本冲突。
4.2 技能注册表的初始化与自定义
装好工具之后,下一步是初始化技能注册表。reverse-skill 通常会带一份默认注册表,覆盖常见的逆向场景。但默认注册表不一定适合你的具体需求,所以需要做一轮自定义。
自定义的核心是根据你的实际分析对象调整技能优先级和参数。比如你主要分析的是嵌入式固件,那默认注册表里针对 PE 文件的技能就可以降权,针对 ARM/MIPS 架构的技能要加权。再比如你经常遇到加壳样本,那就需要把"壳识别"和"脱壳"相关的技能提前。
注册表的配置文件一般是 YAML 或 JSON 格式,结构清晰。我拿一个简化版的配置片段举例:
skills: - name: file_identification priority: 100 tools: - cmd: "file {target}" parser: "file_output_parser" - cmd: "readelf -h {target}" parser: "readelf_header_parser" triggers: - "always" # 每个任务开始时必跑 output_schema: "file_info" - name: static_string_analysis priority: 80 tools: - cmd: "strings -n 6 {target}" parser: "strings_parser" - cmd: "rabin2 -z {target}" parser: "rabin2_strings_parser" triggers: - "file_type in ['ELF', 'PE', 'Mach-O']" output_schema: "string_list" depends_on: - "file_identification"这个配置里,priority决定技能的执行顺序,triggers决定技能在什么条件下被激活,depends_on声明依赖关系。路由层会按照依赖关系和优先级,自动编排出一条执行链。
自定义的时候有个经验:先把默认注册表跑一遍,看哪些技能被触发了、哪些没触发、哪些触发了但输出没用。然后针对性地调整。不要一上来就大改,否则出了问题很难定位是配置问题还是工具问题。
4.3 跑通第一个任务:从一个简单样本开始
环境准备好之后,不要直接上复杂样本。找一个你自己写的、逻辑简单的二进制做第一次测试。比如写一个 C 程序,里面有一个明显的字符串、一个简单的函数调用、一个系统调用,编译成 ELF,然后让 reverse-skill 分析它。
这个测试的目的是验证整条链路是否通畅:意图识别对不对、技能有没有按预期触发、工具调用有没有报错、证据链有没有建立、最终结论有没有引用证据。如果这个简单样本都跑不通,那复杂样本更不用想。
我在第一次跑的时候遇到的问题是:字符串提取技能正常执行了,但交叉引用分析技能没有触发。排查后发现是触发条件写的是file_type == 'ELF',但文件识别技能输出的file_type字段值是'ELF 64-bit',字符串不完全匹配。这种问题很隐蔽,因为工具本身没报错,只是技能没触发,最终结论里少了一块证据。
修复方式是把触发条件改成模糊匹配,或者统一文件识别技能的输出格式,只保留基础类型。我倾向于后者,因为标准化输出格式比在每个触发条件里做兼容更可靠。
4.4 验证分析结果:怎么判断 Agent 给的结论靠不靠谱
跑通之后,关键问题是:怎么判断 Agent 的输出质量?逆向工程的结论不像正向代码那样可以跑测试用例验证,很多时候你只能靠人工判断。但有几个可操作的检查点:
- 证据完整性:结论里引用的证据,能不能在原始工具输出里找到对应?如果找不到,说明证据链断了。
- 逻辑一致性:多个结论之间有没有矛盾?比如一个地方说函数 A 调用了函数 B,另一个地方说函数 A 没有调用任何函数,这就是矛盾。
- 置信度合理性:Agent 标注的置信度是否符合直觉?如果它对一个明显模糊的结论标了 95% 置信度,那说明置信度评估机制有问题。
- 可复现性:同样的输入跑两次,结论是否一致?如果差异很大,说明路由过程有随机性,需要排查。
我一般会拿一个自己已经手工分析过的样本做基准,让 Agent 跑一遍,然后对比它的结论和我的手工结论。差异部分就是需要重点检查的地方——可能是 Agent 错了,也可能是我手工分析时漏了什么。这种对比是提升路由包质量最有效的方式。
5. 实战中的坑:技能路由在真实逆向场景下的失效模式
5.1 意图误判:用户说的和实际要的不是一回事
意图识别再准,也有翻车的时候。我遇到过一个典型案例:用户上传了一个二进制,说"帮我看看这个文件有没有问题"。路由层把"有没有问题"识别为"安全分析"意图,触发了恶意行为检测技能链。但实际上,用户是开发者,他说的"有没有问题"是指"这个编译产物有没有 bug",不是"是不是恶意软件"。
这个误判导致 Agent 跑了一堆安全分析工具,输出了一堆"未发现恶意行为"的结论,但用户真正想要的崩溃原因分析完全没做。用户看到输出后一脸懵,Agent 也觉得自己完成了任务。
这个坑的本质是:自然语言里的模糊表达,在缺乏领域上下文时无法准确路由。修复方式是在意图识别层增加一个"领域判断"环节——先判断用户是开发者视角还是安全分析视角,再在对应领域内做意图细分。领域判断的信号可以来自用户的历史行为、上传文件的特征(比如有没有调试符号)、以及对话中的其他线索。
5.2 工具输出解析失败:格式变化导致的静默错误
逆向工具的输出格式,远没有你想象的那么稳定。同一个工具,换个版本、换个参数、换个输入文件,输出格式可能就变了。如果你的解析器写死了格式,遇到变化时要么报错,要么更糟——静默解析出错误的结果。
我踩过的一个坑是:某个反汇编工具在输出函数列表时,如果函数名包含特殊字符,会用引号包裹,否则不用。我的解析器没有处理引号,导致带特殊字符的函数名被截断。这个错误不会报错,只是函数名少了一截,后续的交叉引用分析就找不到对应的函数了。
修复方式有两层:第一层是解析器要尽量宽容,用正则而不是字符串分割来提取字段;第二层是增加输出校验,比如解析出的函数数量是否合理、函数名是否符合命名规范,不符合就报警。
提示:在技能注册表里,为每个工具的解析器配一个"输出校验规则",能在很大程度上避免静默错误。校验规则不需要很复杂,比如"解析出的条目数不能为 0""关键字段不能为空"就够了。
5.3 证据链断裂:中间步骤失败导致后续结论无支撑
逆向分析的流水线里,如果中间某个技能失败了,后面的技能可能还在跑,但它们的结论没有证据支撑。这时候 Agent 如果不知道前面失败了,会给出看起来完整、实际上不可靠的结论。
比如字符串提取技能因为编码问题漏掉了关键字符串,交叉引用分析技能就找不到对应的引用,行为推断技能就会得出"该程序没有网络通信"的错误结论。整个链条上没有任何一个环节报错,但最终结论是错的。
解决这个问题的关键是在证据链管理里加入"完整性检查"。每个技能执行完后,检查它的输出是否满足预期(比如字符串提取技能至少应该提取到多少条字符串),不满足就标记该技能为"降级执行",并在最终结论里明确说明"由于某环节降级,以下结论置信度降低"。
5.4 性能瓶颈:大文件分析时的资源耗尽
逆向分析的对象可能很大——几百 MB 的固件、几十 MB 的二进制。如果路由层不加控制地把整个文件喂给所有工具,内存和 CPU 很快就会耗尽。
我遇到过一次:分析一个 200MB 的固件镜像,字符串提取技能直接把整个文件读进内存,然后 OOM 了。修复方式是在技能注册表里为每个技能声明资源限制,比如最大处理文件大小、最大内存占用、超时时间。超过限制时,技能要么分块处理,要么跳过并标记。
分块处理本身也有坑:如果字符串跨块边界,分块提取会漏掉。所以分块策略要保证重叠区域,或者用流式处理而不是分块。这些细节在技能实现时就要考虑清楚,不能等到出问题再补。
6. 从 reverse-skill 延伸:技能路由包的通用设计原则
6.1 技能粒度:一个技能只做一件可验证的事
这是我在多个技能路由项目里总结出的最重要的一条原则。技能粒度如果太粗,Agent 在里面还是不知道该干什么;如果太细,路由层会变得极其复杂。判断标准是:这个技能的输出能不能被独立验证。
"提取字符串"可以被验证——你数一数提取了多少条,看看有没有漏。"分析程序行为"不能被独立验证——你怎么知道分析得对不对?所以前者适合作为一个技能,后者应该拆成多个可验证的子技能。
按这个原则拆下来,一个完整的逆向分析流程可能会拆成十几个技能。听起来多,但每个技能都很简单、很稳定,组合起来反而比一个大而全的技能更可靠。
6.2 失败降级:技能不可用时怎么保持流程继续
不是所有技能在任何环境下都能用。比如动态调试技能需要调试器支持,如果目标环境不允许调试(比如某些受限环境),这个技能就不可用。这时候路由层不能直接崩溃,而应该降级到替代方案——比如用静态模拟代替动态调试,或者跳过该环节并明确标注。
降级策略要在技能注册表里预先声明。每个技能可以配一个fallback字段,指定不可用时的替代技能或处理方式。这样路由层在编排时就能自动处理,不需要人工干预。
6.3 可观测性:让路由过程透明可查
技能路由包最怕的就是"黑盒"——Agent 给了一个结论,但你不知道它是怎么得出的。所以可观测性是必须的:每一步调用了什么工具、输入是什么、输出是什么、耗时多少、有没有报错,都要有日志记录。
这些日志不仅是排查问题的依据,也是优化路由策略的数据来源。比如你发现某个技能经常超时,那可能需要调整它的资源限制或者优化实现;某个技能经常被触发但输出没用,那可能需要调整它的触发条件。
我一般会把日志分成两层:一层是执行日志,记录工具调用的原始信息;另一层是决策日志,记录路由层为什么选择这个技能、为什么跳过那个技能。决策日志对调试意图识别特别有用。
6.4 持续迭代:用真实任务反哺技能注册表
技能注册表不是一次写完就固定的。每次跑真实任务,都会发现新的场景、新的坑、新的优化点。这些都应该反哺到注册表里。
我的做法是维护一个"待办技能列表":每次遇到注册表覆盖不到的场景,就记下来,攒到一定数量后集中补充。补充的时候不是直接写技能,而是先手工跑一遍流程,确认步骤稳定后再固化成技能。没有经过手工验证的流程,不要直接写成技能,否则会把错误的步骤固化下来,后面很难发现。
7. 我在这类项目上踩过的几个真实教训
第一个教训是关于过度依赖模型判断。我一开始觉得,意图识别交给语言模型就行了,它那么聪明肯定能判断对。实际跑下来发现,模型在没有领域约束的情况下,判断准确率远低于预期。后来改成"模型初判 + 规则校验 + 低置信度时主动澄清"的组合策略,准确率才上来。模型不是不能用,而是不能单独用。
第二个教训是关于技能之间的耦合。我早期设计的技能之间共享了很多中间状态,看起来省事,但一个技能改了,其他技能可能就受影响。后来改成每个技能只通过标准 Schema 通信,不共享内部状态,虽然多写了一些转换代码,但稳定性提升明显。技能之间要像微服务一样解耦,这个思路在路由包里同样适用。
第三个教训是关于测试样本的多样性。我一开始只用自己写的几个样本测试,跑得挺好。后来拿真实世界的样本一测,各种边界情况全出来了——加壳的、混淆的、跨架构的、损坏的。所以测试样本一定要多样,最好覆盖你实际会遇到的各类文件。我现在的测试集里有几十个样本,每次改完路由逻辑都跑一遍回归。
第四个教训是关于文档和注释。技能注册表里的每个字段、每个触发条件、每个参数,都要写清楚为什么这么设。我吃过亏——过了两个月回头看自己写的配置,完全不记得某个参数为什么是那个值,只能重新试。现在我强制自己给每个非显然的配置项写一行注释,说明设置理由和调整依据。
这类项目的价值不在于它现在有多完善,而在于它提供了一个可扩展的框架。你每补充一个技能、每修复一个坑,整个系统的能力就增强一分。这种渐进式的积累,比追求一步到位更现实。