【AI】Tencent_VM_技术研究报告
Tencent VM(腾讯虚拟机保护)技术研究报告
文档说明:本文档基于公开技术博客与学术论文整理而成,旨在系统性地记录 Tencent VM(腾讯虚拟机混淆保护)的架构设计、混淆机制、兼容性处理及去虚拟化(Devirtualization)方法。文档包含实际反汇编截图、IR 代码、数据结构布局等详细技术资料,所有信息均标注来源链接,供安全研究人员学习参考。
整理日期:2026-08-05
信息来源:back.engineering 技术博客(1篇)、k0mkc「ゴロリのブログ」Devirt 系列连载(11篇)、arXiv 学术论文(2篇)
图片说明:本文档中所有反汇编/IR 截图均来自上述公开博客文章,版权归原作者所有。
目录
1. 概述
1.1 什么是 Tencent VM
Tencent VM 是腾讯反作弊系统 AntiCheatExpert(ACE) 中使用的自研虚拟机混淆保护技术。它在被保护的二进制文件中注入 .tvm(Tencent VM)段,将原始函数的机器码翻译为自定义字节码,由内嵌的 VM 解释器在运行时解释执行。该技术广泛应用于腾讯系游戏(如 Strinova 等)的反作弊保护中,ACE 近期也开始涉足 Anti Tamper(反篡改)领域。
被虚拟化的函数入口被无条件替换为 JMP 到 VM 段,下图展示了从原始函数入口到 .tvm 段跳转的完整代码:

图1-1:被虚拟化的函数入口被无条件 JMP 替换为跳转到 .tvm 段 [k0mkc Devirt 1]
来源:k0mkc - Devirt 1、back.engineering - Static Devirtualization of Tencent VM
1.2 VM 类型定位
Tencent VM 在技术谱系上最接近 VMProtect / Themida 类型,而非 Riot Vanguard 的 Griffin VM。其主要区别如下:
| 特征 | Tencent VM | Griffin (Vanguard) | Themida |
|---|---|---|---|
| Handler 合并 | 无 | 有 | 无 |
| VM 上下文存储位置 | 栈 | — | 寄存器 |
| 设计评价 | "不是特别强的VM" | 设计更好 | — |
k0mkc 评价该 VM "不是特别强的VM,适合作为 Devirt 入门",但同时也指出其设计存在多处性能问题。back.engineering 则从去虚拟化角度证明:"经典 VM 混淆在引导式符号求值面前不强,简单编译器优化 pass 即足以化简 Tencent VM"。
1.3 目标文件与编译环境
已知的分析目标主要为 ACE 内核驱动(ACE-GAME.sys、ACE-BASE.sys、ACE-BOOT.sys、ACE-CORE.sys 共 4 个驱动),使用 WDK 10.0.16299(Windows 10 1709,2017年10月) 编译。
通过对比 GsDriverEntry 和 __security_init_cookie 的实现版本确定编译器版本:
| WDK 版本 | 发布时间 | __security_init_cookie 形式 |
|---|---|---|
| WDK 7.1.0 | 2010 | TAILCALL |
| WDK 10.0.14393 | 1607, 2016-07 | TAILCALL |
| WDK 10.0.15063 | 1703, 2017-04 | TAILCALL |
| WDK 10.0.16299 | 1709, 2017-10 | CALL+RET(最后非 tail-call 版本) |
GsDriverEntry 是 WDK 的 CRT 函数,如果 DriverEntry 不显式指定给链接器,会由 GsDriverEntry 包装。ACE 驱动使用相当旧的 WDK 编译。
2. VM 架构设计
2.1 .tvm 段与 VM 入口
ACE 二进制中存在 .tvm(及 .tvm0)段,这是 "Tencent VM" 的缩写,存放 VM 字节码和解释器代码。被虚拟化的函数入口点被无条件 JMP 替换为跳转到 .tvm 段中的 VM 入口。
2.1.1 VM 入口完整流程
VM 入口处的详细反汇编如下(展示了 VM 栈空间分配和初始化代码):

图2-1:VM入口详细反汇编,包含栈空间分配和VM初始化 [k0mkc Devirt 1]
VM 入口的工作流程(从进入 .tvm 段开始):
- 分配栈空间:在栈上为 VM context 分配空间
- 保存寄存器:将所有通用寄存器(GPR)和 EFLAGS 压栈
- 移入 VM context:将寄存器从栈移入 VM context 结构
- 进入 dispatcher:通过带有垃圾指令(junk instructions)的间接 JMP 进入 dispatcher 循环
2.1.2 垃圾指令与间接 JMP
VM 入口处的间接 JMP 之前插入了大量垃圾指令(junk instructions),用于干扰静态分析:

图2-2:VM入口处带有垃圾指令的间接JMP [k0mkc Devirt 1]
关键发现:该间接 JMP 是静态可计算的——虽然看起来是间接跳转,但其目标地址可通过静态分析确定。k0mkc 编写了 IDA 插件来自动化此过程,自动计算间接 JMP 的目标地址。这是逆向分析的重要突破口。
2.2 VM 上下文与寄存器布局
VM 上下文分配在栈上(与 Themida 将上下文存于寄存器的方式不同)。这是 Tencent VM 的一个显著特征。
2.2.1 VM 入口帧结构
VM 入口通过以下指令创建局部帧:
sub rsp, 68h ; 创建 0x68 字节局部帧
mov rbp, rsp ; 建立帧指针(UWOP_SET_FPREG),guest RSP 保存在此
- 0x68 字节本地区:其中 0x48 字节块仅在 unwind 时使用,保存所有 9 个非易失性寄存器
- RBP 在 VM 入口保存后,整个 VM 执行期间从不作为 scratch 使用
[RBP+0]槽位保存 dummy 函数地址(用于 SEH 虚拟 unwind,详见第4章)
2.2.2 精确寄存器内存布局
VM context 中存在两套寄存器存储布局,分别位于不同偏移:
rbp+0x00..0x88 flags, rax, rbp, rbx, rcx, rdi, rdx, rsi, rsp, r8..r15
rbp+0x90..0x110 rax, rbx, rcx, rdx, rsp, rbp, rsi, rdi, r8..r15, flags
第一套布局(rbp+0x00..0x88):
| 偏移 | 内容 | 大小 |
|---|---|---|
| +0x00 | flags | 8 |
| +0x08 | rax | 8 |
| +0x10 | rbp | 8 |
| +0x18 | rbx | 8 |
| +0x20 | rcx | 8 |
| +0x28 | rdi | 8 |
| +0x30 | rdx | 8 |
| +0x38 | rsi | 8 |
| +0x40 | rsp | 8 |
| +0x48 | r8 | 8 |
| +0x50 | r9 | 8 |
| +0x58 | r10 | 8 |
| +0x60 | r11 | 8 |
| +0x68 | r12 | 8 |
| +0x70 | r13 | 8 |
| +0x78 | r14 | 8 |
| +0x80 | r15 | 8 |
第二套布局(rbp+0x90..0x110):
| 偏移 | 内容 | 大小 |
|---|---|---|
| +0x90 | rax | 8 |
| +0x98 | rbx | 8 |
| +0xA0 | rcx | 8 |
| +0xA8 | rdx | 8 |
| +0xB0 | rsp | 8 |
| +0xB8 | rbp | 8 |
| +0xC0 | rsi | 8 |
| +0xC8 | rdi | 8 |
| +0xD0 | r8 | 8 |
| +0xD8 | r9 | 8 |
| +0xE0 | r10 | 8 |
| +0xE8 | r11 | 8 |
| +0xF0 | r12 | 8 |
| +0xF8 | r13 | 8 |
| +0x100 | r14 | 8 |
| +0x108 | r15 | 8 |
| +0x110 | flags | 8 |
两套布局的设计目的:在 VMEXIT 和 VM 重入时,通过 push/pop 对在两套布局之间拷贝上下文,同时配合 not + xchg 混淆增加分析难度。
2.3 字节码与分发器机制
2.3.1 架构概览
Tencent VM 采用经典的 字节码 + 分发器(dispatcher) 架构:
- 寄存器 R11 包含 VM 字节码地址,供解释器使用
- VIP(Virtual Instruction Pointer) 保存在 VM context 结构中
- 执行线程在共享的 VM dispatch loop(调度循环)中进出
- VM 仅建模 AMD64 的一个子集,子集外的指令通过 boxed instruction 处理
2.3.2 Code-as-Data Dispatcher 计算
VM 会将代码作为数据读取(code-as-data trick),读取的数据用于计算 dispatcher 分发目标:

