【原創】VB P-code -- 虛擬機器的藝術

看雪資料發表於2004-12-26

*歲末大盤點*
VB P-code -- 虛擬機器的藝術
作者:cyclotron

   初學crack的時候,總有不少大蝦好心地提醒菜鳥們:別碰VB P-code的東東。不可否認,這確實是善意的提醒。VB P-code特殊的運作方式以及由此造成的天然的除錯困難,是不少早期cracker難以忘卻的惡夢。然而,隨著各類專業的VB P-code Debugger和Decompiler層出不窮,VB P-code已經不再像以前那樣遙不可及了。退一步來說,即使是使用通用偵錯程式SoftICE或OllyDBG來跟蹤VB P-code程式,只要掌握其原理,它也並非是不可戰勝的。
   VB P-code到底是怎樣一種機制?它與普通的可執行程式有何不同?下面由我為大家來揭開VB P-code神秘的面紗……

   眾所周知,採用VB編寫的應用程式有兩種編譯方式,一種是Native Code方式,另一種就是P-code方式。事實上,VB 4.0以前只採用P-code編譯,VB Native Code是在VB 5.0以後發展起來的,其目的是為了在一定程度上改善VB應用程式的執行速度。VB P-code的執行速度較慢,這是由它本身的執行機制所決定的。
   P-code,即Pseudo Code(虛擬碼),這一概念最早出現在Pascal編譯器中,它是為了提供跨平臺可移植性而產生的,實現這一編譯機制的Pascal編譯器被稱為"Pascal P Compiler"。Sun公司在其推出的Java語言上也成功地實現了這種機制。Java程式的偽編譯程式碼由一系列代表一定意義的位元組碼(byte code)組成,它們同屬於一套特定的指令集,這種位元組碼不能由不同的CPU直接執行,而是要透過特殊的直譯器翻譯為CPU可以識別的指令才能執行,這種直譯器就是我們常說的"虛擬機器"。只要在不同的平臺上提供虛擬機器,把位元組碼翻譯為對應的CPU指令集,也就實現了所謂的跨平臺特性。Microsoft推出的VB P-code,實際上也是一組自定義的指令集,必須透過基於堆疊的虛擬機器翻譯為80X86上的指令集才能執行,擔任虛擬機器任務的就是msvbvm50.dll和msvbvm60.dll這兩個動態連結庫檔案。由於在檔案執行過程中多出了這一個解釋的步驟,自然要影響到其執行的速度。正如我們所看到的,VB P-code並沒有實現所謂的跨平臺執行特性,這對於Pseudo這個詞的起源是不恰當的;另一方面,採用P-code形式編譯的VB應用程式的體積要小於採用Native Code形式編譯的同樣程式(這是由於P-code指令集的每一條指令對應於一組80X86指令所完成的任務),所以VB P-code實際上意味著VB Packed-code(壓縮程式碼),用以強調VB P-code程式較小的程式碼體積。

   作為程式設計師,他們現在已經擁有了充分的選擇依據--速度,還是體積;然而,作為cracker,僅僅知道這些是遠遠不夠的,正如本文的標題所道出的,我們需要了解的,是虛擬機器的運作方式--這才是在除錯中真正起作用的東西。

   為了說明虛擬機器的運作方式,我們寫個小程式來除錯一下,下面是這個小程式的原始碼:

程式碼:
Private Sub Command1_Click() X = InputBox("Please input an integer", "Input") If X <> "" Then     Y = X + 2     X = Y * 3     Out.Text = X End If End Sub


   這個程式只處理Command_Click事件,同時用到了InputBox,便於我們用rtcInputBox函式設斷,最後記得用P-code方式編譯。
   現在拿出我們心愛的偵錯程式OllyDBG載入這個程式,在命令列外掛中輸入bp rtcInputBox L(注意區分大小寫),按下F9執行,點選按鈕,我們中斷在下面的地方:

程式碼:
6A360CF2 >PUSH EBP          ;這裡就是rtcInputBox的第一句指令了 6A360CF3  MOV EBP,ESP 6A360CF5  SUB ESP,54 6A360CF8  MOV EAX,DWORD PTR SS:[EBP+1C] 6A360CFB  PUSH EBX 6A360CFC  PUSH ESI 6A360CFD  PUSH EDI 6A360CFE  CMP WORD PTR DS:[EAX],0A 6A360D02  MOV EDI,80020004 6A360D07  JNZ MSVBVM60.6A360E6C 6A360D0D  CMP DWORD PTR DS:[EAX+8],EDI 6A360D10  JNZ MSVBVM60.6A360E6C 6A360D16  OR DWORD PTR SS:[EBP-8],FFFFFFFF


   按下Ctrl+F9返回,當InputBox彈出以後隨意輸入一個整數,然後繼續單步跟蹤,直到我們來到下面的程式碼處:

程式碼:
6A37D2CD  MOVSX EAX,WORD PTR DS:[ESI] 6A37D2D0  PUSH DWORD PTR DS:[EAX+EBP] 6A37D2D3  XOR EAX,EAX 6A37D2D5  MOV AL,BYTE PTR DS:[ESI+2] 6A37D2D8  ADD ESI,3 6A37D2DB  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;注意這句 6A37D2E2  MOVSX EAX,WORD PTR DS:[ESI]      ;我們來到這裡 6A37D2E5  ADD EAX,EBP 6A37D2E7  PUSH EAX 6A37D2E8  XOR EAX,EAX 6A37D2EA  MOV AL,BYTE PTR DS:[ESI+2] 6A37D2ED  ADD ESI,3 6A37D2F0  JMP DWORD PTR DS:[EAX*4+6A37DA58]     ;注意這句 6A37D2F7  MOVSX EAX,WORD PTR DS:[ESI] 6A37D2FA  POP EBX 6A37D2FB  MOV WORD PTR DS:[EAX+EBP],BX 6A37D2FF  XOR EAX,EAX 6A37D301  MOV AL,BYTE PTR DS:[ESI+2] 6A37D304  ADD ESI,3 6A37D307  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;注意這句


    你可能已經注意到了,每一段程式碼都包括了下面這些指令:

程式碼:
XOR EAX,EAX MOV AL,BYTE PTR DS:[ESI+2] ADD ESI,3 JMP DWORD PTR DS:[EAX*4+6A37DA58]


   如果不明白這些指令是幹什麼的,跟蹤時就會覺得老是在原處打轉轉。事實上,這幾條指令正是虛擬機器讀取P-code虛擬碼的引擎部分。為了說明這些指令的功能,我們先來看看VB P-code虛擬碼的格式。

3A 6C FF 03 00
操作碼  運算元

   這是一句典型的VB P-code指令,其助記符為LitVarStr,表示將一個Variant型的字串入棧。3A是指令的操作碼,0003是運算元,是以某種形式標記的字串地址,6CFF在虛擬機器引擎中沒有用到,暫不清楚起什麼作用。現在我們可以來解釋虛擬機器所做的一切了:

程式碼:
6A37D2E2  MOVSX EAX,WORD PTR DS:[ESI]      ;esi指向待解釋指令的運算元 6A37D2E5  ADD EAX,EBP          ;取得某種形式的字串指標 6A37D2E7  PUSH EAX          ;壓棧 6A37D2E8  XOR EAX,EAX          ;eax清零 6A37D2EA  MOV AL,BYTE PTR DS:[ESI+2]      ;取下一條指令的操作碼 6A37D2ED  ADD ESI,3          ;移至下一條指令的運算元 6A37D2F0  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;根據跳轉地址表到下一條指令的解釋單元


   這裡我們看到,虛擬機器並沒有用簡單的條件判斷來識別操作碼,而是採用了一種很巧妙跳轉地址表法,把解釋流程直接導向相應的解釋單元。6A37DA58是地址跳轉表的首地址,eax儲存了下一條指令的操作碼,由於每一個跳轉地址是一個dword,所以用eax乘以4的值加上跳轉表的基地址來索引下一條指令的解釋單元。當然,由於每一條指令的長度是不同的,所以前面提到的讀取P-code虛擬碼的引擎部分並不完全相同。明白了虛擬機器的解釋原理,跟蹤P-code程式就方便多了,一旦看到讀取P-code虛擬碼的引擎部分,我們就知道虛擬機器開始解釋下一條指令了。儘管如此,除錯P-code程式還是比除錯普通的本地機器碼編譯的可執行程式困難得多,因為在虛擬機器的流程下,我們已經無法隨時回顧前面執行過的指令了。
   下面我們來看看加法在虛擬機器中是如何被解釋執行的。按F8跟蹤程式碼來到:

程式碼:
6A37D3B3  ADD EDI,EBP 6A37D3B5  PUSH EDI 6A37D3B6  XOR EAX,EAX 6A37D3B8  MOV AL,BYTE PTR DS:[ESI] 6A37D3BA  INC ESI 6A37D3BB  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;al=FBh


   走到這裡時al的值為FB,這是Variant型變數算術運算偽指令的操作碼,esi則指向了該操作碼後的一個次操作碼94,代表加法運算。跟隨跳轉地址表我們來到:

程式碼:
6A37D9BA  XOR EAX,EAX 6A37D9BC  MOV AL,BYTE PTR DS:[ESI] 6A37D9BE  INC ESI 6A37D9BF  JMP DWORD PTR DS:[EAX*4+6A37DE58]    ;al=94h


   這裡是一個二級跳轉表,我們注意到6A37DE58這個值和前面的跳轉地址表基址不同了,這說明VB P-code算術運算偽指令採用了二級跳轉來解釋執行。我們知道一個位元組的操作碼可以表示的操作指令種類最多為256個,為了解決指令數不夠的問題,Microsoft對部分偽指令進行二級跳轉解釋執行。這次跳轉以後,我們來到:

程式碼:
6A384628  LEA EBX,DWORD PTR DS:[__vbaVarSub] 6A38462E  JMP SHORT MSVBVM60.6A384610 6A384630  LEA EBX,DWORD PTR DS:[__vbaVarMul] 6A384636  JMP SHORT MSVBVM60.6A384610 6A384638  LEA EBX,DWORD PTR DS:[__vbaVarDiv] 6A38463E  JMP SHORT MSVBVM60.6A384610 6A384640  LEA EBX,DWORD PTR DS:[__vbaVarIdiv] 6A384646  JMP SHORT MSVBVM60.6A384610 6A384648  LEA EBX,DWORD PTR DS:[__vbaVarMod] 6A38464E  JMP SHORT MSVBVM60.6A384610 6A384650  LEA EBX,DWORD PTR DS:[__vbaVarAdd]    ;我們跳轉到這裡 6A384656  JMP SHORT MSVBVM60.6A384610 6A384658  LEA EBX,DWORD PTR DS:[__vbaVarAnd] 6A38465E  JMP SHORT MSVBVM60.6A384610 6A384660  LEA EBX,DWORD PTR DS:[__vbaVarOr] 6A384666  JMP SHORT MSVBVM60.6A384610 6A384668  LEA EBX,DWORD PTR DS:[__vbaVarXor] 6A38466E  JMP SHORT MSVBVM60.6A384610 6A384670  LEA EBX,DWORD PTR DS:[__vbaVarEqv] 6A384676  JMP SHORT MSVBVM60.6A384610 6A384678  LEA EBX,DWORD PTR DS:[__vbaVarImp] 6A38467E  JMP SHORT MSVBVM60.6A384610


   很明顯,Variant變數的算術邏輯運算偽操作都分佈在這裡,即將呼叫的函式__vbaVarAdd地址被裝入ebx,繼續向下看到:

程式碼:
6A384610  MOVSX EDI,WORD PTR DS:[ESI]      ;取加法指令的運算元 6A384613  ADD EDI,EBP 6A384615  PUSH EDI          ;運算元入棧 6A384616  CALL EBX          ;這裡呼叫了函式__vbaVarAdd執行加法操作 6A384618  PUSH EDI          ;運算結果儲存在堆疊結構中 6A384619  XOR EAX,EAX 6A38461B  MOV AL,BYTE PTR DS:[ESI+2]      ;繼續取下一條偽指令的操作碼 6A38461E  ADD ESI,3          ;指向下一條偽指令的運算元 6A384621  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;跳向下一條偽指令的解釋單元


   經過上面的跟蹤,VB P-code虛擬機器的解釋過程已經很清楚了,其他的相關指令讀者可以透過除錯自行分析。
   現在我們知道SoftICE或者OllyDBG完全可以勝任除錯VB P-code程式的任務,然而,在大多數情況下,它們並非是我們最好的選擇,根據除錯的習慣,我們總是希望偵錯程式能夠直接跟蹤可執行檔案本身的每一句指令,這樣我們可以直觀地瞭解程式實現的功能。從這個角度來說,SoftICE或者OllyDBG是除錯VB P-code虛擬機器的優秀工具,但是它們無法讓我們將更多的注意力集中在P-code程式本身的功能上。伴隨著這樣一種想法,功能強大的VB P-code專用偵錯程式WKTVBDebugger應運而生了。這又是一種怎樣的偵錯程式?它究竟給VB P-code程式除錯帶來了怎樣一種變革?敬請關注《VB P-code -- 偵錯程式的革命》。
附件:VB_P-code.rar 

相關文章