1. 程式人生 > >如何分析虛擬機器(2):進階篇 VMProtect 2.13.8

如何分析虛擬機器(2):進階篇 VMProtect 2.13.8

原文網址:https://www.52pojie.cn/forum.php?mod=viewthread&tid=723307&page=1

 

序言

系列第1篇對一個極弱的虛擬機器 VMProtect 1.81 Demo 版進行了分析,初步展示了一下對虛擬機器保護程式碼的分析方法。

Demo 版因為程式碼沒有混淆處理,因此在 IDA 中可以分析的很清楚了,甚至還可以根據位元組碼(VM_DATA)一點點靜態還原虛擬指令。

然而正式版本的 VMProtect 虛擬機器是有比較嚴重的混淆的(實際是一種冗餘指令的新增),直接使用 IDA 分析十分困難,許多基本塊會截斷,也沒有第1篇中介紹的明顯的解釋迴圈圖結構。動態除錯也不方便,裡面大量的CALL\JMP,跳來跳去。ESI 還有指令的立即數等還有加密,整體複雜度有很大的提升。

對於這種情況如何處理呢?本文以 VMProtect 2.13.8 為例,展示如何在混淆比較嚴重的情況下找到虛擬機器關鍵結構、快速的分析 Handler,提取出虛擬指令。

本文主要介紹兩個關鍵的部分:

  • 通過 Trace 分析快速把握虛擬機器結構
  • 通過符號執行快速分析 Handler

VMProtect 2.13.8 樣本說明

這裡使用的樣本仍是第1篇中的程式碼加 VM 的。

sub_401000 proc near
mov     eax, dword_403000
add     eax, 12345678h
sub     eax, 12345678h
mov     dword_403000, eax
retn
sub_401000 endp

加密級別為最大速度,目前我們只討論虛擬機器本身,其他諸如IAT保護、反除錯等其他 VMP 保護選項都關閉,讓我們專注於虛擬機器的分析。

其實即使到了 2.13.8 版本, VMProcect 的整體結構仍是和 VMProtect 1.81 一致的。 因此雖然存在混淆,經驗豐富的人還是可以迅速找到關鍵的指令分發點(Dispatcher)位置,從而找到跳轉表,提取所有的 Handler。

如下指令(加*的部分)就是典型的 dispatcher 程式碼,根據 0x404cf8 跳轉表,再找到 ESI 解密的方式,就可以提取所有 Handler。

0x4041ff        mov edx, dword ptr [eax*4+0x404cf8]  ;*****
0x404206        cmc
0x404207        sub edx, 0x1aadee33
0x40420d        mov byte ptr [esp+0x8], 0x28
0x404212        pushad
0x404213        mov dword ptr [esp+0x40], edx        ; ******
0x404217        pushfd
0x404218        mov byte ptr [esp+0x4], ch
0x40421c        push dword ptr [esp+0x8]
0x404220        push dword ptr [esp+0x48]
0x404224        ret 0x4c                             ; ******

這是前人總結出來的特徵。如果是不瞭解 VMProtect 的人,分析起來就要困難一些。

如果我們對 VMProtect 並不十分清楚,直接面對 VMProtect 2.13.8 版本的樣本該如何處理?

虛擬機器結構分析

首先仍 IDA 開啟樣本,跳到 0x401000 處,看下前幾條指令:

.text:00401000                 jmp     loc_405DD8  ; 跳轉到 .vmp0 中的虛擬機器程式碼

.vmp0:00405DD8                 pushf
.vmp0:00405DD9                 call    sub_405CF7  ; call並不是函式呼叫,只是跳轉進入虛擬機器中

.vmp0:00405CF7                 mov     byte ptr [esp+0], 0A4h
.vmp0:00405CFB                 mov     dword ptr [esp+4], 95A00657h
.vmp0:00405D03                 mov     [esp+0], dl
.vmp0:00405D06                 mov     dword ptr [esp+0], 6B1DA363h
.vmp0:00405D0D                 pusha
.vmp0:00405D0E                 push    dword ptr [esp+8]
.vmp0:00405D12                 pusha
.vmp0:00405D13                 mov     [esp+20], al
.vmp0:00405D17                 lea     esp, [esp+44h] ; 前面壓棧的內容彈出來
.vmp0:00405D1B                 jmp     loc_4048F4