图2-3:code-as-data trick — 读取代码作为数据用于dispatcher地址计算 [k0mkc Devirt 1]
对于依赖 const 数据的 Load/Store,可以在 IR 中提升为立即值(需要保证数据一定是 const,属于 unsafe 的非 generic 优化)。
2.3.3 "不返回的 CALL"
VM 入口处通过 CALL 指令进入 dispatcher 循环,但这个 CALL 从不返回——CALL 之后的代码不会被执行。这是因为:
- VM 的 dispatcher 循环是一个无限循环,通过字节码驱动
- CALL 只是为了创建栈帧,用于 SEH 虚拟 unwind(详见第4章)
- CALL 后紧跟
int3指令,back.engineering 将其作为识别 dispatcher call 的启发式
Dispatcher 识别启发式:跟随后面直接放置 int3 的 call——符号执行路径上每一个这样的 call 都是进入 VM dispatch loop 的调用。
2.3.4 VIP 偏移解析算法
VIP 持于 VM context 结构内,其偏移可动态解析。back.engineering 提出的算法为:
找到向 VM context 写入的最后一条 store,其写入值为指向
.tvm0节的指针(字节码地址必在其中)。
该启发式效果很好,会自动揭示 VIP 在 context 中的偏移位置。
2.4 Boxed Instruction 机制
对于 VM 无法建模的指令,Tencent VM 使用 Boxed Instruction(盒装指令) 机制处理。这是虚拟化函数中执行原生指令的实用必需品。
2.4.1 工作流程
- Context 恢复:VM 将 VM context 中的寄存器恢复到物理寄存器
- 原生执行:原生执行原始指令——此时机器状态与未虚拟化函数产生的状态完全不可区分
- 状态捕获:指令执行完毕后,VM 将寄存器状态捕获回 VM context
- 恢复调度:重新进入 dispatcher 循环
2.4.2 关键性质
- Sink Point:每个 boxed instruction 是 VM 被迫物化真实 guest state 的点,因而是可靠的 sink point(汇点)
- 栈帧恢复:逆向分析时可利用此性质恢复原始栈帧大小(stack frame high water mark 技术)
- Chained Unwind Info:非 leaf 函数的 Boxed 命令附带链式展开信息(因其在 VM 外运行)
2.4.3 需要 Boxed 处理的指令类型
- 架构指令:CPUID、RDMSR、WRMSR 等——不实际执行就无法在 VM 内建模
- CALL/RET:函数调用和返回
- SSE/AVX 指令:无条件 VMEXIT(详见 5.5 节)
- VMX/SVM 指令:VMCALL、VMLAUNCH、VMRESUME 等——发现了设置 VMCS 调用 hypervisor 的例程
2.5 VM 的设计限制
2.5.1 根本性限制:无法虚拟化动态栈大小
VM 在切换到 guest 状态(VMEXIT)前不保存 VM 的 RSP。VM 重入时的 RSP 调整是固定值,且每个 Boxed 指令不同(取决于语义的 SP delta)。
→ 结论:该 VM 无法虚拟化具有动态栈大小的函数(即虚拟化时指令的 SP delta 必须静态确定)。
2.5.2 其他设计问题
k0mkc 指出的多处设计/性能问题:
- 滥用 PUSH/POP、PUSHFQ/POPFQ:到处插入导致不必要的性能恶化
- PUSHFQ/POPFQ 是重指令:需要权限检查,实际上是相当重的指令
- 对 Unwind 有严格约束
- 最初怀疑无法正确虚拟化有 SEH/CxxEH 的函数(后续 Devirt 11 证实 VM 确实有 SEH 兼容机制)
- SSE/AVX 无条件 VMEXIT:即使像 XORPS 这样无标志副作用且全 XMM 操作数的指令也走 boxed 路径,性能浪费严重
3. CET 兼容性机制
3.1 问题背景
CET(Control-flow Enforcement Technology)是 Intel 的硬件控制流完整性技术,其核心组件 Shadow Stack(影子栈) 的工作方式:
- CALL 指令:除了向数据栈压入返回地址,还向 shadow stack 压入返回地址
- RET 指令:从数据栈弹出返回地址的同时,从 shadow stack 弹出并验证
Tencent VM 用 CALL 进入 dispatcher,但该 call 从不返回——如果放任不管,shadow stack 会在虚拟化函数生命周期内单调增长直至 fault。
3.2 运行时 CET 检测
Tencent VM 在运行时而非保护时处理此问题,同时具有 CET 兼容和非兼容两种模式,由运行时动态决定。
3.2.1 CALL 后的 Trap
首先观察到 CALL 指令后跟随的 trap 指令,看起来 CET 不兼容:

图3-1:CALL后的trap指令,初看CET不兼容 [k0mkc Devirt 2]
但通过分析 Unwind Info 确认这不是异常处理的控制流,而是"不返回的 CALL"——VM 实际上有 CET 兼容机制。
3.2.2 RDSSP 检测机制
VM 使用 RDSSP 指令在运行时检测 CET 是否启用:

图3-2:使用RDSSP指令检测CET是否启用 [k0mkc Devirt 2]
检测原理:
- 预先将目标寄存器清零
- 执行
RDSSPQ读取当前 shadow stack 指针 - RDSSPQ 编码在 hint-NOP 空间 中
- 在未启用 shadow stack 的处理器/进程上,RDSSPQ 以 NOP 退休,不修改目标寄存器
- 因此清零后的寄存器保持为零——这是对新旧硬件都正确且零成本的特性检测
- 之后测试寄存器值,判断 CET 是否活跃
在 k0mkc 的 IR 中表示为:
%16772: i64 = frame_address 0x0 -0x308
%16773: i64 = load %16772
%16774: i64 = x86_rdssp i64
%16775: i64 = imm 0x1
%16776: i64 = cmp %16774, %16775
back.engineering 的文章中也有对应的反汇编截图:

图3-3:back.engineering 文章中的 RDSSPQ 特征检测反汇编 [back.engineering]
3.3 Shadow Stack 修复
当检测到 CET 活跃时,VM 执行 INCSSPQ 2(寄存器包含值 2),将 shadow stack 指针前进两个条目,丢弃两个永不会有匹配 RET 消费的返回地址。

图3-4:CET启用时使用INCSSP指令消耗shadow stack [k0mkc Devirt 2]
在 k0mkc 的 IR 中表示为:
%16897: i64 = frame_address 0x0 -0x308
%16898: i64 = load %16897
%16899: i64 = imm 0x2
x86_incssp i64, %16899
back.engineering 的反汇编截图:

图3-5:back.engineering 文章中的 INCSSPQ 修复反汇编 [back.engineering]
说明:
- INCSSP 严格来说不是特权指令,用户态也可用(仅限同进程内)
- 因为是驱动程序,影子栈可自由操作
- k0mkc 对为何保留非 CET 路径感到困惑——可能是为了兼容不支持 CET 的老系统
3.4 去虚拟化中的处理
RDSSP、INCSSP 不是语义指令,在去虚拟化时给它们始终返回"No"(CET 禁用),即可通过特化优化完全且安全地删除。
4. SEH 异常处理兼容性
4.1 整体架构
Tencent VM 通过精心设计的 .pdata/unwind info 结构实现 SEH(结构化异常处理)兼容,这是其设计中最精巧的部分。k0mkc 评价:"至少此 unwind 机制是 CET shadow stack 兼容的"。
核心组件包括:
- 单个大型
.pdata条目:覆盖 dispatcher、handler 等 VM 本体整体区域 - VM 专用异常处理器:地址
0x14028ED10 - Dummy 函数(幻影函数):从不执行,仅用于描述 unwind 操作
back.engineering 的 SEH 机制总览图:

图4-1:back.engineering 文章中的 SEH 异常处理机制总览 [back.engineering]
4.2 .pdata 条目
存在一个巨大的 pdata 条目,覆盖 dispatcher、handler 等 VM 本体全部区域:

图4-2:覆盖整个VM本体的巨大pdata条目 [k0mkc Devirt 11]
该 pdata 条目的 unwind info 携带语言特定异常处理器(language-specific exception handler)。VM 专用异常处理器的详细信息:

图4-3:VM专用异常处理器(地址0x14028ED10)详情 [k0mkc Devirt 11]
关键属性:
- 异常处理器地址:
0x14028ED10 UNW_FLAG_EHANDLER和UNW_FLAG_UHANDLER两个标志均置位- VM 内发生异常时,此 handler 被调用
4.3 Dummy 函数(幻影函数)
存在一个从不执行的 dummy 函数,其唯一目的是描述 unwind 操作:

图4-4:用于unwind处理的dummy函数(不实际执行) [k0mkc Devirt 11]
Dummy 函数的 Unwind Info 结构描述了所有非易失性寄存器的保存:

图4-5:dummy函数的Unwind Info,所有非易失性寄存器均被描述 [k0mkc Devirt 11]
关键特性:
- 指向 dummy 函数内部的标签始终保存在 VM entry 的帧指针
[RBP+0]中 - 在整个 VM 执行生命周期内保持不变
- Unwinder 通过
[RBP+0]找到 dummy 函数,查其 .pdata 执行 phantom unwind
4.4 VM 入口帧结构
VM 入口函数是唯一以函数调用形式创建帧的位置——这是"不返回的 CALL"所调用的函数:

图4-6:VM入口函数反汇编,创建unwind用的本地帧 [k0mkc Devirt 11]
帧结构详情:
sub rsp, 68h ; 创建 0x68 字节局部帧(仅 unwind 时使用)
mov rbp, rsp ; 创建帧指针(UWOP_SET_FPREG),guest RSP 保存在此
- 0x68 字节中,0x48 字节块仅 unwind 时使用,保存所有 9 个非易失性寄存器
- VM entry 的 unwind info 携带
UWOP_SET_FPREG,以 RBP 为帧寄存器 → unwind 相对 RBP 执行 - RBP 在 VM entry 保存,此后 VM 执行期间从不作为 scratch,故 unwinder 可在任意点正确 unwind
"不返回的 CALL"存在的原因:
- VM 内发生异常需要 unwind 时,无法用通常的 Unwind Info 的 Op 说明的方法让 Unwinder 从 guest 状态 unwind
- 因此 VM 需要自己定义的异常处理器
- 要拥有 handler,必须在 guest 下方创建帧,且所有 dispatcher 和 handler 等代码必须被 pdata 正确描述
4.5 虚拟 Unwind 完整流程
当 VM 内部发生异常时,虚拟 unwind 的完整步骤:
- OS 调用 VM 的异常处理器(
0x14028ED10) - 异常处理器将每个 guest 非易失性寄存器复制到 VM entry 预留的保存区域
- 用 guest RIP 覆写 unwind target slot——该 slot 位于 guest RSP 下方 8 字节处
- 异常处理器返回
ExceptionContinueSearch - Unwinder 沿
[RBP+0]取 dummy 函数作为下一个条目,查其 .pdata - 通过 phantom unwind info,将异常处理器刚暂存的所有 guest 非易失性寄存器写回 CONTEXT 中对应的原生寄存器
- Unwinder 读取 unwind target slot 内容作 RIP,RSP 前进 8
- 结果:
CONTEXT->RIP = guest RIP,CONTEXT->RSP恰好落在 guest RSP 上 - 此后原始函数的虚拟 unwind 以正确 guest state 继续
4.6 8 字节偏移的补偿原理
步骤 3 中异常处理器在 guest RSP 下方 8 字节处写入 guest RIP,是为了抵消步骤 7 中 unwinder 前进的 8 字节。
即:异常处理器故意把 RIP 写在 guest RSP - 8 的位置,unwinder 读取该值作 RIP 后 RSP +8 → 最终 RSP 恰好等于 guest RSP。这是一个非常精巧的设计。
4.7 其他 SEH 细节
- 如果原始函数有 SEH scope table,Tencent VM 会适当改写
- 非 leaf 函数的 Boxed 命令附带 Chained Unwind Info(链式展开信息)
- k0mkc 判断:该 unwind 机制至少是 CET shadow stack 兼容的
5. 虚拟化机制详解
5.1 条件跳转(JCC)虚拟化
Tencent VM 将原生 JCC 的标志比较操作扩展为多个 VM handler。虚拟化 JCC 是条件控制流虚拟化的核心机制。
5.1.1 四步虚拟化流程
JCC 虚拟化分为四个步骤:
步骤 1:取出 Guest 标志位
从 VM context 中取出 guest 的标志位(如 ZF,ZeroFlag):

