站在巨人的肩膀上學習ctf vm

0x00 前言

本文提到的vm是ctf裏的vm,最近vm還是很熱門的,最近的虎符,de1ctf,再到網鼎杯都有vm的身影,但是vm的知識在網上挺散的(我只找到了綠盟那篇比較系統),vm的wp也相對比較簡單,所以身爲小白的我就在這裏歸納一下網上大佬們的文章,和我的思考和學習總結,希望可以給各位看官一些幫助。

參考文章:

虛擬機保護逆向入門

虛擬機保護技術淺談

《加密與解密》第四版 P592 ~P593

0x01 簡單結構

《加密與解密》“中說ctf_vm不是VMware,,它類似於 P-CODE ,將一系列指令解釋成bytecode 後放到一個解釋引擎中執行,從而對軟件進行保護。”簡單來說,就是先設立一個一個不重複的標誌(一個字節就是一個標誌),然後每個標誌通過分發器(dispatcher),尋找到對應的解釋(handler),之後把這些標誌通過不同的排列組合,並把組合後的標誌存到一個固定的地方(這個大數組就是opcode),而組合後的邏輯與將保護程序邏輯相同,實現替換,從而進行保護。

一下是圖解:

說明:在ctf_vm中 分發器(dispatcher) 與解釋器 (handler)通常是用switch和case實現的,通常有如下結構

switch(opcode)
{
    case 1: // handler1
        ......
    break;

    case 2: // handler2
        ......
    break;
 
    case 3: // handler3
        ......
    break;
}

爲了更好的理解,我們來看一道類似vm思想的題(僞虛擬機)南郵的一道ctf

本文着重看vm保護,其他本文不再贅述。進入vm_protect。

一般分爲兩個部分,在分發器之前(紅框),會先對一些輔助的值進行賦值(這個下文具體講解),第二部分(紫框)就是具體的分發,和每一個handler,

在這道僞虛擬機

1)handler的含義非常好理解,所以也沒有什麼必要去理解紅框裏輔助的值,

2)本道題所保護的flag算法只是最簡單的+ - * /,所以沒必要通過opcode的順序自己來還原(15000個也不可能自己還原)

小結:本題並沒有逆向虛擬機最重要的東西(翻譯handler含義,翻譯所保護代碼的含義),放在這是爲了初步認識ctf_vm的結構。

放上師傅寫的腳本(拿idapython跑一下):

adr1 = 0x6010C0
adr2 = 0x601060
flag =[]

for i in range(0,24*4,4):
    print "%x"%Dword(adr2+i)
    flag.append(Dword(adr2+i))

for i in range(14997,-1,-3):
    a = Byte(adr1+i)
    b = Byte(adr1+i+1)
    c = Byte(adr1+i+2)
    if(a == 1):
        flag[b] -= c
        flag[b] = flag[b]&0xff
    if(a == 2):
        flag[b] += c
        flag[b] = flag[b] & 0xff
    if(a == 3):
        flag[b] ^= c
        flag[b] = flag[b] & 0xff
    if(a == 4):
        flag[b] /= c
        flag[b] = flag[b] & 0xff
    if(a == 5):
        flag[b] ^= flag[c]
        flag[b] = flag[b] & 0xff
print(''.join(map(chr,flag)))

flag: nctf{Embr4ce_Vm_j0in_R3}  嘿嘿嘿,很簡單吧,我們來進入下一個階段。

0x02 網鼎_2020

具體的wp可以看這位師傅寫的:網鼎杯2020 僞虛擬機逆向 wp

咱們直接看vm_protect的部分:

 v10 = 0;
  v9 = 0;
  v8 = 0;
  v7 = 0;
  v6 = 0;
  while ( 1 )
  {
    result = v10;
    if ( v10 >= size )
      return result;
    switch ( opcode[v10] )
    {
      case 1:
        v4[v7] = v5;
        ++v10;
        ++v7;
        ++v9;
        break;
      case 2:
        v5 = opcode[v10 + 1] + flag[v9];
        v10 += 2;
        break;
      case 3:
        v5 = flag[v9] - LOBYTE(opcode[v10 + 1]);
        v10 += 2;
        break;
      case 4:
        v5 = opcode[v10 + 1] ^ flag[v9];
        v10 += 2;
        break;
      case 5:
        v5 = opcode[v10 + 1] * flag[v9];
        v10 += 2;
        break;
      case 6:
        ++v10;
        break;
      case 7:
        if ( v4[v8] != opcode[v10 + 1] )
        {
          printf("what a shame...");
          exit(0);
        }
        ++v8;
        v10 += 2;
        break;
      case 8:
        flag[v6] = v5;
        ++v10;
        ++v6;
        break;
      case 10:
        read(flag);
        ++v10;
        break;
      case 11:
        v5 = flag[v9] - 1;
        ++v10;
        break;
      case 12:
        v5 = flag[v9] + 1;
        ++v10;
        break;
      default:
        continue;
    }
  }
}

