分析工具:IDA
模型:Grok 4.5
前几天刷黑叉子看到一个很有意思的,它强调了「用 LLVM 能轻松实现那些用 LLVM 做不了/太麻烦的事情,所以自己有 IR/编译器的话很强呢」。开始没觉得什么,就继续躺床上刷视频,刷着刷着突然感觉悟了我得试试看。就起来鞭策 Grok 4.5 了,狠狠的抽了一天一夜终于做出来了想要的框架与流程跑通。
以前分析代码虚拟化真的是折磨。一开虚拟化,原来好好的函数变成一堆看不懂的跳转,人肉跟,跟到怀疑人生。新一代多态 VM 更狠,入口天天换,跟 VIP 跟到眼瞎。


抽完那一通,先说结论吧:
虚拟化并没有被一句 prompt 直接秒了。
但「人定方向 + AI 写流水线」这件事,已经能把以前按周按月抠 handle 的活,压到按天迭代。
真正被击穿的,是那种纯手工跟流、一个个手写 handle 语义、还把优化后的空壳汇编当「还原成功」的老路子。
中间把 Grok 额度都干到 80% 多了,速度上grok真的是太爽了:

我之前是分析过 VM 的,知道什么是 VmContext、什么是 handle,也知道这类东西大多是堆栈机那一套。
如果你完全没接触过,上来就让 AI 硬刚,大概率不行——它会一本正经输出一堆你也看不懂、其实还是错的东西。
有了这些前备以后再指挥 AI,就顺多了,基本能搞定:
我也参考了相关博主的方案,用 Rust 搭骨架,执行侧参考了 momo5502/sogen 那套思路。基础框架立住,才能往下做。
人话版名词对照(跟 AI 对齐用的):
这个过程其实谁都能练:找个只开了代码虚拟化、别的壳层尽量干净的 demo,从入口模拟执行到退出。
我觉得特别关键的一点:先让 AI 用 Python 写模拟跑通。
Python 改起来快,错了马上重来。你确认它真的懂你要的步骤了,再让它进 plan、搭 Rust / LLVM 那种重框架,就不容易跑偏。

以前能手工把 handle 跑通的人
以前得人手定义每个 handle 干什么;现在在你盯着方法论的前提下,可以让 AI 自动补表、补规则。优先让它搞清楚:
IR 和回编译这边我选了 LLVM——跟黑叉那帖一个方向。
但泼盆冷水:LLVM 很强,却也不是万能。虚拟机语义很脏的时候,它优化起来会「帮倒忙」;想完美还原业务逻辑,最终还是得自己的语义表 + 数据流。LLVM 更适合当你的 中转站和工程化出口。
搭出来的工程大概长这样(decode / 模拟 / lift / 签名,分模块):

demo 跑通只是热身。后面直接拿 一份应用层的、一份带完整代码虚拟化的内核样本(下面就叫「样本」)开干。
样本里那套东西很典型:分发入口多态、代码岛变形、一类 class0 的「先解码再写工作区」链……人肉跟,跟到怀疑人生。
别看 IDA 里函数方块大不大——那往往是假象。
更靠谱的是用模拟器批量扫:谁进虚拟机分发次数最多、扫过的混淆代码点最多、指令跑得最长,谁才是「最大」。
本轮样本里最大的一号桩,深跑能到大概:
注意:最大的是「虚拟机干活干得多」,不是「优化完 IR 还很长」。
外壳可以巨大;你要是语义没钉死,优化器一通删,最后只剩几个空 call,那不叫函数小,叫你还没还原到位。
人话就是一条传送带:
跑通之后,输出目录会变成这样——一堆函数名下面挂着 .ll / .s / .obj,看着就有点「量产」的感觉了:

CreateFile、ReadProcessMemory、EnumWindows……名字是恢复出来的语义壳;底下才是 lift 出来的中间文件。以前这种量,手工得抠到天荒地老。
AI 时代最容易发生的,不是做不出来,而是 做出一坨看起来很对、其实是自我感动的东西。
有一阵子 hybrid 优化后的汇编难看哭:
人一看:还原失败?返回写错了?
其实多半是这几件事叠在一起:
修完之后,IR 里是可以长出 正经条件分支 的。比如下面这种——有 icmp、有 br i1,后面还能直接 call MessageBoxA,已经有点「人话程序」的样子了:

再落到汇编,就是普通人更眼熟的 je 分支 + call MessageBoxA:

这张图的意义在于:不是虚拟化解不开,而是你中间表示写对了,后端自然会吐出正常的跳转和 API 调用。
为了让优化器「留住分支」,一度搞过:
更麻烦的是:很多「双沿」只是 历史里见过两个后继地址,不等于 CPU 的「零标志置位就跳」。
没真 flags 就硬造 jcc,等于自己骗自己。
现在定的规矩很土但管用:
诚实的代价是:优化后的文件又变短了。但短得真实,比长得假强。
如果你要的是 「这个函数在虚拟机里一步步干了啥」,那就别迷信 -O3。
优化器的工作是删「没用」的东西;你语义还没钉死,它删得越狠,你越像还原失败。
所以我们单独做了 full 模式:一步 handle 都不丢,每一步都留下痕迹。暂时还原不了的身体,就先用 unk_iat_func 这种 占位调用 顶着(意思是:这儿有个未知小函数体,参数带着 handler 地址和 VIP,不是 导入表里的真 API)。
于是就有两套答案:
一开始占位函数叫 vmp_step,别人一看以为是官方探针。
后来改成 unk_iat_func 这类名字:一眼就知道是 未知体 / 占位,别跟 CreateFile 那种真导入搞混。
这里最容易混的两件事:
handle 地址 不等于 导入函数。
你在 full 里看到的 unk_iat_func(0x1406…),说的是「虚拟机跳进了某块 handler」,不是「调用了 ZwXxx」。
样本是内核驱动的话,导入表里通常一堆:
静态扫一遍 call [导入表],能知道 这个驱动可能会用到谁。
但「可能会用」≠「这条执行路径真的调了」。
把导入表槽绑到空桩上,用模拟器跑最大那个桩,就能抓到 真实 call 顺序。本轮那条路径上,大致是一串很「初始化」的东西:
翻译成人话:它在 起线程、弄同步对象,不是在那两百多步 handle 里每一刀都捅系统调用。
中间大段仍然是虚拟机内部空转 / 变形——这部分继续用 handle 流描述就对了。
一份比较老实的输出可以长这样:
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。