ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI辅助逆向工程实战:没有源代码也能拆解闭源软件

AI辅助逆向工程实战:没有源代码也能拆解闭源软件

上周维护一个遗留系统,供应商只递过来一个安装包,源码一张纸都没给。换做以前,这种“只有二进制”的活又得进入人肉逆向的持久战。这次我换了个思路,先用一个代码托管平台上已经攒到3.7万Star的开源项目,让AI把软件整体自动拆解了一遍。“没有源代码,AI也能拆解软件”这句话放在两年前我是当噱头听的,可真的跑完一个样本之后,我承认它值得放进常用工具箱。

如果你也在维护闭源组件、做二进制安全分析、或者接手过交接文档约等于没有的老系统,这篇文章可以帮你少走不少弯路。我会从它到底解决了什么痛点开始,再讲完整实操、遇到的问题以及团队里应该怎么落地。

1. 没有源代码时,传统反编译到底卡在哪一步

1.1 反编译输出像“没有门牌号的街区地图”

先说背景。没有源代码的情况下要理解一个程序,靠的是把机器码还原成人能读的形式。传统反编译工具干的事情就一件:把二进制里的指令流恢复成接近C语言的伪代码。这个方向本身没错,但等你真打开反编译结果就会发现,可读性远没有想象中好。

原因有三。

第一,函数名没了。二进制里大量函数会被自动编号,比如FUN_00402050,光看名字你根本不知道它是处理登录的、解析配置的,还是转发数据包的。第二,局部变量名全部退化成local_8、local_10这类名字,反编译器是根据栈偏移猜的,不表示任何业务含义。第三,类型信息大量丢失,一个结构体可能被拆成一堆连续的字节拷贝,人也只能靠长年累月经验去猜“这其实就是对某个字段赋值”。

我常用一个比喻:传统反编译给了一张很精确的街区地图,道路、拐弯都在,但门牌号、店铺名全没了。你走在大街上能知道这里是商业区,却说不清哪家店卖什么。所以在大型闭源程序面前,纯粹靠人肉阅读反编译伪代码,效率极低。

1.2 动态分析也不是银弹

有人会说,“那干脆动态分析,把程序跑起来,打断点、看看内存和调用栈。”确实,动态分析是逆向工作流里必不可少的一环,但它同样有前提:你得大概知道该在哪个函数下断点。面对一个几十MB的二进制,入口倒是能找到,关键业务函数可能藏在几万个函数里,没有静态线索,动态分析就像在整栋楼里找一盏亮着的灯,只能逐层开。

我这次接手的模块就是这样。它的核心流程被封在一个独立组件里,日志少得可怜,调试器一挂上去甚至能感觉到明显的时间戳校验。老办法是先全量扫描字符串、筛可疑引用、再顺着交叉引用去反编译里人肉比对。整套流程下来,运气好可能要一两天,运气不好一周也说不准。

AI辅助拆解的出现,恰好补的是这段“找路”时间。它不替你做最终的逻辑判断,但能把原本要靠经验磨出来的第一步——快速理解一个函数的大致意图——压缩到几分钟。

2. 这个3.7万Star的项目,拆的是“语义”而不是“语法”

2.1 从使用角度倒推它的工作流

当时选择这个项目,是因为它和普通反编译器不太一样。普通反编译器核心产出是“长得像C的伪代码”,而它的产出更像是“带注释和解释的拆解报告”。

我没有细挖每一行底层实现,但从使用角度倒推,它的工作流大概是这样的:先做指令解析和反汇编,把二进制转成中间表示,然后做反编译层面的结构恢复,得到一份伪代码骨架,最后把这些伪代码按函数切片交给大语言模型,让它补上语义理解,比如“这个循环像在做累加校验”“这段位运算看着像某种编码算法”。这也是它相对聪明的部分:它看的不是字符串字面量,而是控制流和数据流之间的关系。

实际使用时,你既可以在命令行里让它生成整个程序的结构化摘要,也可以针对某个具体函数反复追问。例如我把一个网络模块的入口函数丢进去之后,它能直接告诉我:“根据反编译产物,该函数大概率负责校验握手包中的时间戳字段,若校验失败返回错误码0x1E。”这种输出已经不是伪代码层面的描述,而是业务语义层面的猜测。

一个关键认知:它做的是“语义拆解”,不是“语法重建”。普通反编译器核心是“能不能还原成C”,它核心是“这段逻辑到底在干什么”,哪怕结果不完全精确,也足够给逆向工程师一个可靠起点。

2.2 和传统反编译器是补位关系,不是替代关系

很多人第一个问题是:“那以后是不是不用学传统反编译器了?”我的答案很明确:不是。

维度传统反编译器AI拆解器
核心产出结构可疑、但语法完整的伪代码带自然语言注释、意图推测的拆解摘要
强项精确还原控制流、数据流快速给出函数目的、模块分工、可疑点提示
确定性高,同样的输入产出基本稳定存在一定概率性,需人工验证
使用场景深挖漏洞细节、精确修改逻辑初次摸底、模块索引、写分析报告草稿
对经验要求高,需要大量人肉阅读经验相对低,但验证结论仍然需要经验

在实操里,最顺手的组合是:用AI拆解器生成全貌和优先级,再用传统反编译器对重点函数做精确确认。AI负责指路,人负责踩实。

2.3 适用边界:它不是万能拆解器

它也有限制。第一,加壳、混淆严重的样本要先处理干净再喂给AI,否则一串经过虚拟化的字节进去,出来的解释只能是“无法理解该段逻辑”。第二,单个函数过大的时候,超长上下文会把大模型的注意力稀释,回答质量明显下降。第三,一些特殊指令集、固件类样本支持度也不如x86/ARM那么成熟。

我整理了一张边界清单,团队里新人照着判断就行:

  • 普通x86/x64、ARM的ELF/PE样本:可用,效果好。
  • 加壳样本:先脱壳、还原真实入口点后再分析。
  • 市盈率类密码逻辑、自带混淆框架样本:需要配合动态调试,不能只信AI结果。
  • 超过几千行伪代码的巨型函数:建议先切分或只针对片段提问。

3. 完整实操:把闭源网络模块拆成可读逻辑

3.1 运行环境和初始扫描

先说环境。这个项目本身对GPU没有硬性要求,但由于它主要是调大模型做推断,有显卡体验会好很多。我这边测试机是普通工作站、32GB内存,无独立显卡,照样能跑,只是处理大二进制时速度偏慢。依赖安装很常规,一条命令拉完必要组件,项目自带命令行入口,不需要额外造轮子。

进入正题。我分析的是一个自研的老网络模块(授权范围内的遗留组件),主要目的是搞清楚三个事:握手流程、超时判断、错误码含义。初始扫描我习惯先用系统自带的常用命令做一次摸底,虽然和AI无关,但能帮后续提问更准确。

file legacy_network.so readelf -h legacy_network.so strings -n 8 legacy_network.so | head -100

file确认架构和格式,readelf看入口和段分布,strings则能快速筛出日志、URL、错误码这类硬编码信息。这个步骤不能省。AI拆解器对“这个二进制大概是什么东西”没有先验知识,你把样本格式、入口地址、架构信息手工喂给它,效果会立刻不一样。

3.2 用命令行生成结构化函数摘要

摸底完之后,直接用项目CLI做全量拆解。命令大致长这样,具体参数不同项目会略有差异,但结构就是“输入文件 + 架构声明 + 输出目录 + 摘要深度”。

ai-decomp analyze -f legacy_network.so --arch x86_64 --output ./report --summary-depth medium

生成时间取决于二进制大小。这个模块比较小,几十秒就出了结果。输出的不是一张类似传统反编译器的源码大列表,而是一份按函数分组的摘要,说明每个函数“疑似功能”、调用关系、涉及的关键字符串和常量。

比如其中一个函数摘要写的是:

  • 地址:0x140014F20
  • 疑似功能:解析握手响应中的时间戳
  • 证据:函数内多次调用memcmp,比较常量位于某个偏移,邻接函数都在处理协议头
  • 置信度:中

这段摘要看起来不复杂,但已经帮我把范围缩小到了三个关键函数。搁传统流程里,我先得在反编译器里把函数列表刷一遍,再对着交叉引用猜哪个是核心,很耗时间。

3.3 对指定函数做对话式深挖

有了候选函数之后,下一步就是深挖。这里我用的是交互式对话模式,可以直接把函数地址丢给它。