.vmp0:004048F4                 jmp     loc_40593D

...

其實這些程式碼並沒有什麼有用的操作,先向棧中壓入一些無用的資料,然後又通過 lea  esp, [esp+44h] 把棧頂降回來,相當於把壓入的資料彈出來,結果就是什麼都沒有做。同時使用了大量的 jmp 和 call 將程式碼切割成若干小塊,使 IDA 無法正常分析。

我們還是很喜歡第1篇中的 IDA 生成的控制流圖的。通過圖中明顯的解釋迴圈結構,可以很快的定位哪裡是 Dispatcher,哪裡是 Handler。但現在 jmp 和 call 干擾了 IDA 的分析,IDA 沒法生成完整的 CFG 圖。

為了恢復這個圖,我們還是使用 Trace 分析的方法。Trace 是 x86 指令執行的序列,Trace 中所有 jmp 和 call 會天然的和跳轉目標指令連線起來。Trace 分析其實是很強大的功能。單步執行時我們的注意力可能會被暫存器、記憶體的值所分散,而忽略了程式整體的執行情況。而通過對 Trace 的整體分析,則可以讓我們跳出區域性,從整體去觀察。

虛擬機器解釋執行的過程是一個迴圈:首先取指令,解碼,然後跳轉到 Handler 程式碼,執行完成後再跳轉回來。在 Trace 中我們如何捕捉這個關鍵的迴圈?我想到的方法是,由 Trace 構造一個像之前 IDA 顯示的 CFG 圖類似的圖。 通過找圖中的中心結點,來確定 dispatcher 的位置。

由 Trace 構造的圖,是反映了程式執行過程的基本塊圖,我們可以稱之為執行流圖。構造圖的方法說起來比較費力,直接看圖。


 

假設ABCD是執行的指令序列,按照每條指令在Trace中的先後順序,就可以構造一個圖。相鄰結點合併一下,就可以得到最終的執行流圖。最終 AB 形成一個塊,執行到 CDE 塊,再跳轉回來,再執行FG塊,再跳轉回來,再執行HI塊,再跳轉回來。整個執行的過程就很清楚了。通過寫指令碼分析 OD 的 Trace 檔案,就可以構造出這個圖,程式設計有基礎的同學實現一下並不困難。