图5-1:JCC虚拟化步骤1 — 从VM context取出guest的ZeroFlag [k0mkc Devirt 6]
步骤 2:SETCC 物化
使用 SETCC 指令将标志位物化为纯 i8 类型的 0/1 值:

图5-2:JCC虚拟化步骤2 — SETCC将ZF物化为i8的0/1值 [k0mkc Devirt 6]
步骤 3:VPC 选择
后续基本块基于 0/1 值选择不同的 VPC(Virtual Program Counter):

图5-3:JCC虚拟化步骤3 — 基于0/1值选择不同VPC [k0mkc Devirt 6]
步骤 4:Dispatcher 计算
基于计算出的 offset 决定 dispatch 目标地址:

图5-4:JCC虚拟化步骤4 — 基于offset计算dispatch目标 [k0mkc Devirt 6]
5.1.2 去虚拟化中的 JCC 处理
Devirt 管道处理虚拟分支的方法:
- 循环中发生虚拟分支时,分别用 0/1 分叉
- 再对分支做 0/1 具体化(否则作为不透明值,优化无法生效)
- 优化前的中间 IR 输出:

图5-5:优化前的中间IR输出(JCC虚拟化lifting阶段) [k0mkc Devirt 6]
JCC Devirt 后的结果输出(包含死标志消除和 guest 栈恢复等改进):

图5-6:JCC虚拟化解除后的Devirt输出结果 [k0mkc Devirt 6]
5.2 JCC DAG 模式匹配重写(back.engineering)
back.engineering 提出了完整的 JCC 模式匹配重写方案,将虚拟化 JCC 逻辑通过预定义的 IR SSA DAG 转换回原生 JCC。
5.2.1 标志位掩码与索引
| 标志位 | 掩码 (Mask) | 索引 (Idx) | SET 条件 | CLEAR 条件 |
|---|---|---|---|---|
| CF | 0x1 | 0 | b | ae |
| PF | 0x4 | 1 | p | np |
| ZF | 0x40 | 3 | e | ne |
| SF | 0x80 | 4 | s | ns |
| OF | 0x800 | 5 | o | no |
5.2.2 完整 JCC Pattern 列表
每个标志位有 4 种 pattern 变体(zero/mask × 两种条件),共 20 个 pattern:
; ── CF ── mask 0x1, idx 0
pattern vjcc.cf.ae.zero { body(0x1, 0x0) ; %r = R{e} %z } => { %r = R{ae} %cf }
pattern vjcc.cf.b .zero { body(0x1, 0x0) ; %r = R{ne} %z } => { %r = R{b} %cf }
pattern vjcc.cf.b .mask { body(0x1, 0x1) ; %r = R{e} %z } => { %r = R{b} %cf }
pattern vjcc.cf.ae.mask { body(0x1, 0x1) ; %r = R{ne} %z } => { %r = R{ae} %cf }
; ── PF ── mask 0x4, idx 1
pattern vjcc.pf.np.zero { body(0x4, 0x0) ; %r = R{e} %z } => { %r = R{np} %pf }
pattern vjcc.pf.p .zero { body(0x4, 0x0) ; %r = R{ne} %z } => { %r = R{p} %pf }
pattern vjcc.pf.p .mask { body(0x4, 0x4) ; %r = R{e} %z } => { %r = R{p} %pf }
pattern vjcc.pf.np.mask { body(0x4, 0x4) ; %r = R{ne} %z } => { %r = R{np} %pf }
; ── ZF ── mask 0x40, idx 3
pattern vjcc.zf.ne.zero { body(0x40, 0x0) ; %r = R{e} %z } => { %r = R{ne} %zf }
pattern vjcc.zf.e .zero { body(0x40, 0x0) ; %r = R{ne} %z } => { %r = R{e} %zf }
pattern vjcc.zf.e .mask { body(0x40, 0x40) ; %r = R{e} %z } => { %r = R{e} %zf }
pattern vjcc.zf.ne.mask { body(0x40, 0x40) ; %r = R{ne} %z } => { %r = R{ne} %zf }
; ── SF ── mask 0x80, idx 4
pattern vjcc.sf.ns.zero { body(0x80, 0x0) ; %r = R{e} %z } => { %r = R{ns} %sf }
pattern vjcc.sf.s .zero { body(0x80, 0x0) ; %r = R{ne} %z } => { %r = R{s} %sf }
pattern vjcc.sf.s .mask { body(0x80, 0x80) ; %r = R{e} %z } => { %r = R{s} %sf }
pattern vjcc.sf.ns.mask { body(0x80, 0x80) ; %r = R{ne} %z } => { %r = R{ns} %sf }
; ── OF ── mask 0x800, idx 5
pattern vjcc.of.no.zero { body(0x800, 0x0) ; %r = R{e} %z } => { %r = R{no} %of }
pattern vjcc.of.o .zero { body(0x800, 0x0) ; %r = R{ne} %z } => { %r = R{o} %of }
pattern vjcc.of.o .mask { body(0x800, 0x800) ; %r = R{e} %z } => { %r = R{o} %of }
pattern vjcc.of.no.mask { body(0x800, 0x800) ; %r = R{ne} %z } => { %r = R{no} %of }
5.2.3 模板化重写
template vjcc<FLAG, MASK, IDX, CC_SET, CC_CLEAR> {
%rf = X86ReadFlags %f[0..5]
%w = launder %rf
%m = And %w, imm MASK
%s = Sub %m, imm SUB where SUB ∈ { 0, MASK }
%z = X86Flag.ZF %s
%r = R{KIND} %z where KIND ∈ { e, ne }
} => {
%r = R{ (SUB == MASK) ⊕ (KIND == ne) ? CC_SET : CC_CLEAR } %f[IDX]
}
instantiate vjcc<CF, 0x1, 0, b, ae>
instantiate vjcc<PF, 0x4, 1, p, np>
instantiate vjcc<ZF, 0x40, 3, e, ne>
instantiate vjcc<SF, 0x80, 4, s, ns>
instantiate vjcc<OF, 0x800, 5, o, no>
变换语义:
- 原 DAG:
ReadFlags → launder → And(MASK) → Sub(SUB∈{0,MASK}) → X86Flag.ZF → R{e|ne} - 重写为:直接
R{CC} %f[IDX] - CC 条件选择:
(SUB==MASK) ⊕ (KIND==ne) ? CC_SET : CC_CLEAR
5.3 CALL 处理与 VMEXIT
5.3.1 语义 CALL 完整处理流程
VM 处理语义 CALL(如 CALL __security_init_cookie)的完整流程:
- 从 VM context 读寄存器:将 GPR 从第二套布局(rbp+0x90..0x110)读出到物理寄存器,过程中使用
not+xchg混淆 - VMExit:临时退出 VM 状态
- 执行语义 CALL:执行真正的 CALL 指令
- 恢复 RSP:
lea rsp, [rsp-260h]将 RSP 调整回 VM 栈 - 存回 VM context:将寄存器存回第一套布局(rbp+0x00..0x88)
- 上下文拷贝:通过 push/pop 对从第一套布局拷贝到第二套布局
- 重新进入 VM:
call sub_1401243E3重新进入 dispatcher,int 3作为终止
5.3.2 完整反汇编(CALL __security_init_cookie)
以下是语义 CALL 处理的完整反汇编(地址偏移相对于 RSP):
; ===== 阶段 1:从 VM context 读寄存器到物理寄存器(带 not/xchg 混淆) =====
000 nop
000 lea r11, [rbp+90h] ; r11 = 第二套寄存器布局基址
000 xchg rax, r9
000 mov r9, [r11] ; 读 rbp+90h (rax)
000 not r9 ; not 混淆
000 xchg rax, r9
000 not rax ; 两次 not 还原
000 mov rbx, [r11+8] ; rbx = rbp+98h (rbx)
000 xchg rcx, r10
000 mov r10, [r11+10h] ; rbp+A0h (rcx)
000 not r10
000 xchg r10, rcx
000 not rcx
000 xchg rax, rdx
000 mov rax, [r11+18h] ; rbp+A8h (rdx)
000 not rax
000 xchg rax, rdx
000 not rdx
000 mov rbp, [r11+28h] ; rbp = rbp+B8h (rbp)
000 mov rsi, [r11+30h] ; rsi = rbp+C0h (rsi)
000 mov rdi, [r11+38h] ; rdi = rbp+C8h (rdi)
000 xchg rax, r8
000 mov rax, [r11+40h] ; rbp+D0h (r8)
000 not rax
000 xchg rax, r8
000 not r8
000 xchg r9, rcx
000 mov rcx, [r11+48h] ; rbp+D8h (r9)
000 not rcx
000 xchg rcx, r9
000 not r9
000 xchg r10, rdx
000 mov rdx, [r11+50h] ; rbp+E0h (r10)
000 not rdx
000 xchg rdx, r10
000 not r10
000 mov r12, [r11+60h] ; r12 = rbp+F0h
000 mov r13, [r11+68h] ; r13 = rbp+F8h
000 mov r14, [r11+70h] ; r14 = rbp+100h
000 mov r15, [r11+78h] ; r15 = rbp+108h
000 push qword ptr [r11+80h] ; push rbp+110h (flags)
008 popfq ; 恢复标志寄存器
; ===== 保存 guest RSP =====
000 push qword ptr [r11+20h] ; push rbp+B0h (rsp)
; ===== 阶段 2:VMExit + 执行语义 CALL =====
008 mov r11, [r11+58h] ; r11 = rbp+E8h (r11)
008 mov rsp, [rsp+8+var_8] ; RSP 切换到 guest RSP
-270 call __security_init_cookie ; 执行真正的语义 CALL
; ===== 阶段 3:恢复 RSP 到 VM 栈 =====
-270 lea rsp, [rsp-260h] ; RSP 切回 VM 栈(-260h = 0x270-0x10?)
; ===== 阶段 4:寄存器存回 VM context 第一套布局 =====
-10 mov [rsp-10h+arg_18], rbp
-10 mov rbp, rsp
-10 pushfq
-08 pop qword ptr [rbp+0] ; 保存 flags 到 rbp+0
-10 mov [rbp+78h], r14 ; r14 -> rbp+78h (第一套的 r14)
-10 not r8
-10 xchg r8, rdx
-10 mov [rbp+30h], r8 ; rdx -> rbp+30h (第一套的 rdx)
-10 not rdx
-10 xchg r8, rdx
-10 not r10
-10 xchg r10, r9
-10 mov [rbp+50h], r10 ; r9 -> rbp+50h (第一套的 r10?)
-10 not r9
-10 xchg r10, r9
-10 not r10
-10 xchg rax, r10
-10 mov [rbp+8], r10 ; r10 -> rbp+8 (第一套的 rax)
-10 not rax
-10 xchg rax, r10
-10 not rax
-10 xchg rax, r11
-10 mov [rbp+60h], rax ; r11 -> rbp+60h
-10 not r11
-10 xchg rax, r11
-10 mov [rbp+18h], rbx ; rbx -> rbp+18h
-10 mov [rbp+40h], rsp ; rsp -> rbp+40h
-10 mov [rbp+28h], rdi ; rdi -> rbp+28h
-10 mov [rbp+70h], r13 ; r13 -> rbp+70h
-10 not rax
-10 xchg rax, r8
-10 mov [rbp+48h], rax ; r8 -> rbp+48h
-10 not r8
-10 xchg rax, r8
-10 mov [rbp+80h], r15 ; r15 -> rbp+80h
-10 not rcx
-10 xchg rcx, r10
-10 mov [rbp+58h], rcx ; r10 -> rbp+58h
-10 not r10
-10 xchg rcx, r10
-10 mov [rbp+38h], rsi ; rsi -> rbp+38h
-10 mov [rbp+68h], r12 ; r12 -> rbp+68h
-10 not rdx
-10 xchg rdx, rcx
-10 mov [rbp+20h], rdx ; rcx -> rbp+20h
-10 not rcx
-10 xchg rdx, rcx
-10 pushfq
-08 add qword ptr [rbp+40h], 260h ; 修正 guest RSP(+260h 补偿)
-08 popfq
; ===== 阶段 5:push/pop 上下文拷贝(第一套 -> 第二套) =====
-10 lea r11, [rbp+90h] ; r11 = 第二套布局基址
-10 push qword ptr [rbp+8] ; rax
-08 pop qword ptr [r11] ; -> rbp+90h
-10 push qword ptr [rbp+18h] ; rbx
-08 pop qword ptr [r11+8] ; -> rbp+98h
-10 push qword ptr [rbp+20h] ; rcx
-08 pop qword ptr [r11+10h] ; -> rbp+A0h
-10 push qword ptr [rbp+30h] ; rdx
-08 pop qword ptr [r11+18h] ; -> rbp+A8h
-10 push qword ptr [rbp+40h] ; rsp
-08 pop qword ptr [r11+20h] ; -> rbp+B0h
-10 push qword ptr [rbp+10h] ; rbp
-08 pop qword ptr [r11+28h] ; -> rbp+B8h
-10 push qword ptr [rbp+38h] ; rsi
-08 pop qword ptr [r11+30h] ; -> rbp+C0h
-10 push qword ptr [rbp+28h] ; rdi
-08 pop qword ptr [r11+38h] ; -> rbp+C8h
-10 push qword ptr [rbp+48h] ; r8
-08 pop qword ptr [r11+40h] ; -> rbp+D0h
-10 push qword ptr [rbp+50h] ; r9
-08 pop qword ptr [r11+48h] ; -> rbp+D8h
-10 push qword ptr [rbp+58h] ; r10
-08 pop qword ptr [r11+50h] ; -> rbp+E0h
-10 push qword ptr [rbp+60h] ; r11
-08 pop qword ptr [r11+58h] ; -> rbp+E8h
-10 push qword ptr [rbp+68h] ; r12
-08 pop qword ptr [r11+60h] ; -> rbp+F0h
-10 push qword ptr [rbp+70h] ; r13
-08 pop qword ptr [r11+68h] ; -> rbp+F8h
-10 push qword ptr [rbp+78h] ; r14
-08 pop qword ptr [r11+70h] ; -> rbp+100h
-10 push qword ptr [rbp+80h] ; r15
-08 pop qword ptr [r11+78h] ; -> rbp+108h
-10 push qword ptr [rbp+0] ; flags
-08 pop qword ptr [r11+80h] ; -> rbp+110h
; ===== 阶段 6:重新进入 VM =====
-10 lea r11, dword_14009A4E4 ; 字节码地址?
-10 lea r10, [rbp+88h]
-10 lea rsp, [rsp-8]
-08 mov [rsp-8+arg_0], r10
-08 lea rsp, [rsp-8]
000 mov [rsp+0], r11
000 call sub_1401243E3 ; 重新进入 dispatcher
000 int 3 ; 终止(不会执行到)
5.3.3 VMEXIT 检测方法
通过具体化 RSP 值可以几乎确定精度地检测 VMEXIT,也适用于 CALL、CPUID 等包含栈 pivot 的 Boxed 指令。当 rsp=None 时表示检测到 VMEXIT。
5.4 RSP 虚拟化
5.4.1 问题与挑战
VM 上下文中保存 guest RSP,函数的 prologue 和 epilogue 也被虚拟化。这是去虚拟化的关键挑战。
初期方法的问题(基于 trace):
- 给 SSA 值赋予具体值,实际具体执行 IR 来决定下一个分支
- 输入依赖的部分在大函数中计算成本高
- 需要将具体执行结果"安全地"反映回 IR(双重工作)
- 重要缺陷:guest RSP+0x30 和 guest RSP+0x38 的 store 语义丢失
5.4.2 重构方案:RSP 作为显式 SSA 值
k0mkc 废弃现有代码从头重写,核心改动是将 RSP 建模为显式 SSA 值:
- 可以具体化 RSP 值,在 IR 中落为立即值
- 全部可通过纯 SCCP + InstCombine + Reassociate + GVN 优化
- CALL/RET/PUSH/PUSHFQ/POP/POPFQ 等栈状态变更指令的 lifting 必须遵循此模型
- PUSH/POP 作为对栈槽的 Load/Store 来 lifting
5.4.3 RSP 追踪完整输出
以下是 97 个 site 的 RSP 追踪结果(入口 0x140123ede,VM exit tail-call 目标 0x1400130d0):
[ 0] 0x140123ede rsp=0x7ffffffefd98
[ 1] 0x140124167 rsp=0x7ffffffefd98
[ 2] 0x140124385 rsp=0x7ffffffefd90
[ 3] 0x1401241d7 rsp=0x7ffffffefd90
[ 4] 0x140124122 rsp=0x7ffffffefd90
[ 5] 0x1401243e3 rsp=0x7ffffffefcf8
[ 6] 0x140227b87 rsp=0x7ffffffefcf0
[ 7] 0x140132222 rsp=0x7ffffffefcf8
[ 8] 0x1401e1897 rsp=0x7ffffffefcf0
[ 9] 0x14014db67 rsp=0x7ffffffefcf8
[10] 0x14027322e rsp=0x7ffffffefcf0
...
[91] 0x140139066 rsp=0x7ffffffefcf8
[92] 0x140139077 rsp=0x7ffffffefcf0
[93] 0x14028a091 rsp=0x7ffffffefcf8
[94] 0x14024dea7 rsp=0x7ffffffefd70
[95] 0x14028a089 rsp=0x7ffffffefd88
[96] 0x140124236 rsp=None
stopped: VM exit, tail-call 0x1400130d0
reached 97 sites
rsp=None 表示 VMExit——此时 RSP 值不再能静态追踪。
5.4.4 Guest RSP 优化策略
该方法的特性:对原始 frame 的写入全部落在正 frame offset(guest RSP)上,因此 guest RSP 以下的区域可被优化全部剪除。为此在框架中引入了 DeadStoreElimination(DSE)pass(实现参考 LLVM)。
5.5 SSE/AVX 指令处理
5.5.1 无条件 VMEXIT
Tencent VM 不论语义如何,无条件对 SSE/AVX 指令进行 VMEXIT。即使像 XORPS 这样没有标志副作用且两个操作数都是 XMM 寄存器的指令也走 boxed 路径。