请分析地址 0x140014F20 这个函数。重点是: 1. 它在什么条件下返回成功? 2. 时间戳的差值阈值是多少? 3. 失败时可能设置哪些错误码?

它返回的推断比普通伪代码更适合快速理解,并给出了类似这样的一段重命名伪代码:

int ParseHandshakeTimestamp(conn, packet) { ts = ReadUint32(packet + 0x0C); now = GetSystemTime(); // 疑似校验窗口:差值超过 0x3A98(约15秒)即视为失效 if ((now - ts) > 0x3A98u) { SetErrorCode(conn, 0x1E); return 0; } return 1; }

注意,这些变量名和注释是AI根据反编译产物推测出来的,不会百分之百准确。但我拿着这个结论回到调试器验证,在0x140014F20下断点、随便构造几个超时数据包,很快确认了0x3A98就是阈值。原本可能要半天的定位,压缩到了两小时以内。

3.4 把AI重命名结果反向导回传统反编译器

这里有一个非常实用的技巧:AI拆解完函数语义后,会生成一组“逻辑名到地址”的映射。我通常会把这组映射导出,批量应用在传统反编译器的数据库里。

操作很简单:生成重命名脚本后,在反编译工具里执行即可。比如:

ai-decomp export-names --output ./names.json # 然后将 names.json 导入反编译器,逐个函数执行改名

这样一来,传统反编译器里满屏的FUN_xxx就变成了HandleTimeCheck、ParseAuthPacket、BuildErrorResponse这种可读名字。之后再人肉深挖任何一处细节,体验完全不一样。这个“AI负责批量改名、人负责细节审阅”的组合,是我目前最喜欢的落地形态。

4. 被吹大的效果和那些坑

4.1 幻觉:它可能非常自信地给错结论

最大的坑,是AI大模型天生的幻觉问题。拆解器输出本质上是大模型对伪代码的推测,它不会在不确定的时候说“我不知道”,反而倾向于给一个看起来很合理的解释。

我遇到过一个典型例子:某个函数里有一个大常数表,加上密集的移位、异或操作。AI给出的结论是“疑似AES密钥扩展”。我差点信了,但后面验证时发现,这个函数对输入长度变化的响应完全不符合分组加密的特征。查了更多反编译信息才发现,那是一个CRC-32查表法实现,只是表结构和常见多项式对不上,导致AI成了一个“看起来像加密”的错觉。

从那以后,我和团队定了一条规矩:任何AI结论,必须能在反编译器或调试器里找到对应的证据链。AI说“这是哈希比较”,你就必须能找到那个哈希函数入口、找到被比较的缓冲区、找到判断分支。找不到,宁可先标记为“未知”,也不能写进报告当作事实。

4.2 上下文一长,AI就开始“东拉西扯”

第二个坑,是超长函数导致的上下文溢出。大模型的输入窗口有限,当一个函数反编译出几千行,或者你把整个模块的报告一次性丢进对话,AI后期会开始忽略前面的某些关键信息。最明显症状就是:同一个函数,问第一次和问第五次给出的结论不一致,甚至互相矛盾。

解决办法是拆。我通常把待分析模块按调用关系分成三层:入口层、业务逻辑层、工具函数层。先用工具生成调用图,让AI只负责“叶子节点”级别的小函数解释,然后再用这些解释去反问上层函数。一句话总结:永远不要让AI一口气读完整栋楼的图纸,只让它先看清每个房间的门牌。

4.3 加壳和混淆样本要先处理干净

还有一次,我把一个疑似加壳的样本直接丢给拆解器,结果所有函数摘要都像是从一团乱码里随便猜的,置信度全部是低。原因不是模型不行,而是壳代码没有还原。

加壳样本的入口是壳的导出表,真正的原程序根本没被映射出来。在这种状态下,AI能分析的对象只有壳自身的极少代码,自然得不出业务结论。所以只要发现函数列表明显偏短、字符串全是压缩后的乱码、或者反汇编里大量花指令,就先去脱壳、修正入口点,再回来跑AI拆解。脱壳环节没有捷径,该用的工具还是得用。

4.4 资源开销和自动化超时问题

如果你打算把AI拆解接入CI或批量分析流水线,资源限制同样得提前考虑。没有GPU的机器上,一个5MB的二进制全量分析可能要跑半小时起步,而且大模型推断进程很吃内存。另外,很多AI服务有token数量限制,整包分析容易在中间环节断掉。

我的建议是分两层:小的、重点的二进制用交互式分析,享受完整质量;大的、批量任务只做函数摘要生成,不做高质量语义推断。同时给定超时时间,超时的函数自动降级为普通反编译输出,避免整个任务卡死。

5. 团队落地时,该把它放在哪个环节

5.1 定位是“草稿机+索引器”,不是最终裁判

我们团队现在把这种AI拆解工具看作“草稿机”和“索引器”。草稿机,指它能把一段混沌的汇编逻辑快速翻译成有业务含义的草稿;索引器,指它能根据函数之间的调用关系快速标注“重点看这里”“这个可疑”。但最终结论,必须由人工结合动态调试和官方文档确认。

这样定位有个明显好处:降低新人的起步门槛。原来一个新人看二进制反编译结果,需要大量经验才能判断“这函数重要,这函数只是工具函数”。现在AI先给了一版索引,新人能直接切入重点,学到的东西也更结构化。

5.2 建立“证据链”验证闭环

团队内,我要求所有AI辅助结论至少要附带两条证据:

  1. 具体地址或偏移。
  2. 可验证的控制流行为,比如输入范围、分支条件、错误码值。

只有结论没有证据的输出,一律退回。举个例子,如果AI说“该函数校验用户权限”,那验证闭环就是:找到权限比较的常量和变量 -> 找到失败分支的跳转条件 -> 用测试数据在调试器里确认不同权限走不同分支。这三步走完,才算真正接受AI给出的解释。

5.3 成本账:人力节约远大于算力消耗

算产出时不能只看推理时长,要算人力成本。以前逆向一个中等复杂度闭源模块,经验丰富的人也要两到三天;现在AI先把结构梳理好,人工复核聚焦在关键函数,整体周期压缩到半天到一天。而且因为入门门槛降低,普通软件工程师也能参与到早期逻辑梳理中,团队不再把所有压力压在一两个逆向元老身上。

算力成本方面,一个10MB以内的二进制,全量拆解消耗的token费用大致相当于几次常规自动化测试的算力费用,相比节省的人力,投入产出比相当可观。真正贵的是“错误结论导致返工”的隐性成本,所以前面说的证据链机制不能省。

6. 几个提高AI拆解质量的小习惯

6.1 把“这个函数做什么”改成“在什么条件下它返回这个值”

提问方式直接影响回答质量。泛泛问“这个函数做什么”,AI给出的往往是表面描述。改成“从地址0x140014F20开始的反编译片段,在什么输入条件下会返回-1?”,它会直接聚焦到控制流边界,回答更具体、更难幻觉。

一个对比:

  • 较差的问法:请分析这个函数的逻辑。
  • 更好的问法:请分析这个函数的输入、输出、分支条件,并指出最可能被外部影响的数据路径。

6.2 先限定范围,再让它总结

不要一次性问“整个程序的架构是什么”。正确的方式是:先给它一个函数的签名,再让它解释核心逻辑;或者先指明某个结构体字段的用途,再问整个协议是如何组合这些字段的。拆解器就像组装的螺丝刀,一次拆一个零件比整机盲拆稳得多。

6.3 让它给出多个假设,而不是一个结论

我在关键函数上会直接问:“给出至少三种可能解释,并分别说明支持理由和反驳理由。”这样能逼它暴露不确定度,而不是硬挑一个顺眼的答案。等它列出多个假设后,我再按证据逐条排除,最终留下的往往就是正确解释。

6.4 把中间产物保存下来,减少重复提问

AI拆解产生的大量中间结果——重命名映射、函数摘要、置信度——我会全部导出到本地。后续任何一次人工复核,都不需要重新调用模型。保存中间产物的另一个好处是,当你发现AI判断错误时,可以精准定位“是哪一份反编译输入导致它判断错”,而不是推翻整篇报告重来。

我现在养成的习惯是:先让AI读一遍,自己再读一遍,双方结论一致才动手,有分歧的时候反而最值得往下挖——因为分歧点通常就是逻辑藏得最深的那个角落。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进