項目簡介
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