图5-7:SSE/AVX指令被无条件VMEXIT(boxed处理) [k0mkc Devirt 8]
5.5.2 性能影响
k0mkc 评价这是"相当粗暴的(ゴリラ的)Boxed 命令处理方式":
- 严格来说此语义的 Boxed 命令处理成本非常重
- 根本不需要移动到 guest 状态(因为 XORPS 无标志副作用且全 XMM 操作数)
- 连续 Boxed 命令也每次只执行一个语义指令
- VM 施加的性能劣化可能比想象中更严重
5.5.3 对 Devirt 的影响
- 可以完全像现有 Boxed 命令一样处理,Devirt 无需做任何特殊处理
- 如果语义没有标志副作用/内存副作用,会自然地通过 IR 的 DCE/DSE 消除
5.6 循环虚拟化
5.6.1 循环分类
循环分为两类,对去虚拟化带来不同的挑战:
有界循环(bounded)——边界为固定值:
int bounded(int a, int b) {
int count = 0;
for (int i = 0; i < 32; i++)
count += (a >> i) & (b >> i) & 1;
return count;
}
无界循环(unbounded)——边界为运行时值:
int unbounded(int a) {
int steps = 0;
for (; a != 1; steps++)
a = (a % 2 == 0) ? a / 2 : 3 * a + 1;
return steps;
}
5.6.2 计算复杂度分析
- 固定循环检测通过 unroll 是 O(n)
- 但考虑 lifting 周期,每个周期增加使计算成本趋向 O(n²) 的超线性
- 即:循环次数越多,每轮 lifting 也跑得越多,总计算量超线性增长
5.6.3 优化方法:基于 VPC 的 backedge 检测
关键洞察:优化到该程度时 VPC 应已可具体化。
优化方法:只需基于 VPC 查看是否有 back-edge,无需实际跑完循环即可正确构建 IR。
- back-edge 不一定总在 loop header,判定方法需要工夫
- 已解决大部分 edge case
- 该规模函数在优化适当传播且所有具体评估可解时,毫秒级完成从 Devirt 到代码生成
循环 Devirt 输出示例:

