论坛首页 逆向工程技术区 阅读主题

[原创]刺杀vmp3.9.4混淆

172 浏览 21 回复
#1 楼主 2026-06-01 21:08:47
实际上我想研究的工作是借助符号执行来完成vmp虚拟机里各 handler 的语义识别,不使用以往传统上的指令特征识别方法(而且现在的vmp似乎实现了单个 handler 里集成多个虚拟机操作原语的特性,这也导致根据特征识别到的 handler 的语义是不全面的),但是在分析 trace 中,还是会因为看到一大片被混淆后的指令数据而感到十分头大,因此一步步来,先把混淆解了再说。这些便是我写下此篇文章的动机,希望对大家能有所帮助。然后我依旧用到了自写的 x64dbg 插件 supertrace 和 supertrace-pybind 来生成信息更全面的 trace 数据以进行分析工作。分析观察被 vmprotect 3.9.4 保护的样本的trace,我主要发现了以下混淆手段:(1) 垃圾代码垃圾代码指的是前面一些指令执行所写入的寄存器或内存数据被后续指令的写入所覆盖,而这些数据直到下一次被覆盖前都没有任何的引用。这个就不必多说了,最稳定也是最经典的一种混淆手段。(2) 代码空间局部性混乱(或者也可以叫代码乱序)指的是把本来好好的一块基本块硬生生地分割成两份基本块,并用直接控制流转移指令(如 jmp imm 或 call imm 指令)将其按顺序连接起来。这样达到的效果就是你在调试器里会看到rip寄存器左右反复横跳,上上下下左右左右BABA。 vmp里大量运用了此手段,并且还会搭配下面所说的永真永假型不透明谓词来强化混淆效果。 为了打字方便,我在下文都称其为 "代码乱序"(3) 不透明谓词(永真永假型) 所谓不透明,就是对方难以推断的。不透明谓词就是代码的编写者知道是真是假是什么,但是攻击者难以从字面获悉。vmp的不透明谓词采用的形式是永真永假型。然后观察发现并未使用到可真可假型的不透明谓词(不过我听说旧版本的vmp曾用过)。 所谓"永真永假型",指的是基本块里 jcc 指令的跳转事实从未发生改变,即要么跳转,要么不跳转,不存在有时会跳有时不会跳的情况(本质上讲就是跳转条件要么恒成立要么恒不成立)。 构造永真永假型的不透明谓词有两种形式,一种是跳转条件全是基于一系列常量合成。因为都是常量,所以跳转条件表达式的最终结果肯定也是恒不变的。另一种是代入用户可控变量到跳转条件表达式中,在表达式里,无论用户变量取值为多少,表达式的整体运算最终结果是恒不变的。 (4) 内存操作数常量隐藏混淆vmp是基于栈的虚拟机,那么对于栈的访问以及从vm区块里读取虚拟机字节码,肯定会产生非常多的内存访问操作。然而vmp(这个混淆手法好像是在 3.8 以上的时候开始引入的)会在本来正常的内存访问操作数里插入一堆复杂的数值,并用寄存器来表示出来,例如 qword ptr ss:[rsp] 被变换成 qword ptr ss:[rsp + rdx - 0x76B2378A](rdx 此时为 0x76B2378A)。 也就是说,vmp在内存操作数里引入的 index 寄存器或 base 寄存器都是已知量,进一步观察发现,这些数值都是从执行过的上部分指令的 imm 里一步步计算过来的。 任意一个常量的计算过程(针对 rax 的反向切片):(5) MBA混淆将简单的表达式转换成复杂的算术糅合布尔运算的表达式,这个也不必多说了,数据流混淆的经典手段。
vmp里的万用门计算就大量运用了这种混淆手法。 (6) 间接跳转混淆指的是间接跳转指令的目标地址实际上是已知的且目的地只有一处。vmp破坏了call指令的通俗使用约定(调用call指令会在栈中传入位于该call指令的下一条指令的地址,也就是返回地址,一般而言函数调用结束后会根据保存在 [rsp] 的返回地址通过retn指令返回到call调用处),它会从栈中取出返回地址并进行一系列运算,而后得到一个真正的目标地址,然后再跳转过去。 当然实际上vmp的间接跳转混淆也可以算是上面所讲到的代码乱序,因为call指令从语义上讲也是相当于入栈返回地址并修改 rip,即 push retAddr; jmp targetAddr(7) 等价语义指令替换在保持语义一致的前提下,将指令替换成另一条复杂的指令(或者是不太常见的指令)。比如 push rax 可被替换成 lea rsp, qword ptr ss:[rsp - 8]; mov qword ptr ss:[rsp], rax,mov rax,5 可被替换成 or rax,5(rax 必为 0)或 add rax,5(rax 必为 0)第一个例子之所以用 lea rsp, qword ptr ss:[rsp - 8] 而不用 sub rsp, 8,是因为应用后者的话会额外引入 修改标志位 的语义行为,这就体现我们所说的要保持语义一致的说法,当然在实际中我们也可以视具体情况而选择忽略。第二个例子也同理。 稳定性检查: 既然我们要做反混淆,就肯定得想办法检查反混淆后的新代码是否还能正常工作。我的做法是首先关掉vmp的内存保护,将反混淆后的新指令覆盖掉原先的混淆指令,看最后程序是否能正常运行。可靠性检查: 反混淆后手动观察代码是否还存在未被反混淆的指令,如果重新看到了混淆指令,就说明反混淆效果存在欠缺。还是在vs2022 用 release x64编译以下代码:然后用vmp 3.9.4进行加壳保护,参数如下: 然后使用 x64dbg 的追踪功能来记录被vm函数的完整指令追踪记录,先找到vm函数,在vm函数的开头和结尾处下断点,然后开启x64dbg的跟踪,等到结尾处的断点被命中后就可以关闭跟踪得到完整的trace文件。 接下来就开始写代码了,先把trace信息读进来首先我们需要从trace里获取基本块(我们做反混淆的对象就是基本块),因为是从trace里提取的,按照顺序提取的基本块肯定会有重复的(也就是重复的执行),所以我们用set集合来存储所有的基本块先定义基本块的类:然后我们基本块的填入是这样的,从 record 里依次读取所有记录指令,遇到控制流指令就马上把收集到的记录指令写入到基本块,最后再调用 writeBBControlIns() 来写入基本块的跳转信息(也就是最后一条指令)并追加 bbs 里,由于 bbs 是set集合,python 自动帮我们做了基本块去重处理。(这时候的基本块就不再有 trace 时间的概念了)经过上述代码的处理,我们得到了 3577 个基本块。 这里补充下,实际上从 trace 获取基本块可能还会有大基本块包裹小基本块的情况,造成这种现象的原因是有跳转指令指向了某个基本块的内部,这样就必须做基本块拆分的工作,但根据观察(至少对该样本来说),vmp没有这种现象,所以就省略了。我们可以先画个图来看看反混淆前基本块互相间链接的样子:先定义"edge"类然后是主要的画图代码我们规定以下线条样式: 瞧瞧这,实在是太大了,所以这里我们只截取几张局部的来看: 对于基本块合并,我们的合并规则是一个基本块有且仅有一个后继,且该后继仅有此前驱(即无分支或合并点);此外,若基本块间无控制流中断(如跳转)且逻辑连续,也可合并。‌
例如在上图中,0x14043bf81: 6 有且只有一个后继基本块 0x1402

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-289859.htm
#2 2026-06-01 21:08:47
如果一个块的尾调用是在函数头 被赋值的 那么单独符号这个块 的地址不是遭重了
#3 2026-06-01 21:08:47
你调试器附加不会被检测到么,我用了TitanHide最新版插件,然后x64dbg还是被检测到调试器
#4 2026-06-01 21:08:47
太硬核了哥
#5 2026-06-01 21:08:47
谢谢分享  有VMP最新版工具吗
#6 2026-06-01 21:08:47
method


