一、tvm簡介
一句話概括就是騰訊自家的虛擬化加密殼。把騰訊的安全產品拉入 PE 工具,看到區段中有.tvm0那就沒跑了。
demo
這次還原用到的demo是前段時間游戲安全技術競賽的決賽附加題(https://gslab.qq.com/html/competition/2023/race-final.htm)一個非常好的demo,驅動基本上全vm了。
還要特別感謝這位大佬放出來的脫殼版(https://bbs.kanxue.com/thread-276892.htm),給我節省了許多驗證還原效果的時間。
二、資料
還原腳本項目地址:xx_tvm(https://pan.baidu.com/s/1gjmYQHXELELJwqhb-HJ9iA?pwd=ICEY)
文檔我也只說明了一些明顯的點,還是看代碼更加清晰。
然后給你的idapython安裝以下的庫:
import capstone import keystone import copy import unicorn
三、混淆
1400d302c : not r10 ,R10 <-- ffffffffffffffff 1400d302f : xchg rax, r10 ,RAX <-- ffffffffffffffff ,R10 <-- 0 1400d3031 : mov [rbp+var_s8], r10 1400d3035 : not rax ,RAX <-- 0 1400d3038 : xchg rax, r10
可優化成:
1400d3031 : mov [rbp+var_s8], rax
直接特征識別即可,請參考idapython/TVMunicornTrace.py .tvmFunTask.mabe_1()
1400d5b36 : xchg rax, r11 ,RAX <-- 14006024c ,R11 <-- 0 1400d5b38 : mov rax, [rbp+98h] 1400d5b3f : not rax ,RAX <-- fffffffebff9fdb3 1400d9148 : xchg rax, r11 ,RAX <-- 0 ,R11 <-- fffffffebff9fdb3 1400d914a : not r11 ,R11 <-- 14006024c
可優化成:
1400d5b38 : mov r11, [rbp+98h]
直接特征識別即可,請參考idapython/TVMunicornTrace.py .tvmFunTask.mabe_2()
1400d9156 : push r10 ,RSP <-- 1500 1400d9158 : lea r10, loc_1400E1D70+2 ,R10 <-- 1400e1d72 1400d915f : lea r10, [r10-0A812h] ,R10 <-- 1400d7560 1400d9166 : jmp r10 1400d7560 : pop r10 ,RSP <-- 1508 ,R10 <-- 0
可優化成:
1400d9156 : jmp 1400d7560 1400d7560 : nop
直接特征識別即可,請參考idapython/TVMunicornTrace.py .tvmFunTask.mabe_3_4()
4.(類似 3)
1400d7240 : push r10 ,RSP <-- 1500 1400d7242 : mov r10, 14011A470h ,R10 <-- 14011a470 1400d724c : pushfq ,RSP <-- 14f8 1400d724d : add r10, 0FFFFFFFFFFFBF569h ,R10 <-- 1400d99d9 ,RF <-- 3 1400d7254 : popfq ,RSP <-- 1500 ,RF <-- 12 1400d7255 : jmp r10 1400d99d9 : pop r10 ,RSP <-- 1508 ,R10 <-- 1638
可優化成:
1400d7240 : jmp 1400d99d9 1400d99d9 : nop
直接特征識別即可,請參考idapython/TVMunicornTrace.py .tvmFunTask.mabe_3_4()
去混淆前:

去混淆后:


四、TVM
虛擬機的大致架構如下,非常標準。

實際上tvm有多個handle分發器和多張handleTable,但是它們的作用、內容完全一致,所以我就只畫出一個handle分發器,下面也只講解一張handleTable。
如何進入虛擬機
使用unicorn 跟一次tvm入口到出口,腳本參考:idapython/TVMunicornTrace.py
初始rsp設置為 0x1800
入口:(已去混淆) 完整文件請查看trace_file/tvm入口到出口 去混淆.log
未去混淆的查看trace_file/tvm入口到出口 未去混淆.log
140086efa : call sub_1400085E8 ,RSP <-- 17f8 //這個函數被vm 1400085e8 : jmp sub_1400D2FF4 1400d2ff4 : lea rsp, [rsp-248h] ,RSP <-- 15b0 //開辟虛擬機棧空間 1400d2ffc : mov [rsp+10h], rbp //保存進入虛擬機時的寄存器狀態 1400d3001 : mov rbp, rsp ,RBP <-- 15b0 1400d3004 : pushfq ,RSP <-- 15a8 1400d3005 : pop [rbp+0h] ,RSP <-- 15b0 1400d3008 : mov [rbp+78h], r14 1400d3012 : mov [rbp+30h], rdx 1400d3022 : mov [rbp+50h], r9 1400d3031 : mov [rbp+8h], rax 1400d303f : mov [rbp+60h], r11 1400d3043 : jmp short loc_1400D3054 1400d305a : mov [rbp+18h], rbx 1400d305e : mov [rbp+40h], rsp 1400d3062 : mov [rbp+28h], rdi 1400d3066 : mov [rbp+70h], r13 1400d306f : mov [rbp+48h], r8 1400d3078 : mov [rbp+80h], r15 1400d3085 : mov [rbp+58h], r10 1400d308f : mov [rbp+38h], rsi 1400d3093 : mov [rbp+68h], r12 1400d3097 : jmp loc_1400D3145 1400d314c : mov [rbp+20h], rcx 1400d3156 : pushfq ,RSP <-- 15a8 1400d3157 : add qword ptr [rbp+40h], 248h #恢復成原來的棧頂 1400d315f : popfq ,RSP <-- 15b0 1400d3160 : lea r11, [rbp+90h] ,R11 <-- 1640 1400d3167 : push qword ptr [rbp+8] ,RSP <-- 15a8 1400d316a : pop qword ptr [r11] ,RSP <-- 15b0 1400d316d : push qword ptr [rbp+18h] ,RSP <-- 15a8 1400d3170 : pop qword ptr [r11+8] ,RSP <-- 15b0 1400d3174 : push qword ptr [rbp+20h] ,RSP <-- 15a8 1400d3177 : pop qword ptr [r11+10h] ,RSP <-- 15b0 1400d317b : push qword ptr [rbp+30h] ,RSP <-- 15a8 1400d317e : pop qword ptr [r11+18h] ,RSP <-- 15b0 1400d3182 : push qword ptr [rbp+40h] ,RSP <-- 15a8 1400d3185 : pop qword ptr [r11+20h] ,RSP <-- 15b0 1400d3189 : push qword ptr [rbp+10h] ,RSP <-- 15a8 1400d318c : jmp loc_1400D30DC 1400d30dd : pop qword ptr [r11+28h] ,RSP <-- 15b0 1400d30e1 : push qword ptr [rbp+38h] ,RSP <-- 15a8 1400d30e4 : pop qword ptr [r11+30h] ,RSP <-- 15b0 1400d30e8 : push qword ptr [rbp+28h] ,RSP <-- 15a8 1400d30eb : pop qword ptr [r11+38h] ,RSP <-- 15b0 1400d30ef : push qword ptr [rbp+48h] ,RSP <-- 15a8 1400d30f2 : pop qword ptr [r11+40h] ,RSP <-- 15b0 1400d30f6 : push qword ptr [rbp+50h] ,RSP <-- 15a8 1400d30f9 : pop qword ptr [r11+48h] ,RSP <-- 15b0 1400d30fd : push qword ptr [rbp+58h] ,RSP <-- 15a8 1400d3100 : pop qword ptr [r11+50h] ,RSP <-- 15b0 1400d3104 : push qword ptr [rbp+60h] ,RSP <-- 15a8 1400d3107 : pop qword ptr [r11+58h] ,RSP <-- 15b0 1400d310b : push qword ptr [rbp+68h] ,RSP <-- 15a8 1400d310e : pop qword ptr [r11+60h] ,RSP <-- 15b0 1400d3112 : push qword ptr [rbp+70h] ,RSP <-- 15a8 1400d3115 : pop qword ptr [r11+68h] ,RSP <-- 15b0 1400d3119 : push qword ptr [rbp+78h] ,RSP <-- 15a8 1400d311c : pop qword ptr [r11+70h] ,RSP <-- 15b0 1400d3120 : push qword ptr [rbp+80h] ,RSP <-- 15a8 1400d3126 : pop qword ptr [r11+78h] ,RSP <-- 15b0 1400d312a : push qword ptr [rbp+0] ,RSP <-- 15a8 1400d312d : jmp loc_1400D30A8 1400d30aa : pop qword ptr [r11+80h] ,RSP <-- 15b0 1400d30b1 : lea r11, byte_14006024B+1 ,R11 <-- 14006024c 1400d30b8 : lea r10, [rbp+88h] ,R10 <-- 1638 1400d30bf : lea rsp, [rsp-8] ,RSP <-- 15a8 1400d30c4 : mov [rsp], r10 1400d30c8 : lea rsp, [rsp-8] ,RSP <-- 15a0 1400d30cd : mov [rsp], r11 1400d30d1 : call sub_1400D5B02 ,RSP <-- 1598 1400d5b02 : lea rsp, [rsp-8] ,RSP <-- 1590 1400d5b07 : mov [rsp+8+var_8], rbx 1400d5b0b : lea rsp, [rsp-8] ,RSP <-- 1588 1400d5b10 : mov [rsp+10h+var_10], rsi 1400d5b14 : lea rsp, [rsp-8] ,RSP <-- 1580 1400d5b19 : mov [rsp+18h+var_18], rdi 1400d5b1d : lea rsp, [rsp-8] ,RSP <-- 1578 1400d5b22 : mov [rsp+20h+var_20], rbp 1400d5b26 : lea rsp, [rsp-8] ,RSP <-- 1570 1400d5b2b : mov [rsp+28h+var_28], r15 1400d5b2f : sub rsp, 68h ,RSP <-- 1508 ,RF <-- 12 1400d5b33 : mov rbp, rsp ,RBP <-- 1508 1400d5b38 : mov r11, [rbp+98h] ,R11 <-- V_RIP 1400d5b42 : jmp loc_1400D9146 1400d914f : mov r10, [rbp+0A0h] ,R10 <-- V_REG_p 1400d9156 : jmp loc_1400D7560 1400d756a : call loc_1400DA21F ,RSP <-- 1500 1400da21f : lea rsp, [rsp+8] ,RSP <-- 1508 1400da224 : lea r9, loc_1400E47B0 ,R9 <-- 1400e47b0 1400da22e : jmp loc_1400D8689 1400d868e : mov [rbp+0], r9 1400d8695 : jmp loc_1400DB455 1400db45f : mov [rbp+8], r11
從1400d2ffc到1400d315f,tvm第一次保存進入虛擬機前的寄存器狀態:
rsp = rbp = 15b0 [15b0](rbp+0) rflag [15b8](rbp+8) rax [15c0](rbp+10) rbp (原來的棧底) [15c8](rbp+18) rbx [15d0](rbp+20) rcx [15d8](rbp+28) rdi [15e0](rbp+30) rdx [15e8](rbp+38) rsi [15f0](rbp+40) rsp (原來的棧頂) [15f8](rbp+48) r8 [1600](rbp+50) r9 [1608](rbp+58) r10 [1610](rbp+60) r11 [1618](rbp+68) r12 [1620](rbp+70) r13 [1628](rbp+78) r14 [1630](rbp+80) r15
接下來是第二次保存進入虛擬機前的寄存器狀態:
1400d3160 : lea r11, [rbp+90h] ,R11 <-- 1640 #接著往棧上保存原始寄存器狀態
從1400d3160到1400d30aa,保存狀態如下:
[1640](r11+0) rax [1648](r11+8) rbx [1650](r11+10) rcx [1658](r11+18) rdx [1660](r11+20) rsp [1668](r11+28) rbp [1670](r11+30) rsi [1678](r11+38) rdi [1680](r11+40) r8 [1688](r11+48) r9 [1690](r11+50) r10 [1698](r11+58) r11 [16a0](r11+60) r12 [16a8](r11+68) r13 [16b0](r11+70) r14 [16b8](r11+78) r15 [16c0](r11+80) rflag
接下來保存 虛擬機指令起始點和虛擬機寄存器的起始指針。
1400d30b1 : lea r11, byte_14006024B+1 ,R11 <-- 14006024c //這個是V_RIP (即虛擬指令起始點) 1400d30b8 : lea r10, [rbp+88h] ,R10 <-- 1638 //這個是 V_REG_P 1400d30bf : lea rsp, [rsp-8] ,RSP <-- 15a8 1400d30c4 : mov [rsp], r10 1400d30c8 : lea rsp, [rsp-8] ,RSP <-- 15a0 1400d30cd : mov [rsp], r11
結構如下:
[15a0] V_RIP 虛擬指令起始點 [15a8] V_REG_P 1638(虛擬機寄存器)
14006024c這是當前函數的虛擬指令起始點,可以先記住,后面就知道為什么我這么說了。

接下來進入:
1400d30d1 : call sub_1400D5B02 ,RSP <-- 1598
進call,return地址入棧:
[1598] = return add(call sub_1400D5B02 下一行的地址 是int3)
這個函數,它也會保存一下寄存器狀態,但是沒啥用:
1400d5b02 : lea rsp, [rsp-8] ,RSP <-- 1590 1400d5b07 : mov [rsp+8+var_8], rbx 1400d5b0b : lea rsp, [rsp-8] ,RSP <-- 1588 1400d5b10 : mov [rsp+10h+var_10], rsi 1400d5b14 : lea rsp, [rsp-8] ,RSP <-- 1580 1400d5b19 : mov [rsp+18h+var_18], rdi 1400d5b1d : lea rsp, [rsp-8] ,RSP <-- 1578 1400d5b22 : mov [rsp+20h+var_20], rbp 1400d5b26 : lea rsp, [rsp-8] ,RSP <-- 1570 1400d5b2b : mov [rsp+28h+var_28], r15
保存的狀態如下:
[1570] r15 rbp = 15b0 [1578] rbp [1580] rdi [1588] rsi [1590] rbx
然后會把V_RIP換個位置;
1400d5b2f : sub rsp, 68h ,RSP <-- 1508 ,RF <-- 12 1400d5b33 : mov rbp, rsp ,RBP <-- 1508 1400d5b38 : mov r11, [rbp+98h] ,R11 <-- V_RIP 1400d5b42 : jmp loc_1400D9146 1400d914f : mov r10, [rbp+0A0h] ,R10 <-- V_REG_p 1400d9156 : jmp loc_1400D7560 1400d756a : call loc_1400DA21F ,RSP <-- 1500 1400da21f : lea rsp, [rsp+8] ,RSP <-- 1508 //進call又rsp+8,假裝是jmp 1400da224 : lea r9, loc_1400E47B0 ,R9 <-- 1400e47b0 //這是int3 指令的地址 1400da22e : jmp loc_1400D8689 1400d868e : mov [rbp+0], r9 1400d8695 : jmp loc_1400DB455 1400db45f : mov [rbp+8], r11 V_RIP 放到 [1510]
接下來就開始處理虛擬指令了。先把棧空間的格式整理一下:(這個地方非常重要)
虛擬機內RBP = RSP = 1508 ,R10 = V_REG_P [1508] int3指令指針 [1510] V_RIP //當前執行到的位置 ..... [1570] r15 rbp = 15b0 [1578] rbp [1580] rdi [1588] rsi [1590] rbx [1598] = return add(sub_1400D5B02 下一行的地址 是int3) [15a0] V_RIP //虛擬指令起始點,這是不會變的 [15a8] V_REG_P //1638(虛擬機寄存器),這是不會變的 [15b0](rbp+0) rflag //這里以下是進入虛擬機前的寄存器狀態 [15b8](rbp+8) rax [15c0](rbp+10) rbp (原來的棧底) [15c8](rbp+18) rbx [15d0](rbp+20) rcx [15d8](rbp+28) rdi [15e0](rbp+30) rdx [15e8](rbp+38) rsi [15f0](rbp+40) rsp (原來的棧頂) [15f8](rbp+48) r8 [1600](rbp+50) r9 [1608](rbp+58) r10 [1610](rbp+60) r11 [1618](rbp+68) r12 [1620](rbp+70) r13 [1628](rbp+78) r14 [1630](rbp+80) r15 [1638] UnKnow V_REG_P = r10 = 1638 //虛擬機的虛擬寄存器從 1638 開始往下都是 [1640](r11+0) rax //這里以下是進入虛擬機前的寄存器狀態 [1648](r11+8) rbx [1650](r11+10) rcx [1658](r11+18) rdx [1660](r11+20) rsp [1668](r11+28) rbp [1670](r11+30) rsi [1678](r11+38) rdi [1680](r11+40) r8 [1688](r11+48) r9 [1690](r11+50) r10 [1698](r11+58) r11 [16a0](r11+60) r12 [16a8](r11+68) r13 [16b0](r11+70) r14 [16b8](r11+78) r15 [16c0](r11+80) rflag
其實只需要記住 R10放著V_REG_P 和 [rbp+8] 放著 V_RIP 。
handle分發
這是一段 tvmhandle的分發(tvmopcode的處理方式):
1400d7234 : mov r9, [rbp+8] ,R9 <-- 14006024c 1400d7240 : jmp loc_1400D99D9 1400d99db : mov r8b, [r9] ,R8 <-- e8 1400d99de : xor r8b, 5Dh ,R8 <-- b5 ,RF <-- 82 1400d99e2 : mov rdx, 25E9ECA9BDE22AEAh ,RDX <-- 25e9eca9bde22aea 1400d99ec : not rdx ,RDX <-- da161356421dd515 1400d99ef : lea rdx, [r9+rdx] ,RDX <-- da1613578223d761 1400d99f3 : jmp loc_1400D86A6 1400d86a8 : mov r9, 0DA161356421DD513h ,R9 <-- da161356421dd513 1400d86b2 : not r9 ,R9 <-- 25e9eca9bde22aec 1400d86b5 : lea r9, [rdx+r9] ,R9 <-- 14006024d 1400d86bf : mov [rbp+8], r9 1400d86c9 : movzx r8, r8b 1400d86cd : sub r8, 1 ,R8 <-- b4 ,RF <-- 6 1400d86d1 : jmp loc_1400D7E10 1400d7e11 : cmp r8, 0C8h ,RF <-- 93 1400d7e18 : jnb loc_1400D9954 1400d7e1e : lea r9, word_1400DB5AA ,R9 <-- 1400db5aa 1400d7e25 : mov r8, [r9+r8*8] ,R8 <-- d60f2 1400d7e29 : lea r9, cs:140000000h ,R9 <-- 140000000 1400d7e30 : jmp loc_1400D6558 1400d6559 : add r8, r9 ,R8 <-- 1400d60f2 ,RF <-- 2 1400d655c : jmp r8 //進入handle
[rbp + 8] 是 V_RIP,放入r9,然后從[R9] 取出 tvmopcode 放入r8b 然后異或 0x5D,然后 r9 +1(上面是混淆過的,實際效果就是 + 1 )。
R8b - 1 如果大于0xC8,就跳轉到1400D9954,說明這是未知的tvmopcode,出錯。1400D9954是int3指令。
然后 handleTable 放入 R9,取表項 [R9 + R8 *8],即為這個handle的偏移,加上基址0x140000000,即為這個handle 的實際處理地址:1400d60f2,通過 jmp r8 跳轉過去。
(可以看到 取得第一個 tvmopcode 是 0xe8 ,和上面圖中的是一樣的)
小總結:
1.從 tvmopcode --> handle 的方式:
handleTableBase = 0x1400db5aa Dllbase = 0x140000000 handle = Dllbase + qword[(tvmopcode^0x5d - 1)*8 + handleTableBase]
注意,tvm有多張handleTable,但是里面的內容都是一樣的。所以拿到一張表就行了。
2.handle的個數:
從tvmopcode^0x5d - 1 < 0xC8可以推測一共有 0xc8 ( 0~0xc7 )個 tvmopcode,例:
handleTable的第 0x00 項: tvmopcode = (0x00 + 1) ^ 0x5d = 0x5c ...... handleTable的第 0x3c 項: tvmopcode = (0x3c + 1) ^ 0x5d = 0x60 ...... handleTable的第 0xc7 項: tvmopcode = (0xc7 + 1) ^ 0x5d = 0x95
導出所有handle
現在我們知道,tvm的handleTable有0xc8個有效項,我們就可以遍歷handleTable,并且靜態跟蹤出handle。看它是如何處理的:
導出handle代碼(idapython/TVMHandleOut.py)
補充:雖然handle有0xc8個有效項,但是很多是重復的,是留作拓展用的,真正有效的handle就 80個:
左邊 是 handle處理地址,右邊是 tvmopcode。
1400d9954 : 9a 9b 98 99 9c 9d e2 e3 e0 e1 e6 e7 e4 eb ec ed f2 f3 f0 f1 f6 f7 f5 fa fb f9 fe ff c2 c0 c1 c6 c7 c4 c5 c8 ce cf cc cd db d8 de df dc dd 23 26 27 24 2a 28 2e 2c 2d 32 33 30 31 36 37 34 35 3a 3b 38 39 3e 3f 3d 02 03 00 07 04 05 0a 0b 08 0e 0f 0d 12 13 10 17 14 15 1a 1b 18 1e 1f 1c 63 61 66 67 64 6b 69 6f 6c 71 76 77 74 7b 79 7e 7f 7c 53 50 55 5b 58 59 5e 5f 5c 1400d8eb1 : 5a 1400d5b5a : 54 1400d5cd8 : 57 1400db3d3 : 56 1400d761b : 51 1400da3e5 : 52 1400dabb0 : 4d 1400d8196 : 4c 1400d6c8c : 4f 1400d82fa : 4e 1400d6e7d : 49 1400d7172 : 48 1400d9374 : 4b 1400daa8e : 4a 1400db0f9 : 45 1400dae51 : 44 1400d8be2 : 47 1400d946b : 46 1400d995a : 41 1400daf90 : 40 1400d7a8a : 43 1400d5e10 : 42 1400d7258 : 7d 1400daecb : 78 1400d5d02 : 7a 1400d974c : 75 1400d7f26 : 70 1400d8e1b : 73 1400d8dbf : 72 1400d8362 : 6d 1400d6fc9 : 6e 1400d7c1f : 68 1400d63cd : 6a 1400d9abb : 65 1400d8431 : 60 1400d7797 : 62 1400da161 : 1d 1400d7313 : 19 1400d92b0 : 16 1400d9c5c : 11 1400dab1e : 0c 1400d86e2 : 09 1400d7352 : 06 1400dad5e : 01 1400d9524 : 3c 1400dad2c : 2f 1400d8b8d : 29 1400d7d39 : 2b 1400d8f7b : 25 1400d696f : 21 1400d71f5 : 20 1400d71b3 : 22 1400d770e : d9 1400da48c : da 1400d74d2 : d5 1400d70ee : d4 1400d95ad : d7 1400d6f6c : d6 1400d84d0 : d1 1400da357 : d0 1400d78f9 : d3 1400d7998 : d2 1400d8561 : c9 1400d624c : cb 1400d9169 : ca 1400d64ac : c3 1400d5c7b : fd 1400d9dd5 : fc 1400d6913 : f8 1400dafcf : f4 1400d7bc7 : ef 1400d7fc4 : ee 1400d9446 : e9 1400d60f2 : e8 1400d69e8 : ea 1400d9584 : e5 1400da10c : 9f 1400d8e89 : 9e 1400d8cdd : 95
并在在 當前文件夾/handleout 文件夾內,輸出全部handle靜態跟蹤(運行腳本的同時會對handle去簡單的混淆)
:(剛好80個不相同的handle,文件名用 tvmopcode)

一共有80個不同功能的tvmopcode,為節省篇幅,我這里挑一個常見的講解,全部的handle分析,其余的全部放在(handle_out/)。
(那個特別多的是int3,應該是預留以后更新用的)
我靜態跟蹤handle是以 jnb 為結尾的(就是判斷是否大于0xc8后的jnb),所以跟蹤文件后半段有一些不用看。
0xe8 p_a b_ULONG64 0x3F26 *(PULONG64)p_a = b_ULONG64; v_mov_iregll_ll ----------------------------------------//虛線以上是分析出來的 0x1400d60f5 : mov r9, [rbp+8] 0x1400d6102 : mov r8w, [r9] 這里取2字節 (取虛擬寄存器都是取2字節) 0x1400d6106 : xor r8w, 3F26h 異或 3F26(不同的handl異或的值不同) 得到 a, 0x1400d610c : mov rdx, 0F84A86395161A270h 所以就是取虛擬機寄存器 V_REG_P + a 0x1400d6116 : not rdx 0x1400d6119 : jmp loc_1400D9077 0x1400d9078 : lea rdx, [r10+rdx] 0x1400d907c : movzx r8, r8w 0x1400d9080 : mov rcx, 7B579C6AE9E5D8Eh 0x1400d908a : not rcx 0x1400d908d : add r8, rcx 0x1400d9090 : lea r8, [rdx+r8] p_a = r8 = V_REG_P + a 0x1400d9094 : lea r9, [r9+2] V_RIP+=2 0x1400d909b : mov rdx, [r9] b_ULONG64 從tvm指令表中取8字節 0x1400d909e : jmp loc_1400DA2DD 0x1400da2ed : mov [r8], rdx 放入 [r8],即放入 p_a 0x1400da2f0 : jmp loc_1400DA9AC 0x1400da9b4 : lea r9, [r9+8] V_RIP+=8 0x1400da9be : mov [rbp+8], r9 V_RIP放回[rbp+8] 0x1400da9c8 : jmp loc_1400DAC79 0x1400dac7c : mov r9, [rbp+8] 這里以下屬于下一個handle分發,不用看 0x1400dac89 : mov r8b, [r9] 0x1400dac8c : xor r8b, 5Dh 0x1400dac90 : mov rdx, 0D3676A56DAFF3C65h 0x1400dac9a : jmp loc_1400D7763 0x1400d7764 : not rdx 0x1400d7767 : lea rdx, [r9+rdx] 0x1400d776b : mov r9, 2C9895A92500C398h 0x1400d7775 : not r9 0x1400d7778 : lea r9, [rdx+r9] 0x1400d777f : jmp loc_1400D5B91 0x1400d5b95 : mov [rbp+8], r9 0x1400d5b9e : movzx r8, r8b 0x1400d5ba2 : sub r8, 1; switch 200 cases 0x1400d5ba6 : jmp loc_1400D98A8 0x1400d98aa : cmp r8, 0C8h 0x1400d98b1 : jnb def_1400D655C;
我對 tvmAsm的命名規則:
v_opcode_op0_op1_op2_....._opn 例如: v_mov_iregw_iregw : 從虛擬寄存器op1 取 2字節 放入虛擬寄存器op0 w 表示 USHORT v_mov_ipreg_iregl : 從虛擬機寄存器op1 取4字節,放入以虛擬機寄存器op0作為地址的空間,有點類似與: mov [reg],reg v_mov_iregb_b : 從tvm指令表中取1字節放入虛擬機寄存器op0中,類似于Asm中的立即數賦值: mov reg,0xff v_and_oregl_iregl_iregl_oregl:(i 就是 in ,o 就是 out 的意思) 取虛擬寄存器op1 和 虛擬寄存器op2進行與運算,并將結果放入虛擬寄存器op0,與運算后的 rflag 放入 虛擬寄存器op3 l 表示 ULONG32 v_cmp_iregll_iregll_oregl: 即為 cmp 虛擬寄存器op0,虛擬寄存器op1 ,cmp后的rflag 放入 虛擬寄存器op2 ll 表示 ULONG64
我這里直接將全部tvmAsm展示出來:(參考idapython/deTvm.py . tvmHandleTableInit())
TVMTABEL.append("v_sar_oregll_iregll_iregb_oregl",0x01,0xF8BE)
TVMTABEL.append("v_or_oregll_iregll_iregll_oregl",0x1D,0x4AA7)
TVMTABEL.append("v_mov_iregll_iregl",0x2B,0xBF3E)
TVMTABEL.append("v_movzx_iregl_iregb",0x2F,0x7EE9)
TVMTABEL.append("v_ror_oregb_iregb_iregb_oregl",0x3C,0xF8E1)
TVMTABEL.append("v_mov_iregl_iregl",0x4A,0x564B)
TVMTABEL.append("v_mov_iregw_iregw",0x4B,0xD916)
TVMTABEL.append("v_add_oregb_iregb_iregb_oregl",0x4C,0xDD9D)
TVMTABEL.append("v_add_oregll_iregll_iregll",0x4D,0x477D)
TVMTABEL.append("v_add_oregl_iregl_iregl_oregl",0x4E,0xA9C7)
TVMTABEL.append("v_add_oregw_iregw_iregw_oregl",0x4F,0x82BC)
TVMTABEL.append("v_sub_oregl_iregl_iregl_oregl",0x5A,0xC198)
TVMTABEL.append("v_sar_oregl_iregl_iregb_oregl",0x06,0x8374)
TVMTABEL.append("v_and_oregl_iregl_iregl_oregl",0x6A,0x5CF0)
TVMTABEL.append("v_and_oregll_iregll_iregll_oregl",0x6D,0xD9B1)
TVMTABEL.append("v_and_oregl_iregl_iregl",0x6E,0xA1CE)
TVMTABEL.append("v_xor_oregl_iregl_iregl_oregl",0x7A,0xD8ED)
TVMTABEL.append("v_mov_ipreg_iregll",0x7D,0xD878)
TVMTABEL.append("v_shr_oregll_iregll_iregb_oregl",0x09,0x9D87)
TVMTABEL.append("v_int3",[0x9a,0x9b,0x98,0x99,0x9c,0x9d,0xe2,0xe3,0xe0,0xe1,0xe6,0xe7,0xe4,0xeb,0xec,0xed,0xf2,0xf3,0xf0,0xf1,0xf6,0xf7,0xf5,0xfa,0xfb,0xf9,0xfe,0xff,0xc2,0xc0,0xc1,0xc6,0xc7,0xc4,0xc5,0xc8,0xce,0xcf,0xcc,0xcd,0xdb,0xd8,0xde,0xdf,0xdc,0xdd,0x23,0x26,0x27,0x24,0x2a,0x28,0x2e,0x2c,0x2d,0x32,0x33,0x30,0x31,0x36,0x37,0x34,0x35,0x3a,0x3b,0x38,0x39,0x3e,0x3f,0x3d,0x2,0x3,0x0,0x7,0x4,0x5,0xa,0xb,0x8,0xe,0xf,0xd,0x12,0x13,0x10,0x17,0x14,0x15,0x1a,0x1b,0x18,0x1e,0x1f,0x1c,0x63,0x61,0x66,0x67,0x64,0x6b,0x69,0x6f,0x6c,0x71,0x76,0x77,0x74,0x7b,0x79,0x7e,0x7f,0x7c,0x53,0x50,0x55,0x5b,0x58,0x59,0x5e,0x5f,0x5c],0x0000)
TVMTABEL.append("v_jmp_iregxR11",0x9E,0x0AD7)
TVMTABEL.append("v_jmp_iregxR10",0x9F,0x2E72)
TVMTABEL.append("v_shl_oregll_iregll_iregb_oregl",0x11,0x5403)
TVMTABEL.append("v_shl_oregl_iregl_iregb_oregl",0x16,0xEEF7)
TVMTABEL.append("v_not_oregll_iregll",0x19,0x1400)
TVMTABEL.append("v_setz_oregb_iregl",0x20,0x0D45)
TVMTABEL.append("v_movsxd_iregll_iregl",0x21,0x8BC8)
TVMTABEL.append("v_setR8d_iregl",0x22,0x77D7)
TVMTABEL.append("v_movsx_iregl_iregb",0x25,0xD8E4)
TVMTABEL.append("v_movzx_iregl_iregw",0x29,0xF10A)
TVMTABEL.append("v_mov_ipreg_iregb",0x40,0xE304)
TVMTABEL.append("v_mov_iregll_ipreg",0x41,0xE229)
TVMTABEL.append("v_mov_ipreg_iregl",0x42,0x5431)
TVMTABEL.append("v_mov_ipreg_iregw",0x43,0x02CB)
TVMTABEL.append("v_mov_iregb_ipreg",0x44,0xBE8C)
TVMTABEL.append("v_mov_iregll_iregll",0x45,0x58FB)
TVMTABEL.append("v_mov_iregl_ipreg",0x46,0x10BC)
TVMTABEL.append("v_mov_iregw_ipreg",0x47,0x6F62)
TVMTABEL.append("v_mov_iregb_iregb",0x48,0xCFFE)
TVMTABEL.append("v_add_oregll_iregll_iregll_oregl",0x49,0x41AA)
TVMTABEL.append("v_not_oregll_iregll",0x51,0xDB42)
TVMTABEL.append("v_add_oregl_iregl_iregl",0x52,0x77C6)
TVMTABEL.append("v_not_oregb_iregb",0x54,0xDCF3)
TVMTABEL.append("v_not_oregl_iregl",0x56,0xE297)
TVMTABEL.append("v_not_oregw_iregw",0x57,0x666D)
TVMTABEL.append("v_or_oregb_iregb_iregb_oregl",0x60,0xFBFD)
TVMTABEL.append("v_or_oregl_iregl_iregl_oregl",0x62,0x7819)
TVMTABEL.append("v_and_oregll_iregll_iregll_oregl",0x65,0x2954)
TVMTABEL.append("v_and_oregb_iregb_iregb_oregl",0x68,0xD8A5)
TVMTABEL.append("v_and_oregb_iregb_iregb_oregl",0x70,0x64D1)
TVMTABEL.append("v_and_oregl_iregl_iregl_oregl",0x72,0x4A64)
TVMTABEL.append("v_and_oregw_iregw_iregw_oregl",0x73,0xB562)
TVMTABEL.append("v_xor_oregll_iregll_iregll_oregl",0x75,0x69C2)
TVMTABEL.append("v_xor_oregb_iregb_iregb_oregl",0x78,0x19C1)
TVMTABEL.append("v_ret_iregx",0x95,0x805C)
TVMTABEL.append("v_shr_oregb_iregb_iregb_oregl",0x0c,0x62D7)
TVMTABEL.append("v_dec_oregl_iregl_oregl",0xc3,0x467C)
TVMTABEL.append("v_inc_oregb_iregb_oregl",0xc9,0x4267)
TVMTABEL.append("v_inc_oregll_iregll_oregl",0xca,0x6EE0)
TVMTABEL.append("v_inc_oregl_iregl_oregl",0xcb,0x7FB4)
TVMTABEL.append("v_test_iregw_iregw_oregl",0xd0,0x0499)
TVMTABEL.append("v_test_iregb_iregb_oregl",0xd1,0x2A99)
TVMTABEL.append("v_test_iregll_iregll_oregl",0xd2,0x8606)
TVMTABEL.append("v_test_iregl_iregl_oregl",0xd3,0x7FDE)
TVMTABEL.append("v_cmp_iregw_iregw_oregl",0xd4,0x87DF)
TVMTABEL.append("v_cmp_iregb_iregb_oregl",0xd5,0x3728)
TVMTABEL.append("v_cmp_iregll_iregll_oregl",0xd6,0x637D)
TVMTABEL.append("v_cmp_iregl_iregl_oregl",0xd7,0xCBEF)
TVMTABEL.append("v_sbb_oregb_iregb_iregb_oregl",0xd9,0x3F78)
TVMTABEL.append("v_sbb_oregll_iregll_iregll_oregl",0xda,0x0E0D)
TVMTABEL.append("v_jmp_iregxRax",0xe5,0xCD84)
TVMTABEL.append("v_mov_iregll_ll",0xe8,0x3F26)
TVMTABEL.append("v_mov_iregl_l",0xe9,0x448A)
TVMTABEL.append("v_mov_iregll_ll",0xea,0x43EF)
TVMTABEL.append("v_mov_iregw_w",0xee,0x44B1)
TVMTABEL.append("v_mov_iregb_b",0xef,0x144C)
TVMTABEL.append("v_mul_oregll_oregll_iregll_iregll",0xf4,0xEB97)
TVMTABEL.append("v_jmp_ll",0xf8,0x0000)
TVMTABEL.append("v_rep stosb_iregll_iregb_iregll",0xfc,0x8D54)
TVMTABEL.append("v_je_iregb_ll_ll",0xfd,0xDE5E)
解釋:
TVMTABEL.append 第一個參數是我給tvmAsm取的名字,第二個參數就是 tvmopcode,第三個參數是:如果這個handle要取虛擬寄存器,就必須通過 這個值解密取得虛擬寄存器,就像是我上文中解釋的:
TVMTABEL.append("v_mov_iregll_ll",0xe8,0x3F26
這里再挑幾個特殊說明一下:
TVMTABEL.append("v_jmp_ll",0xf8,0x0000):
因為有一些指令,tvm并沒有對其進行模擬,所以需要臨時退出虛擬機,然后執行那種指令,再返回虛擬機。(下文細說)
TVMTABEL.append("v_je_iregb_ll_ll",0xfd,0xDE5E)
當虛擬機寄存器op0為0x1時,V_RIP + op1 否則 V_RIP + op2,以實現虛擬機內的跳轉
TVMTABEL.append("v_ret_iregx",0x95,0x805C)
這是退出虛擬機的tvmasm,具體實現是恢復原始寄存器,然后通過ret退出虛擬機。
TVMTABEL.append("v_jmp_iregxR11",0x9E,0x0AD7)
TVMTABEL.append("v_jmp_iregxR10",0x9F,0x2E72)
TVMTABEL.append("v_jmp_iregxRax",0xe5,0xCD84)
這三個tvmasm也是恢復原始寄存器,但是是通過jmp 跳出虛擬機 jmp r11、jmp r10、jmp rax
TVMTABEL.append("v_mul_oregll_oregll_iregll_iregll",0xf4,0xEB97)
乘法,虛擬機寄存器op2 * 虛擬機寄存器op3
結果高64位放入 虛擬機寄存器op0 ,低64位放入 虛擬機寄存器op1
把tvm的跟蹤規則寫好后,就可以跟蹤導出這個函數的虛擬化控制流:
跟蹤參考
idapython/deTvm.py . traceTask.track()
獲取 tvmAsmTrace:
這是我挑的一個短的函數:0x140001250參考trace_file/sub_0x140001250.log
使用函數traceTask.track(0) + traceTask.traceOut(),輸出如下:

可能會好奇這些 PO_reg 怎么來的,其實這是我對 traceCode的優化:
XXREGNAME = {"PO_rax":0x08,
"PO_rbx":0x10,
"PO_rcx":0x18,
"PO_rdx":0x20,
"PO_rsp":0x28,
"PO_rbp":0x30,
"PO_rsi":0x38,
"PO_rdi":0x40,
"PO_r8":0x48,
"PO_r9":0x50,
"PO_r10":0x58,
"PO_r11":0x60,
"PO_r12":0x68,
"PO_r13":0x70,
"PO_r14":0x78,
"PO_r15":0x80,
"PO_rf":0x88}
正常的取 虛擬機寄存器 都是 [r10 + xxx],r10 就是 V_REG_P,前面說過了,看下面這一段(前面也出現過)。
[1638] UnKnow V_REG_P = r10 = 1638 //虛擬機的虛擬寄存器從 1638 開始往下都是 [1640](r11+0) rax //這里以下是進入虛擬機前的寄存器狀態 [1648](r11+8) rbx [1650](r11+10) rcx [1658](r11+18) rdx [1660](r11+20) rsp [1668](r11+28) rbp [1670](r11+30) rsi [1678](r11+38) rdi [1680](r11+40) r8 [1688](r11+48) r9 [1690](r11+50) r10 [1698](r11+58) r11 [16a0](r11+60) r12 [16a8](r11+68) r13 [16b0](r11+70) r14 [16b8](r11+78) r15 [16c0](r11+80) rflag
例如 PO_r8 其實就是 [r10 + 0x48]
所以 tvmAsm對 PO_reg 操作 可以理解為對虛擬機外的真實寄存器操作。
tvmAsm to Asm
這里我使用了 標記working + 賦值表記錄 的方法,將所有有意義的 tvmAsm找出來。
先說哪種tvmAsm會被標記為 working:(標記為working表明至少可以還原出一條原始Asm)
v_mov_iregll_iregll ( iregll :PO_rsp ,iregll :[ r10 + 0xa8 ] ); 第一個參數必須為 原始寄存器 PO_reg 這一句 ,會將 [r10 + 0xa8]的值放入 PO_rsp , 那么我們就可以推測,原Asm 可能為 mov rsp,xxx (也不一定對,但至少可以還原出一條Asm) v_mov_ipreg_iregll ( iregll :PO_rsp ,iregll :[ r10 + 0xa8 ] ); 同理可以推測 原Asm 為 mov qword ptr[rsp],xxx(也不一定對,但至少可以還原出一條Asm) v_jmp_ll ( ll :0x1400c7588 ); 上文說過,tvm并不能模擬全部指令,有一些指令需要臨時退出虛擬機去執行,所以遇到這種指令,必然可以還原出一條Asm v_ret_iregx、v_jmp_iregxR11、v_jmp_iregxR10、v_jmp_iregxRax 上文說過,這幾個指令可以直接還原成 ret、jmp r11、jmp r10、jmp rax v_je_iregb_ll_ll 這個可以還原出 jcc + jmp (下文詳解) v_rep stosb_iregll_iregb_iregll 這個可以還原出 rep stosb ,不用管參數 v_test_iregx_iregx_oregl 可以還原成 test v_cmp_iregx_iregx_oregl 可以還原成 cmp
只要標記好這幾個點,就能還原出全部的Asm。參考(idapython/deTvm.py . traceTask.track())
那么我們標記好后,這段 tvm指令就如下:

箭頭指著的就是標記為working的TraceCode。那么接下來要干嘛,就很清晰了,把相關的traceCode找出來(變量溯源)。
例子1:
這一句被標記為working,我們找他使用的參數的賦值語句,直到 找到整數 或 PO_reg
140059e46 : v_mov_iregll_iregll ( iregll :PO_rsp ,iregll :[ r10 + 0xa8 ] );<-----
我們把 [r10 + 0xa8]的賦值語句找出來(往上找):
140059e28 : v_and_oregll_iregll_iregll_oregl ( oregll :[ r10 + 0xa8 ] ,iregll :[ r10 + 0xb8 ] ,iregll :[ r10 + 0xb8 ] ,oregl :PO_rf );
這一句用到了 [r10 + 0xb8],找它的賦值語句:
140059e23 : v_not_oregll_iregll ( oregll :[ r10 + 0xb8 ] ,iregll :[ r10 + 0xb8 ] );
又是 [r10 + 0xb8],再往上找:
140059e07 : v_add_oregll_iregll_iregll_oregl ( oregll :[ r10 + 0xb8 ] ,iregll :[ r10 + 0xb0 ] ,iregll :[ r10 + 0xa0 ] ,oregl :PO_rf );
用到 [r10 + 0xb0] 和 [r10 + 0xa0],往上找:
[r10 + 0xb0]: 140059e02 : v_not_oregll_iregll ( oregll :[ r10 + 0xb0 ] ,iregll :[ r10 + 0xa8 ] ); [r10 + 0xa0]: 140059df7 : v_mov_iregll_ll ( iregll :[ r10 + 0xa0 ] ,ll :0x28 );
[r10 + 0xa0]找到盡頭了,[r10 + 0xb0]還沒找到盡頭,繼續網上找[ r10 + 0xa8 ]的賦值語句:
140059df2 : v_mov_iregll_iregll ( iregll :[ r10 + 0xa8 ] ,iregll :PO_rsp );
找到盡頭,是將 PO_rsp 放入。我們把這些 traceCode放在一起:
參考idapython/deTvm.py . traceTask.VRegRecord()

這樣也有點不好看,用變量傳播優化一下:
參考idapython/deTvm.py . tvmToAsm.optimize()

這一些 traceCode,就可以還原出一句 Asm:(以下我們對這一段可還原成Asm的TraceCode集合統稱為一個 tvmToAsm 結構)
[r10 + 0xb8] = ~(~rsp + 0x28) = rsp - 0x28 //前五句結合 [r10 + 0xa8] = [r10 + 0xb8] & [r10 + 0xb8] = [r10 + 0xb8] //沒變化
于是這一段就可以還原成:
sub rsp,0x28
為什么是sub rsp,0x28而不是lea rsp,[rsp - 0x28]是有講究的:
看traceCode中的一句v_add_oregll_iregll_iregll_oregl,他是有輸出 rflag的,并且放入的位置就是 PO_rf,說明這一句ASM是會影響標志位,而lea是不影響標志位的,所以將它還原成sub。
手動還原 Asm
我們順勢對這個函數的所有被標記為working 的traceCode進行變量溯源+優化,那么最后就可以得到:tvmToAsmAll。
一個函數內的所有 tvmToAsm 組成一個 tvmToAsmAll ,下圖中一段一段的就是 tvmToAsm。

看,真正有效的就這一些,其余的都可以看作花指令。所以這個函數的原始ASM就是:(手動還原)
sub rsp,0x28 mov rcx,0x1400011a0 call sub_140005E38 #下文解釋為什么 V_jmp_ll(0x1400c7588) 可以轉換成 這個 mov byte ptr[0x14000d264],0x0 add rsp,0x28 ret
關于v_jmp_ll
上文說過,tvm不能模擬全部的Asm,所以有些Asm需要暫時退出虛擬機執行,然后再返回虛擬機:
我們到0x1400c7588,然后往下跟(中間是還原真實寄存器),直到出現 mov rsp,[rsp] ,之后的下一句就是真實需要執行的ASM了。即為 call sub_140005E38。
接下來會重新進入虛擬機,步驟和上文進入虛擬機的步驟大致相同。

所以 v_jmp_ll 是最容易還原成 Asm 之一的 tvmAsm了。
參考idapython/deTvm.py . tvmToAsm.vjmp_handle()
補充:
關于如何找到workingTraceCode的相關traceCode,我的方案如下:(如果你有其他方案,可以不用看這一段)
把全部traceCode的賦值語句找出來,然后給相關虛擬機寄存器添加賦值記錄,形成一張賦值表,例如:
140059dc2 : v_mov_iregb_b ( iregb :[ r10 + 0x90 ] ,b :0x6 ); 140059dc6 : v_mov_iregll_ll ( iregll :[ r10 + 0x98 ] ,ll :0x1 ); 140059dd1 : v_mov_iregll_ll ( iregll :[ r10 + 0xa0 ] ,ll :0x1 ); 140059ddc : v_mov_iregll_ll ( iregll :[ r10 + 0xa8 ] ,ll :0x600 ); 140059de7 : v_mov_iregll_ll ( iregll :[ r10 + 0x0 ] ,ll :0x140001250 ); 140059df2 : v_mov_iregll_iregll ( iregll :[ r10 + 0xa8 ] ,iregll :PO_rsp ); 140059df7 : v_mov_iregll_ll ( iregll :[ r10 + 0xa0 ] ,ll :0x28 ); 140059e02 : v_not_oregll_iregll ( oregll :[ r10 + 0xb0 ] ,iregll :[ r10 + 0xa8 ] ); 140059e07 : v_add_oregll_iregll_iregll_oregl ( oregll :[ r10 + 0xb8 ] ,iregll :[ r10 + 0xb0 ] ,iregll :[ r10 + 0xa0 ] ,oregl :PO_rf );
我就可以得到這么一張表:

traceTaskRegTable為總賦值表, traceTaskRegList為單個虛擬機寄存器的賦值棧, traceTaskReg為入棧的單個記錄。 相關定義參考:idapython/deTvm.py 的三個類,命名同上
當需要檢索這一句traceCode的相關traceCode時:
140059e02 : v_not_oregll_iregll ( oregll :[ r10 + 0xb0 ] ,iregll :[ r10 + 0xa8 ] );
就可以直接查表,先找到[ r10 + 0xa8 ]的賦值記錄棧traceTaskRegList,然后通過地址找到最近的一次賦值traceTaskReg,然后traceTaskReg記錄了這一句traceCode,就可以找到了。
于是就找到了相關traceCode:
140059df2 : v_mov_iregll_iregll ( iregll :[ r10 + 0xa8 ] ,iregll :PO_rsp );
因為是將 PO_reg 賦值給它,所以到此檢索完畢,如果不是,則按照相同的方法繼續往上找。
相關代碼參考:idapython/deTvm.py . traceTask.VRegRecord()
一些特殊的例子
例如有兩個連續的 tvmToAsm,導出的相關traceCode如下:

我們可以發現,有兩段相關traceCode是相同的,這就出現問題了:
第一個tvmToAsm可以翻譯成xor edi,edi,那么第二個tvmToAsm能翻譯成什么呢?
如果我們人為識別,就可以將其翻譯成mov ecx,edi,因為[r10 + 450]在前面已經放入了PO_rdi,下面又取它放入PO_rcx。
于是我們可以進行優化,如果 有一句v_mov_iregx_iregx(PO_reg , xxxx)那么我們就可以將xxxx的上一次賦值標記為PO_reg。
還是以上面的那一段代碼為例:
v_mov_iregll_iregll ( iregll:PO_rdi ,iregll:[ r10 + 0x450 ] ); <----------------
[r10 + 0x450]上一次賦值語句為:
v_mov_iregll_iregl ( iregll:[ r10 + 0x450 ],iregl :[ r10 + 0x98 ] )
我們可以將其標記為PO_rdi,那么當其他的workingTraceCode向上進行查找賦值表的時候,就可以找到PO_rdi,于是就優化為了:

實現代碼參考:idapython/deTvm.py . traceTask.VRegRecord()
關于push和pop
push:

這是兩個連續的 tvmToAsm,
如果按照一般的分析方式,那么這兩句可以翻譯為:
mov qword ptr[rsp - 0x8],r13 lea rsp,[rsp - 8]
乍一看沒什么問題,就是將push r13分開成兩句執行,可是如果是pop,那么情況就有點不同了:
pop:

如果按照一般的分析方式,那么這兩句可以翻譯為:
lea rsp,[rsp + 8] mov r13,qword ptr[rsp]
這就出問題了,這兩句并不等于pop r13指令,那么問題出在哪呢,我們取消掉變量傳播優化再看看:

問題就出在,這兩句 workingTraceCode在進行變量溯源時,都找到了0x14004dca7這一句,
將 PO_rsp 放入虛擬機寄存器 [r10 + 0xa8],并且在第一句workingTraceCode中,又更改了 PO_rsp的值,所以導致出錯。
幸運的是這種情況只會出現在 push 和 pop 中(參考idapython/deTvm.py . tvmToAsmAll.outerror()),
所以我們要對 push 和 pop 特殊處理,請參考:idapython/deTvm.py . tvmToAsmAll.findPushAndPop()
特殊處理,優化后的 push 和 pop:
push:

pop:

這就清晰很多了。
關于 jc
前置知識:
各個標志位的位置:

JO jmp if OF = 1 JNO jmp if OF = 0 JB JC JNAE jmp if CF = 1 JNB JNC JAE jmp if CF = 0 JZ JE jmp if ZF = 1 JNZ JNZ jmp if ZF = 0 JBE JNA jmp if CF = 1 or ZF = 1 JNBE JA jmp if CF = 0 and ZF = 0 JS jmp if SF = 1 JNS jmp if SF = 0 JP JPE jmp if PF = 1 JNP JPO jmp if PF = 0 JL JNGE jmp if SF != OF JNL JGE jmp if SF = OF JLE JNG jmp if ZF = 1 or SF != OF JNLE JG jmp if ZF = 0 and SF = OF
tvm巧妙的利用了and sub setz je 這四種指令模擬一個jcc,例如:
JE:

(v_je 的跳轉是通過 加減 V_RIP 實現的,我這里直接優化成 絕對地址,省了我們去計算)
上面的代碼,先保留 rflag 的zf位,然后再減去zf位,如果結果為0,那么 V_RIP 就變成 v_je 的第二個操作數0x14003c274否則 V_RIP變為0x14003c1fe。
于是,上面這段tvmasm可以翻譯為:
je xxx(V_RIP = 0x14003c274) jmp xxx(V_RIP = 0x14003c1fe)
JG

第一個tvmToAsm:
如果 zf = sf = of = 0,則V_RIP =0x1400369ec,否則 V_RIP =0x140036ac6(其實就是下面那一段,因為優化了所以地址對不上)。
第二個tvmToAsm:
如果 zf = 0 且 sf = of = 1,則V_RIP =0x1400369ec,否則 VRIP =0x1400368ef。
于是,上面這段tvmasm可以翻譯為:
jg xxx(V_RIP = 0x1400369ec) jmp xxx(V_RIP = 0x1400368ef)
提取特征
JE 的特征 0x40 0x40 (看上圖你就知道是什么特征了)
JG 的特征 0x8c0 0x0 0x8c0 0x880(看上圖你就知道是什么特征了)
全部的jcc特征:(代碼參考idapython/deTvm.py . tvmToAsm.vjcc_handle())
JBE JNA 0x41 0x1 0x41 0x40 (這其實是錯誤的,tvm的bug?) JGE JNL 0x880 0x0 0x880 0x880 JL JNGE 0x880 0x800 0x880 0x80 JG JNLE 0x8C0 0x0 0x8C0 0x880 JZ JE 0x40 0x40 JNZ JNE 0x40 0x0 JS 0x80 0x80 JNS 0x80 0x0 JC JB JNAE 0x1 0x1 JNC JNB JAE 0x1 0x0 JA JNBE 0x41 0x0 JP JPE 0x4 0x4 0x0 0x0 JNP JPO 0x4 0x0 JO 0x800 0x800 JNO 0x800 0x0 JLE JNG 0x8C0 0x40 zf 0x8C0 0x800 of 0x8C0 0x80 sf 0x8C0 0xC0 zf+sf 0x8C0 0x840 zf+of 0x8C0 0x8C0 zf+of+s
上面我說JBE JNA的特征是0x41 0x1 0x41 0x40這其實是錯誤的:
因為 這只是jmp if ZF != CF,真正的JBE JNA是jmp if CF = 1 or ZF = 1,
對應的特征應該是0x41 0x1 0x41 0x40 0x41 0x41,在這個版本的 tvm 中 ,它將JBE JNA錯誤處理成了jmp if ZF != CF
我逆較新版本的 tvm ,JBE JNA這里的bug就被修復了,就是0x41 0x1 0x41 0x40 0x41 0x41。
(看來ACE部門用的tvm版本不夠新啊)
腳本還原ASM
參考idapython/deTvm.py . tvmToAsmAll.AllTvmAsmToAsm()
第一步,先識別所有的AsmOpcode:
push 和 pop 在上文已經識別出來了,
jcc 在上文也識別出來了,
tvm沒有模擬的Asm,也可以通過跟蹤 v_jmp_ll 得到,上文也說了。
一些明顯的 tvmAsm也可以直接識別原本的AsmOpcode:
asmOpcode = [
"sar","or","ror","xor","shr","shl","movsxd",
"movsx","movzx","dec","inc","test",
"cmp","rep stosb","sbb","int3"
如果tvmToAsm中的traceCode的tvmAsm含有以上的字符串,
那么可以直接將這個tvmtoAsm的AsmOpcode設置為對應項,例如:

可以直接將Asm的Opcode 設置成 xor,如果要翻譯成Asm的話,就翻譯成了xor edx,edx。
如果tvmToAsm只出現了 and,沒有出現 not add 那么可以識別成 and 如果tvmToAsm只出現了 not,沒有出現 and add 那么可以識別成 not
以上部分,參考代碼:idapython/deTvm.py . tvmToAsm.setASMOpcode_1()
識別 lea mov add sub ,這部分比較復雜,如果是人為識別就簡單。
我們先對每一個tvmToAsm內的traceCode再進行一次變量分析,列出一個賦值表,類似于上文賦值表的結構。
除此之外,我們還要對其進行標記設置,例如:
sub:

標記的結構:[handle,tvmPara,IsUseRflag],依次是處理手段、虛擬機寄存器、是否使用(輸出)標志位
14004a4c3 : 將 [r10 + 0xb0] 標記為 [not,PO_rsp,False]
14004a4c8 : 將 [r10 + 0xb8] 標記為 [not,PO_rsp,False],[add,0x80,True] //會拷貝前一個的標記
14004a4e4 : 將 [r10 + 0xb8] 標記為 [not,PO_rsp,False],[add,0x80,True],[not,None,False]
-優化-> [None,PO_rsp,False],[sub,0x80,True] //會拷貝前一個的標記
14004a4e9 : 將 [r10 + 0x98] 標記為 [None,PO_rsp,False],[sub,0x80,True] //會拷貝前一個的標記
有了這些標記,再加上14004a507這一句指令,我們就可以識別這個tvmToAsm的AsmOpcode了:
出現 sub ,且影響標志位,所以為 sub
add:

1400627da : [r10 + 0xa8] 標記 [None,PO_r9,False],[add,0x9,False] 1400627e1 : 同上 1400627e6 : [r10 + 0x91] 標記 [None,PO_r9,False],[add,0x9,False],[mem,None,False] 1400627eb : [r10 + 0x90] 標記 [None,PO_r9,False],[add,0x9,False],[mem,None,False],[add,PO_rax,True] 1400627ff : [r10 + 0xa8] 標記 [None,PO_r9,False],[add,0x9,False] 140062806 : 同上
有了這些標記,再加上14006280b這一句指令,我們就可以識別這個tvmToAsm的AsmOpcode了:
出現了 add 標記,并且影響標志位,只能是 add
lea:

14004a825 : [r10 + 0xa0] 標記 [None,PO_rsp,False],[add,0x50,False]
有了這些標記,再加上14004a82c這一句指令,我們就可以識別這個tvmToAsm的AsmOpcode了:
標記中出現了add,且不影響標志位,就只能是 lea
mov:

140062826 : [r10 + 0x98] 標記 [None,PO_r9,False],[add,0x9,False] 14006282d : 同上 140062832 : [r10 + 0x90] 標記 [None,PO_r9,False],[add,0x9,False],[mem,None,False]
有了這些標記,再加上140062837這一句指令,我們就可以識別這個tvmToAsm的AsmOpcode了:
標記中出現了 add,但不影響標志位,并且出現了 mem ,那么只能是 mov
以上部分都只是一些簡單例子,關于更加嚴格的sub add lea mov分類,參考代碼:
idapython/deTvm.py . tvmToAsm.record_tage()負責跟蹤標記
idapython/deTvm.py . tvmToAsm.setASMOpcode_2()負責分類sub add lea mov
第二步,操作數識別:
到這里的時候,全部 tvmToAsm 的 AsmOpcode都已經全部識別。
首先介紹一個工具函數 :GetAsmPara可以將標記轉換成Asm操作數的格式。
mov:
例子和上圖是一樣的。轉換為標記如下:
140062826 : [r10 + 0x98] 標記 [None,PO_r9,False],[add,0x9,False] 14006282d : 同上 140062832 : [r10 + 0x90] 標記 [None,PO_r9,False],[add,0x9,False],[mem,None,False]
看到140062837這一條workingTraceCode:
v_mov_iregb_iregb ( iregb :PO_rax ,iregb :[ r10 + 0x90 ] );
看到第二個參數的類型為 iregb,是8bit寬的操作數。 GetAsmPara( PO_rax ) 返回 "al" GetAsmPara( [r10 + 0x90]) : 通過標記[None,PO_r9,False],[add,0x9,False],[mem,None,False], 返回 "[r9 + 0x9]" 我們知道這個tvmToAsm的AsmOpcode為mov,且workingTraceCode的 第二個參數類型為 iregb, 那么我們就可以知道:第一個操作數為 al ,第二個操作數為 byte ptr[r9 + 0x9] 于是這個tvmToAsm就可以翻譯為: mov al,byte ptr[r9 + 0x9]
代碼參考:idapython/deTvm.py . tvmToAsm.mov_handle()
lea:
例子和上圖是一樣的。轉換為標記如下:
14004a825 : [r10 + 0xa0] 標記 [None,PO_rsp,False],[add,0x50,False]
看到14004a82c這一條workingTraceCode:
v_mov_iregll_iregll ( iregll :PO_r8 ,iregll :[ r10 + 0xa0 ] );
GetAsmPara( PO_r8 ) 返回 "r8" GetAsmPara( [r10 + 0xa0 ]) : 通過標記[None,PO_rsp,False],[add,0x50,False] 返回 "rsp + 0x50" //沒有中括號,標記中出現mem才會有中括號,lea要后面自己加中括號 我們知道這個tvmToAsm的AsmOpcode為lea,且workingTraceCode的 第二個參數類型為 iregll, 那么我們就可以知道:第一個操作數為 r8 ,第二個操作數為 [rsp + 0x50] 于是這個tvmToAsm就可以翻譯為: lea r8,[rsp + 0x50]
代碼參考:idapython/deTvm.py . tvmToAsm.lea_handle()
add sub:
用上面add的例子,但是進行GetAsmPara的是紅框框起來的這兩個:(對于sub的處理是一樣的)

1400627ff : [r10 + 0xa8] 標記 [None,PO_r9,False],[add,0x9,False] 140062806 : 同上
對紅框的兩個參數進行GetAsmPara:
看類型 iregb ,8bit寬度 GetAsmPara( PO_rax ) 返回 "al" GetAsmPara( [r10 + 0xa8] ): 標記為:[None,PO_r9,False],[add,0x9,False] 因為 [r10 + 0xa8]類型為 ipreg,且8bit寬度 所以返回 "byte ptr[r9 + 0x9]" 于是這個tvmToAsm就可以翻譯為: add byte ptr[r9 + 0x9],al
代碼參考:idapython/deTvm.py . tvmToAsm.add_sub_handle()
以下都是 對 紅框框框起來的 tvmPara 進行GetAsmPara
push:

代碼參考:idapython/deTvm.py . tvmToAsm.push_handle()
pop:

代碼參考:idapython/deTvm.py . tvmToAsm.pop_handle()
cmp test:
cmp:

test:

代碼參考:idapython/deTvm.py . tvmToAsm.cmp_test_handle()
movzx movsx movsxd
舉例如果 opcode是movzx:

其余的一樣,取 opcode 對應的 第二個參數,然后 workingTraceCode的第一個參數進行GetAsmPara。
代碼參考:idapython/deTvm.py . tvmToAsm.movzx_movsx_movsxd_handle()
not
取 tvmAsmOpcode內含有 "not"字符串的那一句traceCode,取第二個參數進行GetAsmPara。
代碼參考:idapython/deTvm.py . tvmToAsm.not_handle()
xor sar shr shl sbb ror or and
取 tvmAsmOpcode含有 上述opcode 的traceCode,取第三個參數進行GetAsmPara。
取workingTraceCode的第一個參數進行GetAsmPara,以xor為例:

代碼參考:idapython/deTvm.py . tvmToAsm.xxx_handle()
jcc
上面我們已經獲取了 jcc的類型,和它的兩個跳轉地址,雖然都是 V_RIP,
我們對這兩個V_RIP往下找第一個workingTrace,它所在的tvmToAsm就是對應的跳轉地址,
我們可以給這個地址的的Asm打上跳轉目的地標簽,例如:

可以翻譯為:
...... je lable1 jmp label2 ...... label2: ;(0x14003c0e6往下找第一個tvmToAsm) mov rax,1 ;只是舉個例子 ...... label1: ;(0x14003c13b往下找第一個tvmToAsm) mov rcx,2 ;只是舉個例子 .....
參考代碼:idapython/deTvm.py . tvmToAsm.jcc_handle()
ASM 2 HE
這部分略,比較簡單,就是通過keystone庫函數將Asm編譯成十六進制機器碼,然后再創建一個段,把內存寫進去。詳情參考:idapython/deTvm.py . tvmToAsmAll.WriteHex()和main0函數。
值得一提的是,我發現了keystone的一個bug,你們可以試一下:
import keystone
ASM2HEX = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_64)
asm = """
mov rcx,qword ptr ds:[0x14000d250]
"""
byte,con = ASM2HEX.asm(asm,addr=0x1400ef00a)
for by in byte:
print("%02x"%by,end="")
輸出的結果是:
48 8b 0d 50 d2 00 40
將其轉換成 ASM,是
1400ef00a mov rcx, qword ptr [rip + 0x4000d250]
這明顯是錯誤的,0x1400ef00a+0x7+0x4000d250 != 0x14000d250
asm = """ mov qword ptr ds:[0x14000d250],rcx """ byte,con = ASM2HEX.asm(asm,addr=0x1400ef00a)
就能輸出正確的字節碼。
所以我采用的方法是一句一句將Asm轉換成HEX,如果遇到mov reg,qword ptr[xxx],
就把格式改成mov reg,qword ptr[rip + yyy],詳情參考代碼:
idapython/deTvm.py . Asm.AsmToHex()
五、deTvm.py腳本玩法
main0是對全部ida識別的函數進行特征分析,如果符合tvm函數特征,就對它進行還原。
可以算是一鍵還原全部tvm函數了,有可能有些tvm函數不符合特征,你也可以手動添加還原函數。
例如:
你知道一個函數0x140001250它是被vm的,那么你這么寫,腳本就會自動特征識別V_RIP。
testTrace = traceTask(0x140001250,tvm0base) #tvm0base是tvm0段的起始地址 testTrace.track(0) #開始跟蹤 得到traceCode testTrace.traceOut() #輸出原始traceCode
如果你這個被vm的函數不符合我寫的特征,但它確實是tvm的函數,那么可以這么寫,自己設置V_RIP:
#上一個例子的 函數 0x140001250 它的 V_RIP 就是 0x140059dc2 testTrace = traceTask(0,tvm0base) testTrace.VStart = 0x140059dc2 #自己找這個vm函數的起始地址 例如V_RIP = 0x140059dc2 testTrace.track(0) #開始跟蹤 得到traceCode testTrace.traceOut() #輸出原始traceCode
traceOut(0)輸出的結果如下:( 基本沒做處理)

如果想看 對標記working的traceCode進行變量溯源的結果,你可以這么寫:
testTrace = traceTask(0x140001250,tvm0base) #tvm0base是tvm0段的起始地址 testTrace.track(0) #開始跟蹤 得到traceCode testTrace.VRegRecord(True) #如果是False就是不使用標記(上文說過) testTrace.tvmToAsmAll.printAll() #輸出
輸出:

如果想進一步的進行變量傳播優化還有 push、pop 優化,可以這么寫:
testTrace = traceTask(0x140001250,tvm0base) #tvm0base是tvm0段的起始地址 testTrace.track(0) #開始跟蹤 得到traceCode testTrace.VRegRecord(True) #如果是False就是不使用標記(上文說過) testTrace.tvmToAsmAll.optimizeAll() #變量傳播優化,push、pop優化 testTrace.tvmToAsmAll.printAll() #輸出
輸出:

如果想看還原成 ASM是什么樣的,可以這樣寫:
testTrace = traceTask(0x140001250,tvm0base) #tvm0base是tvm0段的起始地址 testTrace.track(0) #開始跟蹤 得到traceCode testTrace.VRegRecord(True) #如果是False就是不使用標記(上文說過) testTrace.tvmToAsmAll.optimizeAll() #變量傳播優化,push、pop優化 testTrace.tvmToAsmAll.AllTvmAsmToAsm() #轉換成ASM 注意,一定要VRegRecord + optimizeAll 后才可以調用 testTrace.tvmToAsmAll.printAsmAll() #輸出ASM
輸出:

如果想看 tvmToAsm和Asm對應起來的輸出,可以這樣寫:
testTrace = traceTask(0x140001250,tvm0base) #tvm0base是tvm0段的起始地址
testTrace.track(0) #開始跟蹤 得到traceCode
testTrace.VRegRecord(True) #如果是False就是不使用標記(上文說過)
testTrace.tvmToAsmAll.optimizeAll() #變量傳播優化,push、pop優化
testTrace.tvmToAsmAll.AllTvmAsmToAsm() #轉換成ASM
tvmToAsm_P = testTrace.tvmToAsmAll.tvmToAsmHead #結構為tvmToAsm
while (tvmToAsm_P != None):
tvmToAsm_P.printAsm() #輸出Asm
tvmToAsm_P.print() #輸出traceCodeAll
print("") #隔開
tvmToAsm_P = tvmToAsm_P.BLink #下一個
輸出:

六、還原例子
總所周知 ACE-BASE.sys 的DriverUnload函數是被vm了的,那么我們就用它來看看還原效果:

不錯,很符合我對DriverUnload的想象。
左邊 命名為icxxx的函數均為還原成功的函數。
還原腳本項目地址:xx_tvm
七、一些補充
這個腳本只適用于這個版本的 tvm (ACE用的版本)。
新一點的tvm,雖然用的是同一套虛擬化指令集,但它會對整數進行加密,讀取時進行簡單的xor解密,并且新增了一些 虛擬指令。
而且 進入虛擬機的特征也有點不一樣,不過對腳本進行簡單的修改即可兼容。
看雪學苑
中國網絡空間安全協會
安全圈
中國信息安全
betasec
看雪學苑
威脅棱鏡
看雪學苑
HACK之道
一顆小胡椒
看雪學苑
betasec