图5-8:包含循环控制流的函数Devirt输出结果 [k0mkc Devirt 8]
5.7 VMCS / Hypervisor 检测
在成功去虚拟化的函数中发现了设置 VMCS(Virtual Machine Control Structure) 并调用 hypervisor 的例程:

图5-9:VMCS设置与hypervisor调用例程 [k0mkc Devirt 8]
Lifting 中 VMCALL 等 VMX/SVM 指令此前为 WIP,现已支持——作为 Intrinsic 添加,用可配置 ABI 逻辑建模即可,无特别困难。
此外还发现了使用 RDTSC 进行典型的/经典的虚拟机/管理程序检测逻辑。
5.8 基址重定位 Trick
VM 在 MOV R64, IMM64 指令的 IMM64 部分赋予 DIR64(base relocation)。完全相同的 trick 在 navagio(PUBG 内制反作弊 Zakynthos)中也见过。
但这只是 ASLR 加载时的 delta 补正,不需要特别在意。
6. 混淆技术
6.1 MBA(Mixed Boolean-Arithmetic)混淆
6.1.1 概述
Tencent VM 使用平凡的 MBA 恒等规则递归应用来生成更大的 MBA 表达式。这些规则本质上是布尔代数恒等式,通过递归组合生成复杂的混淆表达式。
去混淆方法:只需在 inst-combine 规则集中定义这些规则,运行优化到不动点(fixpoint)即可完全化简 Tencent MBA。
6.1.2 完整 MBA 恒等规则列表
算术与位运算混合规则(加法/减法与 AND/OR/XOR):
(A|B) + (A&B) = A + B ; AND+OR = 加法
(A^B) + (A&B) = A | B ; XOR+AND = OR
(A|B) - (A&B) = A ^ B ; OR-AND = XOR
((A|B)^A) + A = A | B ; 变种:((A|B)^A) + A = A|B
(A&B) + (A|B) = A + B ; 交换律变种
(((A^B)|B) & A) + B = A + B ; 复杂变种:((A^B)|B)=A|B, (A|B)&A=A
AND 恒等构造规则(8种不同形式构造 A&B):
~((~A ^ ~B) | ~A) = A & B ; De Morgan 变种
~(((A^B) & ~B) ^ ~A) = A & B ; 复杂 De Morgan 变种
A - (A - (A&B)) = A & B ; 减法回绕变种
B - (((A&B)&B) ^ B) = A & B ; 带 XOR 的减法变种
常量-变量恒等规则(A 恒等于 A 的 4 种形式):
((c&A) ^ A) | A = A ; AND+XOR+OR = A
(A & ((A^c) | c)) = A ; AND(OR(XOR)) = A
(((A&c) ^ A) | A) = A ; 变种 1
(((c|A) ^ c) | A) = A ; 变种 2
取反与算术混合规则:
~(A - c) = -A + (c-1) ; 取反减法 = 取反加法(常折叠为 -A)
~( <A+c gadget> ) = -A + c ; A+c 取反 = -A + c
6.2 寄存器混淆(not + xchg)
6.2.1 混淆机制
VM 大量使用 not + xchg 组合进行寄存器混淆交换。在从 VM context 读写寄存器时,通过先 not 再 xchg 的方式混淆实际的寄存器映射关系。
6.2.2 典型模式(以读 rax 为例)
lea r11, [rbp+90h] ; r11 = VM context 第二套布局基址
xchg rax, r9 ; 交换 rax 和 r9
mov r9, [r11] ; 从 context 读值到 r9
not r9 ; 取反混淆
xchg rax, r9 ; 交换回
not rax ; 再次取反(两次 not 还原)
效果:
- 实际结果:rax = [r11](从 context 正确读出)
- 但中间经过了 xchg 和 not,使得静态分析难以直接追踪数据流
- 写寄存器时使用同样的混淆模式
6.2.3 去混淆
在 IR 中,not + not 会被 InstCombine 消掉,xchg 会被优化掉,最终还原为简单的 load/store。
6.3 代码作为数据读取(Code-as-Data)
VM 会将代码作为数据读取(code-as-data trick),读取的数据用于计算 dispatcher 分发目标地址。
对于依赖 const 数据的 Load/Store,可以在 IR 中提升为立即值(需要保证数据一定是 const,属于 unsafe 的非 generic 优化)。
6.4 垃圾指令与 PUSHFQ/POPFQ
- Junk instructions:间接 JMP 前插入垃圾指令,干扰静态分析
- PUSHFQ/POPFQ 滥用:到处插入 PUSHFQ/POPFQ 来在 clobber flag 的指令之间保持标志位
- 性能影响:PUSHFQ/POPFQ 需要权限检查,实际上是相当重的指令,导致不必要的性能恶化
6.5 反 VM 检测混淆
成功去虚拟化的函数中发现了多种反虚拟机/反调试检测逻辑:
- RDTSC 时间检测:典型的/经典的 RDTSC 虚拟机/管理程序检测逻辑
- VMCS / Hypervisor 检测:设置 VMCS 调用 hypervisor 的例程
- CPUID 检测:通过 CPUID 指令检查虚拟化特征
这些检测逻辑本身也被 VM 虚拟化保护,增加了分析难度。
7. 去虚拟化方法
7.1 方法一:引导式符号执行(back.engineering)
back.engineering 提出了完整的静态去虚拟化方案,核心是将原生 AMD64 指令提升为更高级别的 SSA(静态单赋值)中间表示,通过符号化评估整个虚拟化函数。
7.1.1 总体目标
引导式符号执行 = 把原生指令(本文 AMD64)lift 到更高层 SSA IR,以便优化/操纵。目标是符号化求值整个虚拟化函数,生成含全部语义的 IR 函数。符号 lift 循环在遇到间接控制流时需要引导。
经典 VM 混淆用字节码影响 VM 内间接控制流(编码要执行哪些 VM handler)。R11 含 VM 字节码地址。
7.1.2 Dispatcher 内联启发式
符号内联 VM dispatcher 循环的调用。两种启发式:
- 启发式 A(推荐):跟随其后紧跟 int3 的 call——符号执行路径上每一个这样的 call 都是进入 VM dispatch loop 的调用
- 启发式 B(替代):把 VM dispatcher 函数声明为符号引擎的有效 call target 来跟随/内联
7.1.3 字节码常量提升
为求解 VM 内的间接控制流,必须把字节码在 SSA IR 中提升为常量,使其他优化能折掉字节码解密运算。
注意:此 load 提升必须仔细限定作用域,使原始语义 load 不被提升为常量。
7.1.4 VIP 跟踪与循环 backedge 处理
为防止虚拟化循环被 re-lift(unrolling),必须跟踪 VIP:
- 若已 lift 过下一 VIP/vm handler,则只创建 backedge(后向边)
- 不重新 lift 已访问过的 handler
VIP 偏移解析算法(Tencent VM 专用):
找到向 VM context 写入的最后一条 store,其写入值为指向
.tvm0节的指针。
该启发式效果好,会自动揭示 VIP 在 context 中的偏移。
7.1.5 RSP 具体化与栈帧恢复
- RSP 具体化:lift 开始时把 RSP 具体化为任意常量值,以复用现有优化折掉 RSP 修改
- VMEXIT 检测:使用具体 RSP 值作为启发式判断是否处于 VMEXIT
- 栈帧高水位标记(Stack Frame High Water Mark):在 sink point(call 和 boxed instruction)处,RSP 会暴露原函数栈帧大小
- Prolog/Epilog 重建:已知 high water mark 后可重建函数 prolog/epilog
- Spill 处理:若 recompiled 代码发生 spill,须把原函数栈帧放在新栈帧之后;对 RSP 在返回地址及以上处的引用须按 spill 空间新尺寸调整
此方法同样应用于 Themida、VMP、vxlang、Tencent VM、Denuvo 等混淆器。
7.1.6 Lowering(重编译回原生代码)
在完成 IR 优化后,将 IR lowering 回原生 AMD64 代码,重建函数 prolog/epilog,生成可重编译的干净二进制。
去虚拟化后的干净输出示例:

图7-1:ACE-GAME.sys 去虚拟化后的驱动入口,"大多数去虚拟化函数都这么干净" [back.engineering]
7.2 方法二:IR 提升与优化管道(k0mkc)
k0mkc 独立开发了一套基于自研 IR 框架的去虚拟化工具(Rust 实现),经历了从基于 trace 的具体执行到纯静态优化的演进。
7.2.1 Devirt 管道流程图
整个 Devirt 管道的抽象架构如下:

图7-2:Devirt管道整体流程图 [k0mkc Devirt 10]
7.2.2 方法演进
初期方法(基于 trace 的具体执行):
- 给 SSA 值赋予具体值,实际具体执行 IR 来确定下一个分支
- 问题:输入依赖的部分在大函数中计算成本高;需要将具体执行结果"安全地"反映回 IR;guest RSP+0x30 和 +0x38 的 store 语义会丢失
重构后方法(RSP 作为显式 SSA 值):
- 将 RSP 建模为显式 SSA 值,可以具体化 RSP 值在 IR 中落为立即值
- 全部可通过纯 SCCP + InstCombine + Reassociate + GVN 优化
- CALL/RET/PUSH/PUSHFQ/POP/POPFQ 等栈状态变更指令的 lifting 遵循此模型
7.2.3 Devirt 核心循环
核心循环:反汇编 → 优化(折叠到下一个 handler 地址)→ lifting 的循环
- 虚拟分支处理:循环中发生虚拟分支时,分别用 0/1 分叉,再对分支做 0/1 具体化
- VMEXIT 检测:通过具体化的 RSP 判定,几乎确定精度地检测 VMEXIT(也适用于 CALL/CPUID 等含栈 pivot 的 Boxed 命令)
- PHI 节点支持:从只支持线性控制流的特化 pass 扩展到支持 PHI
- 性能:循环中执行相当数量优化 pass,几乎全部在 3000-5000ms 内完成所有式折叠及后续地址折叠(debug build,release 预计 2x-3x 快)
7.2.4 优化 Pass 列表
| 优化 Pass | 作用 | 说明 |
|---|---|---|
| SCCP | 稀疏条件常量传播 | Sparse Conditional Constant Propagation |
| InstCombine | 指令合并 | 含 MBA 规则化简(match 代码见下) |
| Reassociate | 重新关联表达式 | 调整表达式结合顺序以利后续优化 |
| GVN | 全局值编号 | Global Value Numbering,消除冗余计算 |
| DSE | 死存储消除 | Dead Store Elimination,参考 LLVM 实现 |
| mem2reg | 内存到寄存器提升 | 将栈变量提升为寄存器 |
| DCE | 死代码消除 | Dead Code Elimination |
7.2.5 InstCombine 的 Rust match 代码示例
match &self.func.dfg.insts[def] {
IR::Immediate { imm } => Some(*imm),
IR::Add { left, right } => {
// `x + ~x = -1`
if self.is_complement(*left, *right, next) {
return Some(-1);
}
// `(A & B) + (A | B) = A + B`
let (left, right) = self
.and_or_pair(*left, *right, next)
.unwrap_or((*left, *right));
Some(
self.constant(left, next)?
.wrapping_add(self.constant(right, next)?),
)
}
// ... 更多 match arm
}
计算的分支只有简单恒等式,反复 InstCombine 即可折叠。值依赖 Load/Store 时将 Load/Store 提升为立即数。
7.2.6 Codegen 改善
regalloc3 rematerialization:
- 此前 WIP 未启用,导致大量 spill
- 现已对应——只需实现 regalloc3 的
can_rematerializetrait - Machine IR 成熟后不难实现
Intel APX 指令集:
- 大改造 Iced 使其能用 Intel APX 指令集编译
- 利用大量 GPR 避免 spill
- APX 有更多通用寄存器(R16-R31),显著降低寄存器压力
栈帧融合:
- 重新编译时若新发生 spill,需要融合 guest 栈帧以正确布置栈帧布局
- 这对 IDA 等模拟代码中正确放置 CALL 参数很重要
Lea+Store 折叠为 MOV CS:
Lea+Store 对可折叠为单个 MOV CS(rip-rel)的 ISLE lowering 规则:
(rule 116 (lower (ir_store addr value))
(if (single_use value))
(if-let val_def (value_def_inst value))
(if-let imm (ir_imm val_def))
(if-let width (store_imm_width value imm))
(if (single_use addr))
(if-let addr_def (value_def_inst addr))
(if-let (ir_x86_load_address inner) addr_def)
(if-let (pinned_imm_from_value target) inner)
(let ((_ Unit (sink addr_def))
(_2 Unit (sink val_def)))
(emit_store_imm_rip_rel target imm width)))
7.2.7 Anti-VM 函数 Devirt 示例
反虚拟机检测相关函数 devirt 后的输出:

图7-3:Anti-VM检查函数Devirt结果,毫秒级完成从devirt到代码生成 [k0mkc Devirt 10]
7.3 两种方法对比
| 维度 | back.engineering | k0mkc |
|---|---|---|
| 核心方法 | 引导式符号执行 | IR 提升 + 优化管道 |
| IR 基础 | 自研 SSA IR | 自研 IR 框架(Rust) |
| RSP 处理 | 具体化为常量 | 显式 SSA 值建模 |
| JCC 还原 | DAG 模式匹配重写 | SETCC 具体化为 0/1 + 0/1分叉 |
| MBA 还原 | inst-combine 到不动点 | InstCombine 规则集 |
| 循环处理 | VIP 跟踪 + backedge | VPC 判断 backedge |
| 代码生成 | 自研 lowering | regalloc3 + Iced (APX) |
| 验证 | Daax (secret club) 独立确认 | 实际 devirt 输出验证 |
| 目标文件 | 4个ACE驱动 | ACE.sys 单驱动为主 |
| 语言实现 | 未公开(推测 C++/Rust) | Rust |
8. 去虚拟化覆盖率与局限性
8.1 覆盖率统计
back.engineering 对 4 个 ACE 内核驱动进行了完整的去虚拟化测试。覆盖率按驱动度量 = 成功重编译为原生代码的虚拟化函数占比。
度量方法:
- 虚拟化函数由其入口 trampoline(E9 填充 int3)识别
- 去虚拟化后 trampoline 从指向
.tvm0字节码解释器改为指向 recompiled.devirt代码 - 工具未能恢复的函数仍指向
.tvm0
完整统计数据:
| Driver | Virtualized functions | Devirtualized | Coverage |
|---|---|---|---|
| ACE-GAME.sys | 134 | 133 | 99.3 % |
| ACE-BASE.sys | 329 | 313 | 95.1 % |
| ACE-BOOT.sys | 235 | 219 | 93.2 % |
| ACE-CORE.sys | 167 | 150 | 89.8 % |
| Total | 865 | 815 | 94.2 % |
结论:四个内核驱动中,865 个虚拟化函数里有 815 个(94.2%) 被完整恢复为原生代码。
第三方 Daax(secret club)已被提供文件,可独立确认输出的清洁度和覆盖率。
8.2 局限性
Ranged for 循环对激进的间接控制流优化方法是重大问题。
示例:for (int i = 0; i < 10; i++)
问题:
- 虚拟化后条件
i < 10变成 VJCC - 但
i = 0(初始值)使优化把 VJCC 折成单一分支目的地——即循环 back edge - 由于已 lift 过 back edge,lift 停止并创建无限循环
可能的解决方案:对 back edge 重新 lift,把除 VIP 外一切符号化。
8.3 结论
back.engineering 的结论:
我们一直认为经典 VM 混淆在引导式符号求值面前不强。已证明简单编译器优化 pass(常量提升、常量折叠、若干 trivial MBA 化简规则)即足以化简 Tencent VM。
随着 AI 辅助静态去混淆持续改进,将看到更多基于 VM 的混淆器瓦解。下篇文章将公布如何完整静态去虚拟化 Denuvo anti(tamper/cheat)——这是仅次于反作弊的被攻击最严重的软件之一。
对自家产品 CodeDefender,已特别留意直接阻碍基于 lift 的攻击与引导式符号求值。本文表明,通过分析加固目标,混淆器能轻易识别并规避常见陷阱。
9. 逆向分析工具与流程
9.1 k0mkc 工具链(Rust 自研)
k0mkc 从零开始构建了一套完整的 Tencent VM 去虚拟化工具链,全部使用 Rust 语言实现,涵盖从反汇编、IR 提升、优化到代码生成的完整管道。
9.1.1 核心组件
| 组件 | 功能 | 技术选型 |
|---|---|---|
| IDA 插件 | 自动化识别 VM 入口、计算间接 JMP 目标 | IDA Pro SDK |
| 反汇编引擎 | x86/x64 指令解码 | Iced |
| IR 框架 | 自研 SSA IR,支持 lifting/optimization/lowering | Rust 自研 |
| 寄存器分配 | 寄存器分配与再物化(rematerialization) | regalloc3 |
| 代码生成 | lowering 回原生 x86/x64 代码 | Iced (支持 Intel APX) |
| 优化管道 | SCCP / InstCombine / Reassociate / GVN / DSE / mem2reg / DCE | 自研(参考 LLVM) |
9.1.2 IDA 插件功能
k0mkc 开发了 IDA 插件来自动化初步分析:
- VM 入口识别:自动识别被虚拟化函数的入口 trampoline(E9 填充 int3)
- 间接 JMP 计算:VM 入口处的间接 JMP 目标地址是静态可计算的,插件自动计算并标注
- 字节码区域定位:自动定位
.tvm/.tvm0段中的字节码区域 - Dispatcher 识别:基于"CALL 后紧跟 int3"的启发式识别 dispatcher 入口
9.1.3 Iced 反汇编库
Iced 是一个高性能的 x86/x64 指令解码器/编码器:
- 支持完整的 x86/x64 指令集
- k0mkc 对其进行了大改造以支持 Intel APX 指令集
- APX 提供 R16-R31 共 16 个额外通用寄存器,可显著降低寄存器压力
- 代码生成阶段利用 APX 大量 GPR 避免 spill
9.1.4 regalloc3 寄存器分配器
- 基于 Rust 的寄存器分配库
- 支持 rematerialization(再物化) 特性
- rematerialization:当值被 spill 时,不是存储到栈上,而是在需要时重新计算该值
- 实现
can_rematerializetrait 即可启用 - 启用后大幅减少 spill,提升生成代码质量
9.2 back.engineering 工具链
back.engineering 的工具链细节未完全公开,但从文章中可推断以下组件:
9.2.1 引导式符号执行引擎
- 符号 Lift 循环:将原生 AMD64 指令 lift 到高层 SSA IR
- Dispatcher 内联:基于启发式内联 VM dispatcher 循环调用
- VIP 跟踪:跟踪虚拟指令指针,处理循环 backedge
- RSP 具体化:将 RSP 具体化为常量以复用优化
9.2.2 优化与 Lowering
- 常量提升:字节码常量提升为立即值,折掉字节码解密运算
- MBA 化简:inst-combine 规则集,运行到不动点
- JCC DAG 重写:20 种 JCC 模式匹配,将虚拟化标志比较还原为原生 JCC
- 栈帧恢复:Stack Frame High Water Mark 技术恢复原始栈帧大小
- 函数重建:重建 prolog/epilog,生成可重编译的干净二进制
9.2.3 CodeDefender
back.engineering 将 Tencent VM 分析中的发现反哺于自家产品 CodeDefender:
- 特别留意直接阻碍基于 lift 的攻击与引导式符号求值的防护
- 通过分析加固目标,混淆器能轻易识别并规避常见陷阱
- 这是攻防互相促进的典型案例
9.3 完整逆向分析流程
以 k0mkc 的方法为例,完整的 Tencent VM 逆向分析流程如下:
┌─────────────────────────────────────────────────────────────┐
│ 阶段 1:预处理(IDA 插件) │
│ ├── 识别虚拟化函数入口(trampoline 模式匹配) │
│ ├── 计算间接 JMP 目标地址 │
│ ├── 定位 .tvm 字节码区域与 dispatcher │
│ └── 识别 VM context 结构与 VIP 偏移 │
├─────────────────────────────────────────────────────────────┤
│ 阶段 2:Lifting(IR 提升) │
│ ├── 反汇编 VM handler 代码 │
│ ├── 提升为 SSA IR(x86 语义 → IR 指令) │
│ ├── RSP 作为显式 SSA 值建模 │
│ └── 字节码常量提升(code-as-data 处理) │
├─────────────────────────────────────────────────────────────┤
│ 阶段 3:Devirt 核心循环 │
│ ├── 优化 → 折叠到下一个 handler 地址 → 继续 lifting │
│ ├── 虚拟分支处理:0/1 分叉 + 具体化 │
│ ├── VMEXIT 检测(RSP 具体化判定) │
│ ├── VPC backedge 检测(循环处理) │
│ └── PHI 节点构建(控制流合流点) │
├─────────────────────────────────────────────────────────────┤
│ 阶段 4:优化管道(Optimization Pipeline) │
│ ├── SCCP(稀疏条件常量传播) │
│ ├── InstCombine(指令合并 + MBA 化简) │
│ ├── Reassociate(重新关联表达式) │
│ ├── GVN(全局值编号) │
│ ├── DSE(死存储消除) │
│ ├── mem2reg(内存到寄存器提升) │
│ └── DCE(死代码消除) │
├─────────────────────────────────────────────────────────────┤
│ 阶段 5:代码生成(Lowering / Codegen) │
│ ├── regalloc3 寄存器分配(含 rematerialization) │
│ ├── Intel APX 指令集支持(减少 spill) │
│ ├── 栈帧融合(guest 栈帧 + 新 spill 空间) │
│ ├── Lea+Store → MOV CS 折叠 │
│ └── 重建函数 prolog/epilog │
└─────────────────────────────────────────────────────────────┘
9.4 性能指标
k0mkc 的工具在 debug build 下的性能:
| 指标 | 数值 | 说明 |
|---|---|---|
| 中等函数 devirt 时间 | 3000-5000 ms | debug build,含全部优化 pass |
| release build 预计加速 | 2x-3x | 基于 Rust 编译器优化预期 |
| 小函数/简单函数 | 毫秒级 | Anti-VM 检查等函数 |
| 循环 backedge 检测 | 毫秒级 | 基于 VPC 判断,无需 unroll |
back.engineering 的覆盖率(4 个 ACE 驱动):
| 驱动 | 虚拟化函数数 | 成功去虚拟化 | 覆盖率 |
|---|---|---|---|
| ACE-GAME.sys | 134 | 133 | 99.3% |
| ACE-BASE.sys | 329 | 313 | 95.1% |
| ACE-BOOT.sys | 235 | 219 | 93.2% |
| ACE-CORE.sys | 167 | 150 | 89.8% |
| 总计 | 865 | 815 | 94.2% |
来源:k0mkc - Devirt 6、k0mkc - Devirt 10、back.engineering - Coverage Statistics
10. 学术研究背景
Tencent VM 的去虚拟化技术并非孤立发展,而是建立在学术界多年来对二进制去混淆、虚拟化攻击的研究基础之上。本章介绍两篇具有代表性的学术论文,展示从理论研究到实战应用的技术演进路径。
10.1 PUSHAN:无 Trace 的虚拟化去混淆(2026)
10.1.1 论文概况
- 标题:Pushan: Trace-Free Deobfuscation of Virtualization-Obfuscated Binaries
- 作者:Ashwin Sudhir 等(Arizona State University, CISPA Helmholtz Center)
- 发表时间:2026 年 3 月(arXiv: 2603.18355v1)
- 核心贡献:首个实现无 trace(trace-free)、无约束求解(constraint-free) 的完整虚拟化去混淆框架,并支持高质量 C 伪代码反编译
10.1.2 现有技术的三大局限
论文指出现有虚拟化去混淆技术的三大核心缺陷:
| 缺陷 | 描述 | 影响 |
|---|---|---|
| 路径覆盖不完整 | 基于 trace 的技术只能分析单条执行路径 | 无法获取完整 CFG,遗漏关键逻辑 |
| 可扩展性差 | 依赖动态符号执行(DSE),受路径爆炸问题困扰 | 计算成本高,无法处理复杂程序 |
| 可用性低 | 输出原始指令轨迹,无法被反编译器消费 | 分析人员需要回到手动逆向 |
10.1.3 PUSHAN 核心设计思想
两大核心洞察:
-
符号状态增长有界化:CFG 恢复是结构性问题。每个基本块(由地址 + VPC 共同标识)一旦被发现并模拟执行,就不会再次模拟。通过在块边界折叠所有入口路径,避免传统符号执行的路径爆炸。
-
消除约束求解依赖:CFG 恢复不需要路径可行性判断。目标不是确定跳转是否可行,而是枚举可能的控制流目标集合。因此 PUSHAN 在解析间接跳转时,SMT 求解器仅用作表达式简化器和值枚举器,而非输入可行性检查器。
关键创新:将 CFG 恢复与路径可行性解耦。
10.1.4 三阶段处理管道
PUSHAN 的分析管道分为三个阶段:
阶段 1:VPC 敏感 CFG 恢复
- 使用抽象状态(寄存器 + 内存)符号模拟混淆二进制
- 通过原生地址 + VPC 唯一标识基本块,实现上下文敏感重建
- 传播值以简化跳转目标表达式,解析间接跳转
- 使用符号化补充模拟过程中遗漏的边
- 输出:flat CFG(包含 VM 解释器逻辑 + 原始程序逻辑)
阶段 2:语义保持简化
- 对 flat CFG 应用一系列语义保持变换
- 移除 VM 解释器相关逻辑,只保留原始代码逻辑
- 迭代执行直到收敛
阶段 3:反编译
- 使用增强型反编译器生成 C 类伪代码
- 增强内容:节点标识符重写、函数边界检测改进、栈指针追踪扩展
10.1.5 评估结果
在 1000+ 个二进制上的评估结果:
| 数据集 | 样本数 | 完全去混淆 | 说明 |
|---|---|---|---|
| VMProtect / Themida(14个程序) | 28 | 17 个 100% CFG 相似 | 其余高相似度 |
| Tigress(3种配置) | 1000 | 988 个(98.8%) | 完全分析并去混淆 |
| CTF 挑战(定制 VM) | 5 | 5/5 成功 | 1 个直接恢复出 flag |
| 真实恶意软件(VirusTotal) | 1 | 成功 | 2018年样本,首次完整分析 |
与 prior work 对比(以 Netsky 恶意软件为例):
- Yadegari et al.:CFG 相似度仅 33%(VMProtect)/ 19%(Themida)
- PUSHAN:CFG 相似度达 96%(VMProtect)/ 96%(Themida)
10.1.6 与 Tencent VM 去虚拟化的关联
PUSHAN 的方法论与 back.engineering / k0mkc 对 Tencent VM 的分析高度一致:
| 维度 | PUSHAN(学术) | Tencent VM Devirt(实战) |
|---|---|---|
| 核心思想 | VPC 敏感 + 无约束符号模拟 | VIP/VPC 跟踪 + 引导式符号求值 |
| 循环处理 | 块边界折叠 + backedge 检测 | VIP 跟踪 + backedge 创建 |
| MBA 处理 | 语义保持简化(MBA-Blast 等) | InstCombine 到不动点 |
| 输出形式 | C 伪代码(反编译) | 原生 x86 代码(recompiled) |
| 目标 | VMProtect / Themida / Tigress | Tencent VM(ACE) |
来源:PUSHAN: Trace-Free Deobfuscation of Virtualization-Obfuscated Binaries(arXiv: 2603.18355v1, 2026)
10.2 SATURN:基于 LLVM 的软件去混淆框架(2019)
10.2.1 论文概况
- 标题:SATURN: Software Deobfuscation Framework Based on LLVM
- 作者:Peter Garba(Thales), Matteo Favaro(Zimperium)
- 发表时间:2019 年 9 月(arXiv: 1909.01752v2)
- 核心贡献:基于 LLVM 编译器框架的通用去混淆与重编译框架,将攻击面从二进制级别提升到编译器 IR 级别
10.2.2 核心思想
"将攻击面提升回混淆实现的级别——编译器级别。"
SATURN 的核心理念:现代混淆器大多基于编译器框架(如 LLVM)实现,因此去混淆也应该在同一层次(IR 级别)上进行,利用编译器的强大优化能力来消解混淆。
10.2.3 技术架构
核心组件:
| 组件 | 功能 | 技术 |
|---|---|---|
| 代码提升(Lifting) | 二进制代码 → LLVM-IR | Remill |
| CFG 恢复 | 迭代式控制流图构建 | 基于编译器优化 + SMT 求解 |
| 不透明谓词检测 | 识别并移除不透明谓词 | LLVM 优化 + Souper + SMT |
| 去混淆 | 消解各种混淆技术 | LLVM 强优化 + Souper Optimizer |
| 代码亮化(Brightening) | 提升 IR 可读性 | Remill State struct → 原生函数 |
| 重编译 | LLVM-IR → 原生代码 | LLVM 后端(X86/ARM/RISC-V 等) |
Remill State Struct:
Remill 将所有 x86 状态封装在一个 State 结构体中,包括:
- 通用寄存器(GPR)、向量寄存器(VectorReg)
- 算术标志(ArithFlags)、标志寄存器(rflag)
- 段寄存器(Segments)、地址空间(AddressSpace)
- x87 FPU 栈、MMX、FPU 状态等
10.2.4 关键技术
1. 迭代式 CFG 构建算法
- 基于部分去混淆的基本块及其前驱进行路径探索
- 独立于分支检查顺序(优于 [3] 中的算法)
- 不需要先验知识,不依赖执行 trace
2. 不透明谓词检测
- 第一层:LLVM 强优化 + Souper Optimizer 自动化简
- 第二层(验证/兜底):SMT 求解器验证
- 优势:优化后 SMT 查询更小更高效,相比 Triton 等工具求解时间更短
3. 代码亮化(Brightening)
- 将 Remill 的 State struct 形式转换为干净的 LLVM 函数
- 恢复函数参数签名
- 恢复栈帧结构
- 使 IDA 等反编译器能产生有意义的伪 C 代码
10.2.5 支持的混淆技术
SATURN 可有效削弱或移除以下混淆技术:
| 混淆技术 | 处理效果 |
|---|---|
| 常量展开(Constant unfolding) | 完全移除 |
| 算术不透明表达式 | 大部分移除 |
| 死代码插入(Dead code insertion) | 完全移除 |
| 虚假控制流(Bogus control flow) | 完全移除 |
| 整数编码(Integer encoding) | 部分移除 |
| 反符号执行技巧(anti-DSE) | 部分削弱 |
| MBA 表达式 | 部分化简 |
| Tigress 虚拟化 | 保持虚拟化形式但代码可读 |
10.2.6 评估结果
在三个数据集上的测试结果:
| 数据集 | 测试用例 | Opaque Predicate 恢复 | 可执行验证 | 反编译器可读 |
|---|---|---|---|---|
| Dataset #1(Corner cases) | 12 | 全部检测 | 全部语义等价 | 是 |
| Dataset #2(Binsec/SPLIT/FOR) | 5 | 大部分检测 | 语义等价 | 是 |
| Dataset #3(真实世界混淆器) | 2 | 未知(无源码对比) | 有反篡改保护 | 是 |
重要发现:
- 所有测试程序经 SATURN 处理后,IDA 反编译器均能返回有意义的伪 C 代码
- 经过 LLVM 优化后,SMT 查询规模显著减小,求解效率大幅提升
- 对真实世界混淆器(如 Obfuscator-LLVM 变种)也有良好效果
10.2.7 与 Tencent VM 去虚拟化的关联
SATURN 奠定了"编译器 IR 级别去混淆"的范式,这一思想在 Tencent VM 去虚拟化中得到延续和深化:
| 维度 | SATURN(2019,学术) | Tencent VM Devirt(2026,实战) |
|---|---|---|
| 核心思想 | LLVM-IR 级别去混淆 | 自研 IR 级别去虚拟化 |
| Lifting 工具 | Remill | 自研(基于 Iced) |
| 优化来源 | LLVM + Souper | 自研(参考 LLVM 设计) |
| 不透明谓词 | LLVM 优化 + SMT | InstCombine + SCCP |
| 代码生成 | LLVM 后端 | regalloc3 + Iced |
| 处理目标 | OLLVM 类混淆 | VM 虚拟化混淆 |
| 栈恢复 | 部分(State struct → 函数) | 完整(RSP 具体化 + DSE) |
SATURN 证明了基于编译器优化的去混淆范式是可行的,而 Tencent VM 的实战去虚拟化则进一步验证了:面对 VM 级别的混淆,引导式符号执行 + 编译器优化管道是当前最有效的方法。
来源:SATURN: Software Deobfuscation Framework Based on LLVM(arXiv: 1909.01752v2, 2019)
10.3 技术演进脉络
从学术研究到实战应用,虚拟化去混淆技术的发展呈现清晰的演进路径:
2010s 初 2019 2023-2024 2026
│ │ │ │
▼ ▼ ▼ ▼
基于 trace SATURN: 引导式符号执行 PUSHAN:
的动态分析 LLVM-IR级别 实战应用成熟 无trace完整
(VMHunt, 去混淆框架 (back.engineering 去混淆 +
Yadegari) (通用混淆) / k0mkc 针对 反编译输出
Tencent VM) (通用VM)
───────────────────────────────────────────────────────>
从动态到静态 从通用到特定VM 从特定到通用
从单路径到完整CFG 从IR优化到符号执行 从反汇编到反编译
关键转折点:
- SATURN(2019):确立了"编译器 IR 级别去混淆"范式,证明 LLVM 优化可有效消解混淆
- back.engineering / k0mkc(2026):将引导式符号执行应用于 Tencent VM,达到 94%+ 覆盖率,实战验证方法有效性
- PUSHAN(2026):系统化 VPC 敏感到无 trace 去混淆,学术上完整化理论框架并支持反编译输出
共同结论:经典 VM 混淆在符号执行 + 编译器优化面前防御能力有限,随着 AI 辅助静态去混淆的持续改进,更多基于 VM 的混淆器将被瓦解。
11. 信息来源汇总
11.1 技术博客
11.1.1 back.engineering(1 篇)
| 序号 | 标题 | 作者 | 发布日期 | 核心内容 |
|---|---|---|---|---|
| 1 | Static Devirtualization of Tencent VM | back.engineering 团队 | 2026-07-31 | Tencent VM 完整静态去虚拟化方案,包括架构分析、CET/SEH 兼容性、JCC DAG 重写、MBA 化简、94.2% 覆盖率统计 |
- 链接:https://back.engineering/blog/31/07/2026/
- 特点:系统性强,从架构到去虚拟化方法全覆盖,有完整的覆盖率数据,第三方 Daax(secret club)独立验证
11.1.2 k0mkc Devirt 系列(11 篇)
k0mkc 的"ゴロリのブログ"Devirt 系列连载,逐日更新,记录了从零开始逆向分析 Tencent VM 的完整过程:
| 序号 | 标题 | 发布日期 | 核心内容 |
|---|---|---|---|
| Devirt 1 | Tencent VM (ACE) の Devirt をする 1 | 2026-07-20 | VM 概述、.tvm 段、入口 JMP、dispatcher、code-as-data、VM 类型定位 |
| Devirt 2 | Tencent VM (ACE) の Devirt をする 2 | 2026-07-21 | CET 兼容机制、RDSSP 检测、INCSSP 修复、not+xchg 寄存器混淆 |
| Devirt 3 | Tencent VM (ACE) の Devirt をする 3 | 2026-07-22 | 基址重定位 Trick、DIR64 处理 |
| Devirt 4 | Tencent VM (ACE) の Devirt をする 4 | 2026-07-22 | WDK 版本识别、编译环境、RSP 虚拟化初期方法与问题 |
| Devirt 5 | Tencent VM (ACE) の Devirt をする 5 | 2026-07-23 | RSP 作为显式 SSA 值重构、Devirt 核心循环、PHI 节点支持 |
| Devirt 6 | Tencent VM (ACE) の Devirt をする 6 | 2026-07-23 | JCC 虚拟化四步流程、SETCC 物化、VPC 选择、虚拟分支处理 |
| Devirt 7 | Tencent VM (ACE) の Devirt をする 7 | 2026-07-24 | (Devirt 7 内容,CALL/RET 处理等) |
| Devirt 8 | Tencent VM (ACE) の Devirt をする 8 | 2026-07-25 | SSE/AVX 无条件 VMEXIT、循环虚拟化、VMCS/Hypervisor 检测 |
| Devirt 9 | Tencent VM (ACE) の Devirt をする 9 | 2026-07-26 | 循环 backedge 检测、有界/无界循环处理 |
| Devirt 10 | Tencent VM (ACE) の Devirt をする 10 | 2026-07-27 | 完整管道流程图、regalloc3 rematerialization、Intel APX 支持、Anti-VM devirt 示例 |
| Devirt 11 | Tencent VM (ACE) の Devirt をする 11 | 2026-07-29 | SEH 兼容机制、pdata 条目、dummy 函数、虚拟 unwind 流程 |
博客主页:https://k0mkc.hatenablog.com/
特点:逐日更新的实战记录,包含大量反汇编截图和 IR 代码示例,细节详实,适合学习 Devirt 实现细节
11.2 学术论文
| 序号 | 标题 | 作者 | 发表时间 | arXiv 编号 | 核心贡献 |
|---|---|---|---|---|---|
| 1 | PUSHAN: Trace-Free Deobfuscation of Virtualization-Obfuscated Binaries | Ashwin Sudhir 等(ASU, CISPA) | 2026-03 | 2603.18355v1 | 首个无 trace、无约束求解的完整 VM 去混淆框架,支持 C 伪代码反编译,1000+ 二进制评估,98.8% 成功率 |
| 2 | SATURN: Software Deobfuscation Framework Based on LLVM | Peter Garba(Thales), Matteo Favaro(Zimperium) | 2019-09 | 1909.01752v2 | 基于 LLVM 的通用去混淆框架,将攻击面提升到编译器 IR 级别,支持多种混淆技术的消解与重编译 |
11.3 相关技术资源
| 名称 | 类型 | 用途 |
|---|---|---|
| IDA Pro | 商业逆向工具 | 静态反汇编、交互式分析 |
| Iced | 开源 x86/x64 汇编/反汇编库 | 指令解码与编码,支持 Intel APX |
| regalloc3 | Rust 寄存器分配库 | 代码生成阶段的寄存器分配 |
| Remill | 开源二进制 lifting 框架 | SATURN 论文使用,二进制 → LLVM-IR |
| LLVM | 开源编译器框架 | SATURN 基础,IR 优化与代码生成 |
| Souper Optimizer | LLVM 超优化器 | SATURN 使用,peephole 优化 + SMT 求解 |
| KLEE | 符号执行引擎 | 基于 LLVM-IR 的动态符号执行 |
11.4 术语表
| 术语 | 英文 | 含义 |
|---|---|---|
| VM | Virtual Machine | 虚拟机,本文指代码虚拟化保护 |
| VPC / VIP | Virtual Program Counter / Virtual Instruction Pointer | 虚拟程序计数器/指令指针,指向 VM 字节码位置 |
| Devirt | Devirtualization | 去虚拟化,将 VM 字节码还原为原生代码 |
| MBA | Mixed Boolean-Arithmetic | 混合布尔算术混淆 |
| CET | Control-flow Enforcement Technology | Intel 控制流强制技术(影子栈) |
| SEH | Structured Exception Handling | 结构化异常处理 |
| Boxed Instruction | — | 盒装指令,VM 无法建模时原生执行的指令 |
| VMEXIT | — | VM 退出,从 VM 状态切回原生状态 |
| SSA | Static Single Assignment | 静态单赋值形式 |
| SCCP | Sparse Conditional Constant Propagation | 稀疏条件常量传播 |
| GVN | Global Value Numbering | 全局值编号 |
| DSE | Dead Store Elimination | 死存储消除 |
| DCE | Dead Code Elimination | 死代码消除 |
| CFG | Control Flow Graph | 控制流图 |
| APX | Advanced Performance Extensions | Intel APX 指令集(新增 R16-R31) |
文档结束
本文档系统性地整理了 Tencent VM(腾讯虚拟机保护)的架构设计、混淆机制、兼容性处理及去虚拟化方法,涵盖了当前公开的最详细的技术资料。所有内容均来自公开的技术博客与学术论文,仅供安全研究与学习使用。
最后更新:2026-08-05

浙公网安备 33010602011771号