【原創】VB P-code -- 虛擬碼的奧祕

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

*歲末大盤點*
VB P-code -- 虛擬碼的奧祕
作者:cyclotron

   歡迎回到VB P-code虛擬碼世界。
   前兩期我為大家介紹了VB P-code虛擬機器的執行機制和P-code專用偵錯程式WKTVBDebugger,相信大家已經對VB P-code程式有所瞭解了。我們的任務當然不僅僅侷限於理論研究,解析VB P-code的最終目的還是加密解密和逆向工程。由於Microsoft沒有公開VB P-code虛擬碼的技術文件,我們無法獲得現成的虛擬碼指令說明,而單憑VB P-code反編譯器給出的助記符資訊是遠遠不夠的,這就要求我們自行發掘虛擬碼執行的奧祕。
   可能有些朋友還不太明白,既然WKTVBDebugger作為一個虛擬碼級的偵錯程式已經遮蔽了VB P-code虛擬機器的解釋過程,為什麼還費神要去了解這些偽指令執行的細節呢?這裡我請大家思考一個問題:假設你現在用WKTVBDebugger跟蹤AddVar這句偽指令,你如何知道它的運算元和操作結果分別是多少?也許有人說,既然VB P-code虛擬機器是基於堆疊的,那麼運算元和操作結果一定存放在堆疊裡了。不錯,這些內容確實存在於堆疊中,但是你知道它的存放形式嗎?是單個的運算元,是指標,還是其他複雜的資料結構?我敢說,如果你是第一次除錯這句VB P-code指令,它一定會令你不知所措,即使你能最終解決這個問題,也一定會花上不少時間,結果也會令你覺得不可思議。此外,對於不同的P-code偽指令,其存放形式是各不相同的。如果你在除錯一個軟體時,不能看到每句指令的運算元和操作結果,那與靜態反編譯何異之有?WKTVBDebugger的除錯功能還不是形同虛設?當然,這只是其中的一個方面。如果你連偽指令的抽象動作都不明白(畢竟不是所有的偽指令都能從其助記符看出其含義的),那又該如何呢?正如我前面所言,要理解這些偽指令的執行細節,是要費一番功夫的。

   既然如此,我們應當如何解決這些問題呢?答案將由黃金組合OllyDBG+WKTVBDebugger+VBParser來揭曉。為了方便,這次使用的例子程式還是上兩篇使用的VB P-code.exe,我們將通過跟蹤虛擬機器的解釋過程來研究P-code虛擬碼的執行細節。
首先使用ljtt的VBParser(非常專業的VB P-code反編譯器)解析VB P-code.exe,得到虛擬碼如下:

程式碼:
FileName: D:\Contribution\VB P-code\VB P-code--虛擬碼的奧祕\VB P-code.exe -----=====-----=====-----=====--Pcode--=====-----=====-----=====----- [CommandButton] Private Sub Command1_Click() '-=-=-=-=-=-=-= ProcAddr Range: [004019C4 - 00401A54] , ProcSize: 90 =-=-=-=-=-=-=- 004019C4: 27 9C FE           LitVar_Missing          PushVarError 80020004 (missing)                                                   004019C7: 27 BC FE           LitVar_Missing          PushVarError 80020004 (missing)                                                   004019CA: 27 DC FE           LitVar_Missing          PushVarError 80020004 (missing)                                                    004019CD: 27 FC FE           LitVar_Missing          PushVarError 80020004 (missing)                                                    004019D0: 27 1C FF           LitVar_Missing          PushVarError 80020004 (missing)                                                    *********** Referent String: "Input" ***********                               | 004019D3: 3A 4C FF 00 00     LitVarStr               PushVarString Ptr_00401434 004019D8: 4E 3C FF           FStVarCopyObj           [local_C4]=vbaVarDup(Pop) 004019DB: 04 3C FF           FLdRfVar                Push local_C4 *********** Referent String: "Please input an integer" ***********                               | 004019DE: 3A 6C FF 01 00     LitVarStr               PushVarString Ptr_00401400 004019E3: 4E 5C FF           FStVarCopyObj           [local_A4]=vbaVarDup(Pop) 004019E6: 04 5C FF           FLdRfVar                Push local_A4 004019E9: 0B 02 00 1C 00     ImpAdCallI2             Call Ptr_00401020; check stack 001C; Push EAX 004019EE: 46 7C FE           CVarStr                  004019F1: FC F6 8C FE        FStVar                   (……省略)


   以上就是Command_Click事件響應程式碼的開頭部分,是不是一下子覺得有些手足無措?沒關係,我們一句一句來除錯。如果你還記得我在第一篇中所描述的VB P-code虛擬機器執行機制,那麼你應該能想到下一步該怎麼做。VB P-code虛擬機器以流的形式順序讀入每一句偽指令,然後通過一個跳轉地址表找到相應的解釋程式碼,我們要跟蹤它解釋偽指令的細節,就必須在偽指令的操作碼上下記憶體訪問斷點。現在我們看到第一句偽指令LitVar_Missing從004019C4(虛擬地址)開始,那麼用OllyDBG載入VB P-code.exe,在轉存視窗中來到004019C4,對第一個位元組(操作碼)下記憶體訪問斷點,然後按下F9讓它執行,接著點OK。看看我們中斷在哪裡:

程式碼:
6A37D153  MOV AL,BYTE PTR DS:[ESI]      ;中斷在這句,開始讀操作碼了,注意esi的值為004019C4 6A37D155  INC ESI          ;使esi指向運算元 6A37D156  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;根據跳轉地址表和操作碼定址解釋單元


   F8往下走,我們來到這句偽指令的解釋單元:

程式碼:
6A37D39F  MOVSX EDI,WORD PTR DS:[ESI]      ;把字運算元帶符號擴充套件到edi 6A37D3A2  ADD ESI,2          ;esi指向下一句偽指令的操作碼 6A37D3A5  MOV WORD PTR DS:[EDI+EBP],0A      ;ebp顯然是程式堆疊區某處的基址,但不是堆疊頂的指標,它把0A儲存到edi指向的偏移地址處 6A37D3AB  MOV DWORD PTR DS:[EDI+EBP+8],80020004    ;向下8個位元組處存入80020004,根據VBParser的說明,這個數字表示空引數(預設引數),事實上我在原始碼中確實沒有提供這個引數 6A37D3B3  ADD EDI,EBP          ;這次edi得到0A所在的虛擬地址 6A37D3B5  PUSH EDI          ;在堆疊中壓入這個虛擬地址 6A37D3B6  XOR EAX,EAX          ;清空eax,準備讀取下一句偽指令 6A37D3B8  MOV AL,BYTE PTR DS:[ESI]      ;讀取下一句偽指令的操作碼 6A37D3BA  INC ESI          ;esi指向下一句偽指令的次級操作碼或運算元 6A37D3BB  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;根據地址跳轉表和操作碼定址解釋單元


   我們來看一下這些指令執行完以後的堆疊:

程式碼:
0012F458   0012F494          ;棧頂 0012F45C   00000000 0012F460   00000000 …………………… …………………… 0012F488   00000000 0012F48C   00000000 0012F490   00000000 0012F494   0000000A          ;這就是剛才壓入棧頂的資料了 0012F498   00000000 0012F49C   80020004 0012F4A0   00000000


   現在這個偽指令的動作很清楚了,LitVar_Missing執行以後,把一個虛擬地址壓入堆疊,這個虛擬地址指向0000000A,00000000,80020004。實際上,這句偽指令的功能就是在堆疊中提供一個空引數,其堆疊完全沒有參考價值。但我要據此說明的是,對所有的偽指令,我們都將使用這種方法來跟蹤。在下面的說明中我將省略對虛擬機器虛擬碼讀取引擎的註釋,因為這部分都是一樣的。
下面我們來看看004019D3處的偽指令LitVarStr。老規矩,在004019D3處設記憶體訪問斷點,F9中斷在下面的地方:

程式碼:
6A37D3B8  MOV AL,BYTE PTR DS:[ESI]      ;esi=004019D3 6A37D3BA  INC ESI 6A37D3BB  JMP DWORD PTR DS:[EAX*4+6A37DA58]


   執行跳轉來到:

程式碼:
6A37D3C2  MOVSX EDI,WORD PTR DS:[ESI]      ;第一個字運算元FF4C(堆疊區偏移量)帶符號擴充套件到edi 6A37D3C5  MOVZX EAX,WORD PTR DS:[ESI+2]      ;第二個字運算元0000(資料區偏移量)無符號擴充套件到eax 6A37D3C9  MOV EDX,DWORD PTR SS:[EBP-54]      ;根據下一句指令來看,這是P-code程式資料區的基址 6A37D3CC  MOV EAX,DWORD PTR DS:[EDX+EAX*4]    ;根據eax產生偏移量,取得資料區的資料,這裡我們看到eax最後取得   一個虛擬地址,指向Unicode字串"Input" 6A37D3CF  ADD EDI,EBP          ;edi(堆疊區偏移量)指向堆疊區即將儲存資料的地方 6A37D3D1  MOV WORD PTR DS:[EDI],8      ;存入表示型別的資料 6A37D3D6  MOV DWORD PTR DS:[EDI+8],EAX      ;向下偏移8個位元組處存入指向Unicode字串"Input"的虛擬地址 6A37D3D9  PUSH EDI          ;最後堆疊區資料指標入棧 6A37D3DA  XOR EAX,EAX 6A37D3DC  MOV AL,BYTE PTR DS:[ESI+4] 6A37D3DF  ADD ESI,5 6A37D3E2  JMP DWORD PTR DS:[EAX*4+6A37DA58]


   同樣地,有必要觀察一下堆疊:

程式碼:
0012F444   0012F544          ;棧頂 0012F448   0012F514          ;下面是前面其他指令形成地堆疊 0012F44C   0012F4F4 0012F450   0012F4D4 0012F454   0012F4B4 0012F458   0012F494 0012F45C   00000000 …………………… …………………… 0012F540   00000000 0012F544   00000008 0012F548   00000000 0012F54C   00401434  UNICODE "Input" 0012F550   00000000


   結合上述跟蹤,LitVarStr偽指令運算元的觀察方法就很明顯了:首先在dump視窗觀察0012F544處的內容,向後移8個位元組,得到虛擬地址00401434,再從dump視窗觀察00401434處的內容,就是入棧的字串引數了。
相應地,下面我們在WKTVBDebugger中演示一下操作的過程:
1.WKTVBDebugger載入VB P-code.exe;
2.在Form Manager中對Command1控制元件設斷點;
3.點選OK;
4.WKTVBDebugger中斷在下面的地方:

程式碼:
004019C4: 27 LitVar_Missing 0012F474h 004019C7: 27 LitVar_Missing 0012F494h 004019CA: 27 LitVar_Missing 0012F4B4h 004019CD: 27 LitVar_Missing 0012F4D4h 004019D0: 27 LitVar_Missing 0012F4F4h 004019D3: 3A LitVarStr 'Input'    ;這句就是我們在OllyDBG中跟蹤的LitVarStr偽指令 004019D8: 4E FStVarCopyObj 0012F514h 004019DB: 04 FLdRfVar 0012F514h 004019DE: 3A LitVarStr 'Please input an integer'     004019E3: 4E FStVarCopyObj 0012F534h 004019E6: 04 FLdRfVar 0012F534h 004019E9: 0B ImpAdCallI2 rtcInputBox on address 73472265h


5.注意我所註釋的這句偽指令,當我們單步走過這句指令時,右上角堆疊視窗顯示如下(為了便於觀察,在右側的單選框中選擇DWORD):

程式碼:
0012F424: 0012F524 0012F4F4      ;注意這裡的棧頂0012F524 0012F41C: 0012FE3C 0000004E …………………… …………………… 0012F3B4: 00000000 00000000 0012F3AC: 77E6780F 0000008C


6.按下Ctrl+M開啟轉存視窗,在Address to Dump組合框中輸入0012F524,我們看到:

程式碼:
0012F524:08 00 00 00 00 00 00 00 0012F52C:34 14 40 00 00 00 00 00    ;好了,還記得那個8個位元組的偏移嗎?00401434就是我們需要的那個資料區字串的指標了! 0012F534:00 00 00 00 00 00 00 00


7.記下這個指標,輸入到Address to Dump組合框,現在這個Unicode字串終於露出了真面目:

程式碼:
00401434:49 00 6E 00 70 00 75 00         I.n.p.u. 0040143C:74 00 00 00 00 00 00 00         t....... 00401444:00 00 00 00 E1 4E AD 33         ....N?


   當然,就這個指令本身而言,WKTVBDebugger已經在偽指令視窗中給出了其運算元,所以我們要觀察這個字串大可不必那麼麻煩,但是對於其他沒有註釋的偽指令,這恐怕是唯一的辦法了。

   由此可見,要熟練地除錯VB P-code程式,整理出全部偽指令運算元的定址方式是必要的一步。在OllyDBG和WKTVBDebugger面前,P-code偽指令並不神祕,它們只不過是一些人為定義的符號罷了。在WKTVBDebugger中,偽指令像是一個黑盒子,虛擬機器隱藏了黑盒子的全部祕密。既然虛擬機器的內部對OllyDBG是可見的,P-code偽指令又怎會遙不可及呢?下期為大家獻上《VB P-code -- 最後的戰役》,通過一個CrackMe的實戰破解為我們的VB P-code之旅畫上一個圓滿的句號。

相關文章