简单来说 一个块到下一个块的跳转地址 是通过一个值计算的 而这个值在另外一个块被赋值的 而符号执行遇到这种问题 往往会路径爆炸

我应该知道你意思了,现在文章的做法里,我间接跳转的后继跳转地址都是只收集trace里存在的,没有进行符号求解,不过这样的话,确实有可能会造成在进行基本块合并的时候,对即将合并的后一个基本块的前驱条件产生判断错误。针对这种问题我的初步想法还是分析多份trace(对应不同分支路径),得到运行整个vm函数的更完整的基本块信息,这点我在文章里忘记说明了
#7 2026-06-01 21:08:47
简单来说 一个块到下一个块的跳转地址 是通过一个值计算的  而这个值在另外一个块被赋值的 而符号执行遇到这种问题 往往会路径爆炸
#8 2026-06-01 21:08:47
mark
#9 2026-06-01 21:08:47
mark
#10 2026-06-01 21:08:47
以后用什么加密啊
#11 2026-06-01 21:08:47
感谢分享
#12 2026-06-01 21:08:47
method


如果一个块的尾调用是在函数头 被赋值的 那么单独符号这个块 的地址不是遭重了

什么意思,没理解到
#13 2026-06-01 21:08:47
666,刚好也在学习这些
#14 2026-06-01 21:08:47
可怕
#15 2026-06-01 21:08:47
M
#16 2026-06-01 21:08:47
好文章
‹ 上一页 1 2 下一页 ›

请登录后参与讨论

立即登录 注册账号