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 指令,并通过 VMENTERVMEXITVJMP 指令对控制流进行跟踪

使用递归下降的方法,以 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://docs.vtil.org/

https://github.com/vtil-project

https://github.com/can1357/NoVmp

https://github.com/0xnobody/vmpattack

https://back.engineering/17/05/2021/

https://bbs.pediy.com/thread-266206.htm

posted @ 2021-06-18 14:31  Byaidu  阅读(187)  评论(0编辑  收藏  举报