VTIL & NoVmp 源码简要分析
项目简介
VTIL 项目,代表 Virtual-machine Translation Intermediate Language,是一组围绕优化编译器设计的工具,用于二进制去混淆和去虚拟化。
VTIL 与其他优化编译器(如 LLVM)之间的主要区别在于,它具有极其通用的 IL,可以轻松地从包括堆栈机器在内的任何架构中进行转换。
由于它是为翻译而构建的,因此 VTIL 不会抽象出本机 ISA,而是按原样保留通用 CPU 的堆栈、物理寄存器和非 SSA 架构的概念。
本机指令可以在 IL 流的中间插入,物理寄存器可以从 VTIL 指令自由寻址。
VTIL 还使得在任何请求的虚拟地址处插入本机指令变得轻而易举,而不受特定文件格式的限制。
项目地址:https://github.com/vtil-project
架构转换
下面以 NoVmp 项目为例分析 VMP 指令和 VTIL 指令之间的转换
Native 指令到 VMP 指令的映射关系定义在 architecture.cpp
VMP 指令到 VTIL 指令的映射关系定义在 il2vtil.cpp
VTIL 指令到 Native 指令的映射关系定义在 demo_compiler.hpp
转换代码定义在 lift_il.cpp
转换程序通过特征匹配将 Native 指令转换为 VMP 指令,并通过 VMENTER
、VMEXIT
和 VJMP
指令对控制流进行跟踪
使用递归下降的方法,以 Block 指令块为基本单位进行转换
VMP 到 VTIL 转换流程
-
首先转换程序会跟踪原生程序的控制流,若跳转目标地址在 VMP 段内,标志着到达 VMProtectBegin 虚拟机总入口点,开始 VMP 保护
-
VMP 虚拟机中存在若干不同的入口点和出口点,对应
VMENTER
指令和VMEXIT
指令,除了总入口点和总出口点以外,其他都用于调用外部函数或 Native 指令 -
当需要调用外部函数或 Native 指令时,程序会先执行
VMEXIT
指令暂时离开虚拟机,随后调用外部函数或 Native 指令,最后再执行VMENTER
指令重新回到虚拟机 -
转换程序建立 Block 存放转换后的 VTIL 指令,并使用滚动密钥解密当前 VMP 指令的参数
-
遇到
VJMP
指令时,程序会在 Block 中插入JMP
指令,并使用符号执行的方法,求出所有可能的跳转目标,再对这些可能的分支进行递归处理 -
遇到
VMEXIT
指令时,会对跳转的目标地址进行求解-
若目标地址在 VMP 段内,则进一步判断,若
SP
偏移小于 0,且跳转目标处的指令是VMENTER
,则代表这里是 External Call,否则是 VM Exit-
对于 External Call,标志着控制流暂时离开 VMP 虚拟机调用外部函数,程序会在 Block 中插入
VXCALL
指令调用外部函数,再回到 VMP 虚拟机继续处理 -
对于 VM Exit,标志着控制流暂时离开 VMP 虚拟机执行 Native 指令块,程序会在 Block 中通过
VEMIT
指令插入 Native 指令块,再回到 VMP 虚拟机继续处理
-
-
若目标地址在 VMP 段外,标志着到达 VMProtectEnd 虚拟机总出口点,结束 VMP 保护,程序会在 Block 中插入
VEXIT
指令
-
-
遇到其他 VMP 指令时,程序会先将该指令转换为 VTIL 指令,再把转换后的结果插入到 Block
VMP 执行流程
图片来源:https://back.engineering/17/05/2021/
注:在 VMP 3.x 中 CPUID
指令由 VCPUID
指令执行,不会进入 Native Execution,但其他特殊指令的处理和上图是类似的
指令优化
VTIL 内置的 11 个 Pass 优化器定义在 VTIL-Core\VTIL-Compiler\optimizer
下面将对这 11 个优化器进行分析
bblock_extension_pass
若一个 Block 由且仅由另一个 Block 进行调用,该优化器会尝试将这两个 Block 合并为一个 Block
branch_correction_pass
遇到跳转指令时,该优化器会尝试通过符号执行的方法,去除冗余的跳转目标,并将分支指令从 JMP
优化为 JS
dead_code_elimination_pass
该优化器会尝试分析无效的读写操作,去除冗余的指令
fast_dead_code_elimination_pass
该优化器会尝试分析无效的读写操作,去除冗余的指令,功能同上
fast_propagation_pass
该优化器会尝试分析数据的传播过程,去除中间过程冗余的指令
istack_ref_substitution_pass
该优化器会尝试将栈上数据的引用全部替换为 SP
加偏移的形式
mov_propagation_pass
该优化器会尝试分析数据通过 MOV
指令的传播过程,去除中间过程冗余的指令
register_renaming_pass
该优化器会尝试分析数据通过寄存器的传播过程,去除中间过程冗余的指令
stack_pinning_pass
该优化器会尝试分析 SP
的变化,提前计算出栈上读写操作的偏移
stack_propagation_pass
该优化器会尝试分析数据通过栈的传播过程,去除中间过程冗余的指令
symbolic_rewrite_pass
该优化器会尝试通过符号执行和表达式特征匹配的方法,在没有遇到分支指令且 SP
没有变化时,在比特粒度下对寄存器、栈以及内存中数据的前后变化进行分析,从而对表达式进行简化
NoVmp 还原示例
NoVmp 对于线性代码的还原效果比较好,但是对于循环和分支代码的还原效果比较差
简单代码还原
测试代码:
int main(){
VMProtectBegin(MARKER_TITLE);
printf("test");
__asm{
in al, dx
out dx, al
}
VMProtectEnd();
}
还原结果:
Lifted & optimized virtual-machine at 000000000011F53D
Optimizer stats:
- Block count: 4 => 2 (-50.00%).
- Instruction count: 551 => 10 (-98.19%).
Special instructions:
- 0000000000000000: in al, dx
- 0000000000000001: out dx, al
-- Virtualized real references to register 'r15'
-- Virtualized real references to register 'r14'
Register allocation step 0...
Frame size: 0x0 bytes.
Instruction count: 14
Halting register virtualization as it did not improve the result.
Frame size: 0x0 bytes.
Instruction count: 14
-- rbp + 0x8 := r14
-- rbp + 0x0 := r15
push rbp
mov rbp, rsp
sub rsp, 0x18
+0x0 strq rbp -0x10 r14
mov qword ptr [rbp - 0x10], r14
+0x0 strq rbp -0x8 r15
mov qword ptr [rbp - 0x8], r15
+0x0 movq rcx &&base
lea rcx, [rip + routine_base - 0x124000 + 0x0000000000000000]
+0x0 addq rcx 0x19c34
add rcx, 0x19c34
+0x0 vxcallq 0x1118b
call 0x1118b
+0x0 vpinrw rdx:16
in al, dx
+0x0 vpinwb rax:8
+0x0 vpinrb rax:8
+0x0 vpinrw rdx:16
out dx, al
+0x0 lddq r15 rbp -0x10
mov r15, qword ptr [rbp - 0x10]
+0x0 lddq r14 rbp -0x18
mov r14, qword ptr [rbp - 0x18]
+0x0 vexitq 0x118b2
mov rsp, rbp
pop rbp
jmp 0x118b2
routine_base:block_1d155:
push rbp
mov rbp, rsp
sub rsp, 0x18
mov qword ptr [rbp - 0x10], r14
mov qword ptr [rbp - 0x8], r15
lea rcx, [rip + routine_base - 0x124000 + 0x0000000000000000]
add rcx, 0x19c34
call 0x1118b
block_1d4ac:
in al, dx
out dx, al
mov r15, qword ptr [rbp - 0x10]
mov r14, qword ptr [rbp - 0x18]
mov rsp, rbp
pop rbp
jmp 0x118b2
复杂代码还原
测试代码:
int main(){
VMProtectBegin(MARKER_TITLE);
for (int i = 0; i < 3; i++) {
p();
if (i == 0) {
q();
}
else {
r();
}
s();
}
VMProtectEnd();
}
还原过程出错:
[*] Error: Assertion failure, !allocated_register at NoVmp\NoVmp\demo_compiler.hpp:671
[*] Unexpected error: Assertion failure, !allocated_register at NoVmp\NoVmp\demo_compiler.hpp:671
指令集
VTIL 指令集定义在 VTIL-Architecture\arch\instruction_set.hpp
OPCODE | OP1 | OP2 | OP2 | Description |
---|---|---|---|---|
MOV | Reg | Reg/Imm | OP1 = ZX(OP2) | |
MOVSX | Reg | Reg/Imm | OP1 = SX(OP2) | |
STR | Reg | Imm | Reg/Imm | [OP1+OP2] <= OP3 |
LDD | Reg | Reg | Imm | OP1 <= [OP2+OP3] |
NEG | Reg | OP1 = -OP1 | ||
ADD | Reg | Reg/Imm | OP1 = OP1 + OP2 | |
SUB | Reg | Reg/Imm | OP1 = OP1 - OP2 | |
MUL | Reg | Reg/Imm | OP1 = OP1 * OP2 | |
MULHI | Reg | Reg/Imm | OP1 = [OP1 * OP2]>>N | |
IMUL | Reg | Reg/Imm | OP1 = OP1 * OP2 (Signed) | |
IMULHI | Reg | Reg/Imm | OP1 = [OP1 * OP2]>>N (Signed) | |
DIV | Reg | Reg/Imm | Reg/Imm | OP1 = [OP2:OP1] / OP3 |
REM | Reg | Reg/Imm | Reg/Imm | OP1 = [OP2:OP1] % OP3 |
IDIV | Reg | Reg/Imm | Reg/Imm | OP1 = [OP2:OP1] / OP3 (Signed) |
IREM | Reg | Reg/Imm | Reg/Imm | OP1 = [OP2:OP1] % OP3 (Signed) |
POPCNT | Reg | OP1 = popcnt OP1 | ||
BSF | Reg | OP1 = OP1 ? BitScanForward OP1 + 1 : 0 | ||
BSR | Reg | OP1 = OP1 ? BitScanReverse OP1 + 1 : 0 | ||
NOT | Reg | OP1 = ~OP1 | ||
SHR | Reg | Reg/Imm | OP1 >>= OP2 | |
SHL | Reg | Reg/Imm | OP1 <<= OP2 | |
XOR | Reg | Reg/Imm | OP1 ^= OP2 | |
OR | Reg | Reg/Imm | OP1 |= OP2 | |
AND | Reg | Reg/Imm | OP1 &= OP2 | |
ROR | Reg | Reg/Imm | OP1 = (OP1>>OP2) | |
ROL | Reg | Reg/Imm | OP1 = (OP1<<OP2) | |
TG | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 > OP3 |
TGE | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 >= OP3 |
TE | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 == OP3 |
TNE | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 != OP3 |
TL | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 < OP3 |
TLE | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 <= OP3 |
TUG | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 u> OP3 |
TUGE | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 u>= OP3 |
TUL | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 u< OP3 |
TULE | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 u<= OP3 |
IFS | Reg | Reg/Imm | Reg/Imm | OP1 = OP2 ? OP3 : 0 |
JS | Reg | Reg/Imm | Reg/Imm | Jumps to OP1 ? OP2 : OP3, continues virtual execution |
JMP | Reg/Imm | Jumps to OP1, continues virtual execution | ||
VEXIT | Reg/Imm | Jumps to OP1, continues real execution | ||
VXCALL | Reg/Imm | Calls into OP1, pauses virtual execution until the call returns | ||
NOP | Placeholder | |||
SFENCE | Assumes all memory is read from | |||
LFENCE | Assumes all memory is written to | |||
VEMIT | Imm | Emits the opcode as is to the final instruction stream | ||
VPINR | Reg | Pins the register for read | ||
VPINW | Reg | Pins the register for write | ||
VPINRM | Reg | Imm | Imm | Pins the memory location for read, with size = OP3 |
VPINWM | Reg | Imm | Imm | Pins the memory location for write, with size = OP3 |
参考文章
https://github.com/vtil-project
https://github.com/can1357/NoVmp
https://github.com/0xnobody/vmpattack