這是道比上一到正經一些的僞虛擬機的題:(爲啥說是“僞”,它沒有棧,沒有調度)

1)博主一般拿到vm的題,通常會大膽猜一下輔助參數(博主喜歡這麼叫,hhh)的含義(這題沒啥說的,下文具體講述)

      

先瞄一眼是參數還是數組,再通過每一個每一個case和調試大致推出其中的意思(真正的棧虛擬機會複雜的多)
 

v10 --- opcode的index

v9 v6----- flag 的 index

v8  v7 -----v4 的index

v4 估計是最後比較的數組或是參與算法的數組

2)推測出輔助參數的意義後,我們就可以去分析每一個case的含義,此題,非常明顯直接告訴咱了(棧虛擬機可就要肝一會兒了)

3)之後就需要在拿到的一大堆opcode裏區分出,哪些是一個handler(case組成),哪些是真實操作的數據,哪些是調度(這道題裏貌似沒有這個)。這就需要IDA動態調試(https://bbs.pediy.com/thread-247830-1.htm)。

真實的數據:case裏沒有的 ; 在調試時,dispatcher每次分配時跳過的,或者一堆循環,一大片相同的操作,估計就是對數組的操作,那肯定就有真實數據嘛。

區分每一個handler:這個如果題目沒有特別的暗示,就需要動調,找出一些case組合起來的一些結構,會有參考意義。

例如這題:4, 0x10, 3, 5, 1, 這樣類似的一套就是一個handler

調度:,多數就是順着來的,頂多是跳轉,也幾乎是順着來的。(也有可能是我菜,沒見過(手動狗頭))

4)之後確定這些handler,數據,(調度)之後,就要結合動調,分析,整理出所保護的程序(有的題就開始讀彙編了)

(此題具體的wp就去看那位師傅寫的吧.)

這道題也可以用angr一把梭出來,會在別的博客裏寫道。

噠噠!現在已經對ctf_vm的基礎框架有了一些瞭解,下面我們來見識真正的棧虛擬機逆向。

IDA動調連接不上問題:

有時候,在ida動調的時候會出現:參數,ip啥的,都對但就是 長時間響應不了連接不上的問題,如下解決:

果斷把虛擬機關上然後

等它還原完就可以連接了。

0x03 棧虛擬機

這就是真正ctf裏有難度的虛擬機了。整體的架構和上文的題相差不多,但因爲有了棧的加入,就會使題目多了“億”點點細節。

在看具體ctf題之前,我們不妨正向來了解一下虛擬機保護技術https://www.cnblogs.com/findumars/p/5636492.html

虛擬機(CPU)的體系架構可分爲3種,基於堆棧的(Stack based),基於寄存器的(Register based)和3地址機器。我們只講述基於堆棧的虛擬機架構(Stack based);這種架構的虛擬機需要頻繁操作堆棧,其使用的虛擬寄存器(虛擬的eax、ebx等等)保存在堆棧中;每個原子指令的handler都需要push、pop。

ctf通常的.exe程序,是基於寄存器,堆棧一般只是在函數傳遞參數時使用,而在vm保護中用來替代所保護程序的程序,則是基於堆棧的虛擬機架構,所以,在我們還原case,還原handler或者在還原程序邏輯的時候,要始終貫徹用棧操作的思想

既然棧操作,就少不了彙編的一些常用指令:push pop add sub jmp(jcc合適一些)  cmp

我們可以先參考一下上述文章中指令的實現,再接下來的逆向中會大有好處。

Vadd:                            ;virtual add
        Mov eax,[esp+4]          ;取源操作數
        Mov ebx,[esp]            ;取目的操作數
        Add ebx,eax              ;
        Add esp,8                ;把參數從堆棧中刪掉,平衡堆棧
        Push ebx                 ;把結果壓入堆棧
      而原有的add命令的參數,我們需要翻譯爲 push 命令 。根據push 的對象不同,需要不同的實現:
vPushReg32:                      ;寄存器入棧 。esi指向字節碼的內存地址
      Mov eax,dword ptr[esi]     ;從僞代碼(字節碼)中獲得寄存器在VMcontext結構中的偏移地址
      Add esi,4                  ;VMcontext結構保存了各個寄存器的值。該結構保存在堆棧內。
      Mov eax,dowrd ptr [edi +eax]   ;得到寄存器的值。edi指向VMcontext結構的基址
      Push eax                    ;壓入堆棧
      Jmp VMDispatcher            ;任務完成,跳回任務分派點
vPushImm32:                       ;立即數入棧
      Mov eax,dword ptr[esi]      ;字節碼,不用翻譯就是了
      Add esi,4
      Push eax                    ;立即數入棧
      Jmp VMDispatcher
     有Push指令了,也得有Pop指令:
vPopReg32:
      Mov eax,dword,ptr[esi]     ;從僞代碼(字節碼)中獲得寄存器在VMcontext結構中的偏移地址
      Add esi,4
      Pop dword ptr [edi+eax]    ;彈回寄存器
      Jmp VMDispatcher
Add esi,eax
  轉換爲虛擬機的指令如下:
     vPushReg32 eax_index
     vPushReg32 esi_index
     Vadd
     vPopReg32 esi_index      ;不彈eax_index,它作爲返回結果保存在堆棧裏
Vjmp:
     Mov esi,dword ptr [esp]   ;jmp的參數在堆棧中
     Add esp,4                 ;平衡堆棧
     Jmp VMDispatcher
Vcall:
     Push all vreg            ;所有的虛擬的寄存器 (維護在堆棧中)
     Pop  all reg             ;彈出到真實的寄存器中(保存虛擬機的運行結果)
     Push 返回地址            ;可以讓Call調用完成後,把控制權歸還給虛擬機。詳細見原書第480頁。
     Push 要調用的函數地址
     Retn

下面我們來看 hgamectf_第四周_happyVM(一點也不happy)

載到ida裏:

根據上述的經驗,迅速的找到,opcode, vm保護的函數,和輔助參數位置

這裏的輔助參數要特別說明一下,棧虛擬機有大量棧的操作,所以輔助參數並不像上一題一樣只是簡單的index,輔助參數,多數變成了 vm_stack(操作的棧),vm_eip(偏移),vm_esp(棧的esp),vm_input(輸入),vm_code(opcode),vm_block(計數器),和一些其他參數(有可能是參數,或者數組),而這些參數通常存放在__bss段(因爲bss存放程序中未初始化的或者初始化爲0全局變量和靜態變量),我們可以從此入手,大致猜一波 ,進入sub_4007B5:

交叉引用一下(按x):都在bss段

結合整個程序綜合分析:(這些判斷也需要根據下文每一個case裏的調試,整理判斷出來的)

602070是申請堆,八九不離十就是vm_stack(操作的棧)

602084:通過簡單分析,很明顯這就是 vm_eip

602078: 就告訴你了 它是 vm_input

result = *(vm_eip + vm_code) = opcode(這個結構非常常見)

602083:

和棧搞位置,試着調試,確定83爲vm_esp。

602085:根據調試分析,這是一個寄存器(負責儲存cmp留下的結果)

602080 81 82 ,爲三個暫時存放的內存位置 vm_var

 

之後我們按順序來分析每一個case:進入 vm_protect

判斷操作數是否爲兩個(算他是個人,這樣判斷opcode中的常數就有依據了)

之後分析每一個case

case的含義不再白給,就拿case 0舉例;

進入sub_400A4F

可以拿上文正向虛擬機保護技術的代碼做參考,綜合上文bss段推斷出的輔助參數,分析出這是push操作,

所以case 0爲   push opcode

同理,你會分析出 push pop call  add jmp je xor,exit, ret等基礎操作,再通過調試分析出每個case的含義:(以下的opcode1,2是一個意思)(這是指紋師傅的分析整理)

Case 0: push opcode2

Case 1: push reg80

Case 2: push reg81

Case 3: push reg82

Case 4: pop reg80

Case 5: pop reg81

Case 6: pop reg82

Case 7: add reg80, reg81

Case 8: add reg80, opcode2

Case 9: add reg81, opcode2

Case A: add reg82, opcode2

Case B: sub reg80, reg81

Case C: sub reg80, opcode2

Case D: sub reg81, opcode2

Case E: sub reg82, opcode2

Case F: xor reg80, reg81

Case 0x10: cmp reg80, reg81

Case 0x11: call opcde2

Case 0x12: ret

Case 0x13: jmp opcode2

Case 0x14: je opcode2

Case 0x15: push input[reg82]

Case 0x16: pop input[reg82]

Case 0x17:exit

 

 

 

你會發現,現在已經不是上一題,每個case還是僞c語言,而是僞彙編了。(哎~~,挺禿然的)

好了咱們接着上題的套路走:分析opcode (找常數,找handler, 找調度)

1) 因爲這題第一個switch已經告訴你了哪些是兩個操作數,所以常數很好找.

2) 找handler && 找調度,每一個case就是一個,但是要注意,此段程序裏有exit (0x17),ret(0x12),所以會幾句case形成一段,之後jmp到別的段,形成調度,但即使是調度,通常也是按循序排的,比如我這一段jmp到9,之後exit了,9的位置就緊挨着exit。(經驗是這樣的)。

附上對opcode的分析(依然是指紋師傅的):

start
11 2D
0 22
05
10
14 09
17

addr9
0 32
05
03
11 16
06
00 16
5
11 16
17

fun16
e 1
15
4
f
1
16
2
0 0
4
3
5
10
14 2B
5
9 3
13 16

addr2B:
5
12

fun2D:
15
04
10
14 36
A 1
13 2d

addr36:
03
04
12

 

緊接着翻譯程序邏輯(我上文說的讀彙編就是這個意思)

start:
call 2D    //2D函數返回輸入flag的長度
push 22
pop reg81
cmp reg80 reg81  //判斷flag長度是否爲0x22
je 9
exit


addr9:
push 32
pop reg81  
push reg82
call 16      //對輸入進行第一次異或運算,傳入的參數爲0x32
pop reg82
push 16
pop reg81
call 16       //對輸入進行第二次異或運算,傳入的參數爲0x16
exit


fun 16:
sub reg82, 1   //reg82最初的值爲0x22,然後一直遞減
push input[reg82]
pop reg80
xor reg80, reg81
push reg80
pop input[reg82]
push reg81
push 0
pop reg80  
push reg82
pop reg81
cmp reg80, reg81 //當reg82值遞減到0的時候,跳出函數
je 2B
pop reg81
add reg81, 3
jmp fun16

addr2B:
pop reg81
ret


fun 2D:   //函數返回輸入flag的長度
push input[reg82]
pop reg80
cmp reg80, reg81
je 36
add reg82, 1
jmp 2D

addr 36:
push reg82
pop reg80
ret

邏輯是首先判斷輸入的長度是否爲0x22,然後兩次調用異或函數對輸入進行處理。(就這麼個玩意兒!!!)

最後腳本:

arr = [132, 131, 157, 145, 129, 151, 215, 190, 67, 114, 97, 115, 115, 12, 106, 112, 115, 17, 72, 44, 52, 51, 49, 54, 35, 52, 62, 92, 35, 78, 23, 17, 25, 89]
flag = ""

reg81 = 0x16
for i in range(len(arr))[::-1]:
    arr[i] = arr[i] ^ reg81
    reg81 += 3

reg81 = 0x32
for i in range(len(arr))[::-1]:
    flag += chr(arr[i] ^ reg81)
    reg81 += 3

print flag[::-1]

flag: hgame{3Z_VM_W0NT_5T0P_UR_PR0GR355}

虎符 vm

ok,最近的虎符的vm就是相似的難度:

如果各位看官沒玩夠,可以再看一下虎符的題,

wp:虎符ctf -- vm虛擬機逆向 (令則寫的這篇很仔細了)

 

0x04 總結

 

從入門到入土,

希望我的總結可以幫助到想學習ctf vm的看官,如果有錯誤也希望路過的大佬斧正。

謝謝指紋師傅的素材。

(本文的幾道題目稍後加在附件裏)

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章