我做過一個工具,具備生成這種圖的功能。不過為了 Trace 的效率,這個工具沒有使用 OD Trace,而是利用 Intel 的插裝工具 Pin 記錄指令序列。(這個工具還在寫,目前比較渣,不多介紹了。如果寫好了會單獨寫文章介紹。想嚐鮮可以自己下載下來編譯原始碼,傳送門在 https://github.com/lmy375/pinvmp )

下面是用這個工具分析 VMP 樣本生成的執行流圖。


 
 

(圖太大無法顯示清楚,看得清基本的結構就好,完整的SVG格式圖見附件)

這回已經非常直觀了。圖中間的結點跳轉到其他結點中再跳轉回來,是典型的 dispatcher 結構,並列成排的每個程式碼塊,就是 Handler 程式碼塊。

dispatcher 結點的程式碼如下:

0x40435f        lea edx, ptr [edx*8-0x7525bb7f]
0x404366        mov al, byte ptr [esi-0x1]     ; [esi - 0x1] 取指令
0x404369        inc dl
0x40436b        setns dh
0x40436e        dec esi
0x40436f        sub edx, 0x9ec1cfd2
0x404375        cmp dl, 0xea
0x404378        rcl dl, 0x7
0x40437b        sub al, bl  ; 解密 al
0x40437d        dec dl
0x40437f        push esi
0x404380        call 0x4041ce
0x4041ce        pop edx
0x4041cf        dec al      ; 解密 al
0x4041d1        rcl dh, cl
0x4041d3        sar edx, cl
0x4041d5        or dl, ch
0x4041d7        xor al, 0xcd  ; 解密 al
0x4041d9        lea edx, ptr [ebx*8-0xcbcbf63]
0x4041e0        sar dx, cl
0x4041e3        rcr edx, 0x10
0x4041e6        rol dx, cl
0x4041e9        sub al, 0x23
0x4041eb        shl dl, cl
0x4041ed        bsr dx, dx
0x4041f1        bt bx, 0x9
0x4041f6        sub bl, al
0x4041f8        rcl dh, 0x4
0x4041fb        movzx eax, al  ; eax <- al
0x4041fe        pushad
0x4041ff        mov edx, dword ptr [eax*4+0x404cf8]  ; 取跳轉地址, 跳轉表就是 0x404cf8
0x404206        cmc
0x404207        sub edx, 0x1aadee33  ; 解密跳轉地址
0x40420d        mov byte ptr [esp+0x8], 0x28
0x404212        pushad
0x404213        mov dword ptr [esp+0x40], edx  ; 跳轉地址壓棧
0x404217        pushfd
0x404218        mov byte ptr [esp+0x4], ch
0x40421c        push dword ptr [esp+0x8]
0x404220        push dword ptr [esp+0x48]
0x404224        ret 0x4c  ; 通過 ret 跳轉到前面壓棧的 edx

除了註釋外的程式碼,大多是混淆作用,不用關心。

Handler 分析

接下來看 Handler 程式碼。當然我們也可以一個一個人工分析 Handler 程式碼。因為 VMProtect 的混淆沒有程式碼變形,只是添加了很多的棧指令。

因為原本的程式碼沒變,人工分析也不困難,對於瞭解 VMP 的人,即使人工分析,分析每個Handler也不會太困難。

然而如果不瞭解 VMP Handler 的特徵,看這種混淆程式碼是很痛苦的。

這裡介紹一種利用符號執行分析 Handler 的方法。(參考:http://www.miasm.re/blog/2016/09/03/zeusvm_analysis.html

首先簡單說明一下什麼是符號執行。符號執行是將所有暫存器和記憶體當作符號變數,然後模擬執行所有語句。執行完畢後,每個暫存器和寫入的記憶體都會變成符號表達式。

比如如下程式碼

add eax, 1   ; eax = eax_init + 1
add ebx, eax ; ebx = ebx_init + eax 
sub ebx, eax ; ebx = ebx_init + eax - eax = ebx_init
add ebx, ecx ; ebx = ebx_init + ecx_init

記eax和ebx的值是eax_initebx_init。符號執行會將每條指令翻譯成對應的語義表示式。執行過程如註釋說明。

最終的結果

eax = eax_init + 1
ebx = ebx_init + ecx_init
ecx = ecx_init

說明執行了上述程式碼, eax 增加了初始值的 1;ebx 變成 ebx 初始值加 ecx;ecx 還是初始值不變。值得注意的是 ebx 加 eax 又減 eax 這個過程沒有顯示在最終的結果裡,符號執行引擎把結果化簡了。VMP 新增的指令就是這類不影響最終結果的指令,因此可以被符號執行引擎化簡掉

下面證明我們的想法。

使用 Miasm 這個符號執行引擎去分析混淆的程式碼。(Miasm:https://github.com/cea-sec/miasm

以如下的 Handler 程式碼塊為例:

0x40493f        shr al, 0x5
0x404942        sbb al, 0x8b
0x404944        stc
0x404945        movzx eax, byte ptr [esi-0x1]
0x404949        pushad
0x40494a        stc
0x40494b        xor al, bl
0x40494d        jmp 0x404b01
0x404b01        clc
0x404b02        clc
0x404b03        not al
0x404b05        pushad
0x404b06        cmc
0x404b07        xor al, 0xc1
0x404b09        clc
0x404b0a        mov dword ptr [esp], 0xba247870
0x404b11        jmp 0x405123
0x405123        ror al, 0x5
0x405126        pushfd
0x405127        call 0x4051f2
0x4051f2        cmp al, 0x15
0x4051f4        bt sp, bp
0x4051f8        xor bl, al
0x4051fa        clc
0x4051fb        sub ebp, 0x2
0x4051fe        mov byte ptr [esp], 0x64
0x405202        mov byte ptr [esp], 0x14
0x405206        pushfd
0x405207        jmp 0x405bde
0x405bde        dec esi
0x405bdf        pushfd
0x405be0        mov word ptr [ebp], ax
0x405be4        mov word ptr [esp], bp
0x405be8        mov byte ptr [esp], dl
0x405beb        lea esp, ptr [esp+0x50]
0x405bef        jmp 0x4059a4

我們把所有跳轉指令去掉,當作一個連續的程式碼塊。然後把每條指令的二進位制位元組提取出來,拼接在一起,十進位制表示如下:

c0e8051c8bf90fb646ff60f930d890f8f8f6d060f534c1f8c70424707824ba90c0c8059c682c5140003c15660fa3ec30c3f883ed02c6042464c60424149c904e9c6689450066892c248814248d64245090

之所以轉化成二進位制是為了方便使用 Miasm 進行符號執行(程式碼參考:https://github.com/lmy375/pinvmp/blob/master/py/symexec.py )

符號執行後,可以輸出如下結果:

EBP = EBP_init + 0xFFFFFFFE;
ESI = ESI_init + 0xFFFFFFFF;
@16[EBP_init + 0xFFFFFFFE] = {(@8[ESI_init + 0xFFFFFFFF] ^ EBX_init[0:8] ^ 0x3E) >>> 0x5 0 8, 0x0 8 16};
... (還有其他ESP記憶體相關的表示式,忽略)

上面的表示式是Miasm使用的表示式 其中@ 表示記憶體訪問。上面的表示式的含意就是,執行上面的Handler 後。

  • EBP 會減2(說明壓棧2位元組)
  • ESI 會減1(vEIP移動1位元組)
  • [ebp - 2]的2位元組,會寫入值 ([esi -1] ^ ebx ^ 0x3e) 再迴圈右移 0x5 的結果。(說明是取指令立即數,解碼後然後壓入棧中)

因此可以很容易的確定這條指令是 vPushImm1。

這樣分析起來就容易了非常多。(後面有時間會寫一篇利用符號執行處理 Code Virtualizer 混淆的例子,效果也十分明顯!)

很方便就可以分析出每條 Handler。

提取偽指令及偽指令分析

分析完每條 handler 後,就可以從 Trace 中取出 Handler 的呼叫序列。

方法和第1篇一樣,首先用 OD2 跑 Trace,再寫指令碼還原虛擬碼。

同樣的對於需要使用暫存器或者立即數的指令,需要額外處理一下。

比如前面的 vPushImm1 Handler,只要查詢 Trace 中0x405be0        mov word ptr [ebp], ax 中 ax 的值,就可以知道具體的立即數壓入的值是多少了。

而對於 push 和 pop 指令 我們這次除了從Trace中提取出暫存器下標,還提取出讀寫的暫存器值,方便我們分析虛擬碼。

VMProtect 2.13.8 相對之前的 Demo 版本,不但虛擬機器內部混淆有明顯增強,關鍵是虛擬碼級別變得更加難複雜了。(這其實才是 VMP 強度的關鍵)

其實虛擬碼也是可以利用前面介紹的符號執行方法進行分析,不過需要自行處理偽指令的語義(後面有空的話會專門寫東西講利用符號執行分析偽指令)

這裡簡單人肉分析一下:

// 這個程式碼塊人肉看會比較痛苦
// 我也沒有具體看
// 不過根據低版本的偽碼應該是對 eflag 中的 trap 位進行檢查的程式碼。
vPopReg4        R9      = 0x0
vPushImm4       0xc34f4e9
vAdd4
vPopReg4        R14     = 0x4046a7
vPopReg4        R4      = 0xc34f72f
vPopReg4        R5      = 0x404558
vPopReg4        R2      = 0x396622ad
vPopReg4        R6      = 0x401015
vPopReg4        R13     = 0x401015
vPopReg4        R7      = 0x307000
vPopReg4        R11     = 0x401015
vPopReg4        R10     = 0x401015
vPopReg4        R12     = 0x401015
vPopReg4        R14     = 0x19ff94
vPopReg4        R3      = 0x6b1da363
vPopReg4        R3      = 0x95a00657
vPushReg4       R5      = 0x404558
vPushImm4       0x2d47169f
vPushImm4       0x2d47150d
vPushImmSx2     0xfffffeff
vPushReg4       R5      = 0x404558
vPushVESP
vReadMemSs4
vNor4
vPopReg4        R8      = 0x203a21fc
vNor4
vPopReg4        R15     = 0x203a21fc
vPopReg4        R1      = 0x100
vPushVESP
vPushImm1       0x4
vPushReg4       R15     = 0x203a21fc
vPushVESP
vReadMemSs4
vNor4
vPopReg4        R8      = 0x203a21fc
vPushImmSx4     0xffffffbf
vNor4
vPopReg4        R5      = 0x203a21fc
vShr4
vPopReg4        R3      = 0x202
vAdd4
vPopReg4        R1      = 0x4046a7
vReadMemSs4
vPopReg4        R5      = 0x2d47169f
vPopReg4        R8      = 0x2d47150d
vPopReg4        R8      = 0x2d47169f
vPushReg4       R5      = 0x2d47169f
vPopReg4        R1      = 0x2d47169f
vPushReg4       R1      = 0x2d47169f
vPushReg4       R1      = 0x2d47169f
vNor4
vPopReg4        R3      = 0x203a21fc
vPushImm4       0xd2f8b4b8
vNor4
vPopReg4        R5      = 0x203a21fc
vPushImm4       0x2d074b47
vPushReg4       R1      = 0x2d47169f
vNor4
vPopReg4        R3      = 0x203a21fc
vNor4
vPopReg4        R3      = 0x203a21fc
vPopReg4        R3      = 0x405dd8

vPushReg4       R10     = 0x401015
vPushReg4       R7      = 0x307000
vPushReg4       R13     = 0x401015
vPushReg4       R10     = 0x401015
vPushReg4       R14     = 0x19ff94
vPushReg4       R5      = 0x203a21fc
vPushReg4       R11     = 0x401015
vPushReg4       R12     = 0x401015
vPushReg4       R2      = 0x396622ad
vPushReg4       R15     = 0x203a21fc
vPushReg4       R7      = 0x307000

vPushReg4       R4      = 0xc34f72f
vPushImm4       0xf3cb0b17
vAdd4
vPopReg4        R5      = 0x4046a7
vPushReg4       R9      = 0x0

vPushReg4       R3      = 0x405dd8 ; 跳轉目標
vJmp

// 初始化
vPopReg4        R14     = 0x0
vPushImm4       0xc34f4e9
vAdd4
vPopReg4        R10     = 0x4046a7
vPopReg4        R3      = 0xc34f72f
vPopReg4        R7      = 0x307000
vPopReg4        R8      = 0x203a21fc
vPopReg4        R12     = 0x396622ad
vPopReg4        R2      = 0x401015
vPopReg4        R9      = 0x401015
vPopReg4        R10     = 0x203a21fc

vPushReg4       R10     = 0x203a21fc
vPushReg4       R10     = 0x203a21fc
vNor4
vPopReg4        R6      = 0x203a21fc
vPushImm4       0xd2f8b4b8
vNor4
vPopReg4        R1      = 0x203a21fc
vPushImm4       0x2d074b47
vPushReg4       R10     = 0x203a21fc
vNor4
vPopReg4        R5      = 0x203a21fc
vNor4
vPopReg4        R6      = 0x203a21fc
vPopReg4        R6      = 0xd3d6abb
vPopReg4        R0      = 0x19ff94
vPopReg4        R15     = 0x401015
vPopReg4        R1      = 0x401015
vPopReg4        R13     = 0x307000
vPopReg4        R11     = 0x401015
vPopReg4        R5      = 0x404558
vPushReg4       R5      = 0x404558
vPushReg4       R5      = 0x404558
vNor4
vPopReg4        R8      = 0x203a21fc
vPushImmSx2     0x8ff
vNor4

// mov     eax, dword_403000
vPushImm4       0x403000
vReadMem4
vPopReg4        R8      = 0xdeadbeef
vPopReg4        R12     = 0x203a21fc

// add     eax, 12345678h
vPushImm4       0x12345678
vPushReg4       R8      = 0xdeadbeef
vAdd4
vPopReg4        R4      = 0x4046a7
vPopReg4        R11     = 0xf0e21567
vPopNULL4
vPushReg4       R0      = 0x19ff94
vPushReg4       R2      = 0x401015

// sub     eax, 12345678h
vPushImm4       0x12345678
vPushReg4       R11     = 0xf0e21567
vPushVESP
vReadMemSs4
vNor4
vPopReg4        R8      = 0x203a21fc
vAdd4
vPopReg4        R4      = 0x4046a7
vPushVESP
vReadMemSs4
vNor4
vPopReg4        R12     = 0x203a21fc
vPopReg4        R13     = 0xdeadbeef  ; result

// flag
vPushReg4       R4      = 0x4046a7
vPushVESP
vReadMemSs4
vNor4

// mov     dword_403000, eax
vPushReg4       R13     = 0xdeadbeef
vPushImm4       0x403000
vWriteMem4

// flag
vPopReg4        R11     = 0x203a21fc
vPushImmSx2     0xfffff7ea
vNor4
vPopReg4        R5      = 0x203a21fc
vPushReg4       R12     = 0x203a21fc
vPushReg4       R12     = 0x203a21fc
vNor4
vPopReg4        R6      = 0x203a21fc
vPushImmSx2     0x815
vNor4
vPopReg4        R11     = 0x203a21fc
vAdd4
vPopReg4        R8      = 0x4046a7
vPopReg4        R8      = 0x203a21ed

// ret
vPushReg4       R15     = 0x401015
vPushReg4       R9      = 0x401015
vPushReg4       R7      = 0x307000
vPushReg4       R1      = 0x401015
vPushReg4       R12     = 0x203a21fc
vPushReg4       R13     = 0xdeadbeef
vPushReg4       R8      = 0x203a21ed
vPushReg4       R15     = 0x401015
vPushReg4       R13     = 0xdeadbeef
vRet

總結

本篇介紹了利用 Trace 和符號執行分析 VMProtect 2.13.8 的方法。其實只是大概提下思路,想要完全自動化分析還有很長的路要走。

Trace 分析這種方法對混淆程式碼其實還是不錯的,動態執行資訊有了,比靜態看 IDA 要好很多。但這種方法也是有侷限的,首先Trace 檔案可能會非常大,處理起來很麻煩。另一方面 Trace 只記錄跑過的指令,沒有跑過的程式碼 Trace 是分析不到的。就像文中所述,這個VMP樣本使用 Trace 大概只能提取到 20 多個 Handler,因為樣本實際只用到了這麼多。

符號執行是個新東西。對於學術界可能不新了,但是開始出現好用的實用工具如 angr, miasm, triton 等也是近年的事。這種新方法如果應用到虛擬機器保護上也許會有奇效。

(文章涉及的樣本、IDB檔案、Trace檔案、部分指令碼見附件,密碼123456)

Moon
2018/04/08

(本文也發於看雪 https://bbs.pediy.com/thread-225803.htm )

vmp2.13.8.zip

 

2.14 MB, 下載次數: 85, 下載積分: 吾愛幣 -1 CB