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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM