如何製作VB的P-Code偵錯程式(譯自:WKTVBDE的作者)

看雪資料發表於2015-11-15

如何製作VB的P-Code偵錯程式(譯自:WKTVBDE的作者)

VB P-Code Information by Mr Silver

源自:http://www.woodmann.com/crackz/VB.htm


P-Code簡介

術語P-Code既不是一個新名詞也不是Microsoft的發明,P-Code只是簡單地被解釋執行的偽指令。因此,我們並不需要透過什麼複雜的專業詞彙來描述它。P-Code可以被認為是一種普通的機器級程式碼(我們的微處理器不能解釋它)。在執行P-Code之前,需要一個直譯器處理它,轉換P-Code為CPU可以理解的機器語言。這個過程看起來有點象JAVA, 為了執行JAVA語言寫成的應用程式,我們需要一個進行解釋和翻譯的處理程式- 虛擬機器“Virtual Machine”。這個負有想象力的術語實際意味著它是一個翻譯的機制,將JAVA編寫的程式轉換成我們的CPU可以理解的操作碼(指令)。

P-Code的優勢是明顯的。假如我們定義了一組特殊的專屬性指令集,並且不公佈它的詳細定義規範說明,那麼一般人很難理解我們生成的程式程式碼;另一個優勢是減小了執行檔案的尺寸:透過定義獨特的單位元組操作碼,我們能夠使得某些偽指令執行一系列的操作(相當於大量的機器碼完成的工作)。Microsoft的 Visual Basic 包含的P-Code的確是如此:一個VB虛擬機器翻譯P-Code到我們本地的機器碼。 虛擬機器(以dll形式出現),在P-Code程式執行前被呼叫,用來解釋相關VB的偽指令。有如你猜測的那樣,那些DLL的名稱是 :
MSVBVM50.DLL MSVBVM60.DLL
檔名稱清晰的表明是Microsoft Visual Basic 虛擬機器(Virtual Machine), 後跟不同的版本資訊。兩個版本的差異不大:版本6引入了一些新的指令,並採用了更直觀的命名來標註某些版本5中的指令。換句話說,版本6只是改變了版本5中部分指令名稱,而非其內在的功能。

虛擬機器不僅解釋Visual Basic 的P-Code檔案,同時它也被用於執行編譯過的機器碼。這是因為VB 虛擬機器(DLL檔案)同時也包含所有VB程式要呼叫的API。 一個例子是rtcMsgBox, 這是個等價於標準Windows API MessageBox 的VB函式。P-Code程式碼被VB虛擬機器解釋執行,VB中所有的函式都是以這種間接的方式被提供的。

由於這個原因,當我們跟蹤一個Windows API MessageBox被 P-Code程式時,產生了一個嚴重的問題:我們必須要跟蹤P-Code偽指令。

SoftICE 無法跟蹤P-Code偽指令, 它只能跟蹤VB虛擬機器的執行過程。更明確地說, SoftICE 只能理解CPU處理器的機器碼,它不能理解任何偽指令。我們將嘗試去跟蹤P-Code(P-Code偽指令將被轉換成可被我們的CPU理解和執行的機器碼)。

起始表(Beginning of the Tale)
幾乎所有的事情都是如此:好奇心引發了人們迎接一項新的挑戰,我們的故事由此開始。

我記得曾在EFNet網站(論壇)與Green先生討論有關VB P-Code的問題。他那時的工作正好涉及有關VB5編譯的應用程式。他告訴我處理P-Code是非常的困難,所以我們有了製作一個VB P-Code的Debugger程式的想法。 實際上Black先生也認為這是個有意義的思路。考慮到這個專案,我說如果我們沒有任何可用的有關資源,這可不容易實現。而後,我們查詢了許多有關資訊,但沒有任何有意義的發現,空手而歸。好奇心使得我更加努力去細心地發掘有用的資訊資料。的確是不易呀…我曾和Snow先生探討有關問題,他提供給我一個被Lazarus修改過的 MSVBVM50, 在其中,他描述了VB程式表現的所有可能的串比較。這促使我下決心製作一個VB Debugger。

我認為當MSVBVM50執行時注入程式碼是可能的。被注入的程式碼能夠呼叫我的Debugger, 它在一個DLL中實現。  我決定告訴已加入這個專案的Snow先生, 由他負責製作程式碼注入器 (好像一個呼叫裝入器Loader) ,我負責Debugger (DLL)編碼,就是那個被裝入的Debugger(DLL) 。 就在我們兩個完成了一些工作後,我們進行了測試,令人振奮的是它真的可以工作!這個 Debugger專案已經邁出了它的第一步。

我們可以控制VB虛擬機器(截獲有關操作),並且在虛擬機器與VB應用程式之間安置我們的Debugger。最大的問題已經得到解決,雖然在初始階段,我們採用的解決方案(技術上如你所見)並不是最終我們採用的方法。在我們的大目標和指導思想始終如一的情況下,我們不斷對它進行改進,一直到我們完全避免了對虛擬機器本身的修改。

* 第一步

跟蹤分析,控制虛擬機器

為使我們的Debugger能夠工作,有一個關鍵性必須解決的問題:發現P-Code程式碼的翻譯轉換是什麼時候以及如何發生的。一旦我們認識到這一點,我們注入的程式碼將接管對被除錯的VB應用程式的控制,並且傳送有關資料到我們的Debugger。Debugger 依次處理操作碼並返回到VB 虛擬機器。 我本人以前在有關偵錯程式Debugger編碼方面的經驗幾乎為零。不過不久前我差不多完成了一個x86的反彙編器 , 因此我將那些知識用於我的VB Debugger 開發工作。我的構思是這樣的:

對於反彙編/解釋這部分程式碼包含以下基本原理:

- 一個指標(pointer)指向一個快取區(buffer),它包含將被轉換的資料。
- 一個控制程式,它從快取區中讀取操作指令(opcodes)並且重定向程式流,使其依據我們的意圖,指向我們想要它執行的程式位置。

這個任務通常表現為兩種形式: 1、一系列控制描述語句(對於每一個操作碼);2、使用一個地址跳轉表。 我放棄了第一種選擇。因為P-Code中各不相同的操作碼實在太多,這將需要一個巨大的條件控制結構(處理這樣的工作將變成世界上最慢的事情)。 我猜VB虛擬機器對P-Code的翻譯轉換過程採用的是地址跳轉表方法去解釋那些可能的操作程式碼。 這種做法同樣出現在我設計的反彙編器中。 現在,我必須完成以下的工作:

- 定位快取區中待解釋的操作碼,定位跳轉地址表

我設計並且編譯了一個小VB應用程式:

Private Sub Form_Load()

MsgBox "Hello this is P-Code!!!", VBInformation, "Example"

End Sub

我透過SoftICE的符號載入器(symbol loader)調入MSVBVM60.DLL (VB6虛擬機器) ,設定BPX on _rtcMsgBox。 當SoftICE 中斷時,我按 F12返回撥用 _rtcMsgBox 的程式碼:

call eax  // 呼叫 rtcMsgBox
cmp edi,esp  // 我們在此
jnz 66105595  // 檢查堆疊指標
xor eax,eax  // 準備暫存器 eax 去呼叫快取區中的下一個操作碼 :-)
mov al,[ESI]  // 在al中裝入待執行的操作碼, 上面的演示中,它是36h
inc ESI  // 增加指標偏移量(在 ESI 暫存器中)
jmp [eax*4+660FDA58]  // 跳轉到解釋偽指令操作碼 36h 的處理程式

我們看到,就如推測的一般,直譯器從快取區中讀入操作碼(P-Code偽指令)並且放入8位暫存器AL, 將此操作碼作為一個地址偏移量,跳轉到相應的處理子程式。我上面提供的小例子中的 36h 既是作為了一個地址偏移量 (這是一個聰明,靈活的辦法去處理不同的指令避免了成千的分支判斷檢查。利用如此聰明的設計方法還真不太象Microsoft一貫的作風)。 如果我們繼續跟蹤下去,我們會看到利用暫存器ESI作為偽指令操作碼快取區指標來解釋VB應用程式的步驟被不斷地重複執行著。 其實,在VB 的虛擬機器執行時, ESI暫存器始終指向包含被執行的VB應用程式的全部P-Code操作碼的一個快取區。 因此,在SoftICE中,我們總可以利用“ d *ESI”發現將要被執行的操作碼。

的確有如我們已經發現的: ESI暫存器包含了一個指向偽指令操作碼的快取區的導航指標; AL包含下一個將要被執行的操作碼(1位元組)。 最有趣的指令語句是無條件跳轉: JMP [4*EAX+ADDRESS]。 你可以使用從快取區中讀取的位元組作為一個偏移量,進入地址跳轉表中的相應程式入口(基於ADDRESS)。 這個跳轉地址表的最大尺寸很容易被推算出來: 最大的偏移量AL (256) * 我們程式中的索引偏移量4(因為每個地址長度佔4個位元組) : 256 * 4 = 1024 bytes

以上事實和發現證實了我對VB中採用了地址跳轉表技術的推測。 基於Microsoft的文件,它宣告標準的P-Code 包含 了256 個操作碼,其它作為擴充套件的操作碼。 這提醒了我,我的可愛的老PC也有256個不同的操作碼可以執行。不過,實際上包含的操作碼要多的多。如果僅有256獨特的可識別值-偽指令操作碼,那更多的操作碼是如何工作的呢? 它們是什麼值? (我從事反彙編器的經驗使我認識到) 這很容易做到: 256個被保留的識別值中有些是作為(指令)字首使用的。

當直譯器發現這樣的字首,它指示一個擴充套件的指令集合(給出一組新的可能有256種可能性的指令集)。因此,使用字首可以允許無限制的指令操作碼設定。 注意,每個字首應該有它自己的新的跳轉地址表。 稍後,我們會看到許多在當前的VB P-Code程式碼中使用的字首,以及如何定位它們的地址跳轉表。 為了確認我們發現的跳轉地址的正確性,我反彙編了VB虛擬機器,並且探察研究了跳轉地址表中的所有的情況。 正如我們期望的,所有偽指令都有對應的處理程式。 而後,我分析了VB虛擬機器的引擎(DLL)中這個表的內容驗證它在虛擬機器中的地址入口,以及包含的內容。 我檢視了一些偽指令的處理程式,它們有著同樣的結構:

它們從暫存器ESI指向的快取區中讀取資料並執行某些指令。我找到了我想要的-每一條偽指令的跳轉地址。

下一步是用我們自己的內容替換跳轉地址表(包含全部操作碼的處理程式入口地址)。 這些新的地址應該指向我們自己的處理程式,它們在我們的Debugger的 DLL中。 在所有操作碼真正被VB虛擬機器執行前,先透過我們預先設定的程式的處理。 VB虛擬機器中的處理程式被我們自己的處理程式替換(以C的呼叫方式),所有的暫存器和標誌位被儲存,並在執行下一條P-Code指令前恢復為原先的內容。

這裡是我們自己的Debugger處理程式的開始和結束 :

__declspec( naked ) void DebuggerProc()
{

_asm {
mov VBDebugger.OldStack_ESP,esp  // 我們儲存VB 的堆疊
mov VBDebugger.OldStack_EBP,ebp  // 基地址指標和堆疊指標
pushad  // 儲存所有暫存器狀態
pushfd  // 儲存所有標誌

push ebp  // 現在我們放一個標準的呼叫框架
mov ebp, esp
sub esp, __LOCAL_SIZE  // 如果我們能有一些區域性變數,這裡應該預留出空間,從堆疊中減去相應的尺寸

// 這裡是 Debugger的控制程式碼部分
// ...
// ...

// 這裡我們修改跳轉地址。由於它們不能在編碼時修改,因此我們在VB虛擬機器執行時採用自修改程式碼(SMC self-modifying code)設定跳轉地址表:P

_asm {
mov eax, offset JmpOffset
add eax, 3
mov ebx, [VBDebugger.RedirTableAddr]
mov [eax], ebx
}

mov esp, ebp // 我們儲存的堆疊的初始框架
pop ebp
popfd  // 恢復標誌
popad  // 恢復暫存器

// 最後
// 如果我們要以記憶體編輯模式修改被除錯的VB應用程式中的偽指令操作碼,我們可以在AL中改變它。因此這樣的改變只有當我們要修改的操作碼將要跳轉入VB虛擬機器中相應的控制程式時才可進行。

_asm {
cmp [VBDebugger.OPCODE_CHANGE],0
je NoChange
mov al,VBDebugger.Opcode
NoChange:
mov [VBDebugger.VMAddress],0
}

JmpOffset:
// 將程式的控制權返還VB虛擬機器。 注意這個程式碼是在執行時間自修改的
jmp [eax*4+VBDebugger.RedirTableAddr]

}

看到這個程式我們可以發問:誰說C不夠強大? 就像你看到的,透過使用 “Naked” 指示,我們能憑著我們自己的品味要求建立程式。 事實上,這個指示(“Naked”)經常在為Window設計的驅動程式(VxD)中使用。 這個程式的行為有如一個hook(鉤子程式)存在於原來的程式碼和虛擬機器之間。 就像你所見的,這個程式除了返回控制到原始的跳轉表什麼也沒做, 但它可以控制那些我們需要它控制的VB虛擬機器執行的偽指令。

當我們開始有關P-Code的研究時,我們缺少有關資料,Microsft保留了有關技術規範說明。如果你想獲知P-Code的秘密,你必須和微軟簽訂一份叫做NDA (non disclosure Agreement)的保密協議,你保證不散佈任何有關資訊(這些資訊會使得某些公司能夠透過合法行對微軟產生威脅)。因此你只能找到如下的可憐的一點零星資料,你可以在這裡發現它們:

http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/msdn_c7pcode2.asp


此外 Exdec, 是一個由Josephco製作的 P-Code 反編譯器。 Exdec可以反編譯任何P-Code檔案,但操作碼的顯示不夠完善和全面(僅僅第一個位元組)。 我們稍後會知道原因。

跳轉地址表提供了一個基礎。 我們準備了一個程式碼補丁,它將作為一個新的區段部分(section)被加到VB虛擬機器(DLL)中。 這個補丁會獲得初始資料並且呼叫我們的Debugger。 一旦Debugger準備好(安裝好鉤子程式“hooking”到虛擬機器, 我們的補丁程式將繼續載入VB的虛擬機器,重置控制權到VB虛擬機器DLL的OEP(程式入口)。 這個方法有一個主要的缺陷: 必須修改VB的虛擬機器DLL。

我們可以透過建立一個載入器(loader)消除必須修改VB虛擬機器程式碼的必要性。 一個小應用程式(載入器)以懸掛方式(suspended mode)開始一個可執行VB程式,使用 GetThreadContext獲得其入口地址, 複製一個程式碼補丁(它負責裝入Debugger的 DLL )。 一旦補丁程式被執行, 它使用synchronism APIs SetEvent 和 WaitForSingleObject通知主程式。 一旦補丁程式完成,他將恢復原始程式程式碼,並且透過呼叫SetThreadContext返回到原程式的OEP,就象什麼都沒有發生過。

補丁程式執行後,當我們的Debugger得到控制時產生了一個問題。當原始應用程式程式碼和部分VB資料被臨時替換成我們的Debugger需要的資料時,我們必須執行我們的Debugger在一個獨立的執行緒中,這個執行緒會檢查一個記憶體位置是否包含VB5的簽名,它(我們的移植檢查過程)用於指示載入器是否成功執行,並且進行了我們預設的(各種替換)工作。

我們Debugger中的移植檢查過程也會校驗被裝入的是否為一個可執行的VB 應用程式,這是透過察看輸入表是否包含MSVBMXX。DLL(其中 XX 可能是 05或 06)。

另一個問題是不同版本的VB虛擬機器操作碼地址表存在差異。因此我們必須針對不同版本的虛擬機器進行個案分析和處理。 這是個麻煩的方法。因為一旦有新版本的虛擬機器被髮行,我們就要修改我們自己的載入器程式碼。 這個問題一直沒能得到解決,直到最近,我們才想出了比較理想的解決方案。 就如我以前說的,操作碼地址表在VB虛擬機器的引擎區域(在名為 .ENGINE 的區段中)。 這個表的特性之一就是它所包含的所有地址必須指向相同的區段空間, 因此我們設計了一個演算法,它將定位第一組256個相鄰的雙字(DWORDS),這些值包含在.ENGINE 區段, 並且這將確定地址表:-)。

* 第二步

恢復操作碼與重獲VB偽指令操作碼助記符

如果我們開始就注意到了虛擬機器的符號檔案(DBG),這一步對我們來說就簡單了。 一個辦法是利用JosephCo的 Exdec反編譯器獲得助記符。 這並不容易搞定。 我沒有一個個去找操作碼, 而是假設Exdec(它的DLL)包含了一個操作碼列表。 它也正是那樣(我用一個十六進位制編輯器定位和確定每個助記符的位置)。關鍵點:我發現部分操作碼使用了字首-FF,FE,FD,FC,FB (Lead0, Lead1, Lead2, Lead3 and Lead4)位元組。每個字首產生一個新的操作碼錶,共有:

( 5 字首prefixes + 一組標準操作碼設定) * 256 偽指令操作碼 = 1536 個偽指令操作碼

正如你所見,VB的操作碼不少,不過許多沒有作用,而且一些是多餘的。例如:在VB6的虛擬機器中,執行同樣的操作 Lead4 字首不使用自己的操作碼處理程式跳轉表而是直接轉到操作碼46h的處理程式中(這可以透過反編譯VB虛擬機器得到驗證)。 就象我上面提到過的,當我們已經確定了一些操作碼的行為時,我們認識到VB的虛擬機器Debugger 檔案包含全部它自己的助記符資訊(處理程式名稱,地址,每個助記符的名稱)。我們是使用SoftICE轉存(dumping)DBG內容到文字檔案獲得這些資訊的。

這裡列出一小部分:

相對虛擬地址 符號名稱
RVA Size   Symbol name
0F103D8Bh 34 CCyR4
0F103DADh 19 CCyVar
0F103DC0h 9  CBoolCy
0F103DC9h 0  CBoolR8
0F103DC9h 38 CBoolR4
0F103DEFh 32  CStrVar
0F103E0Fh 18 CStrBool
0F103E21h 34 CStrR8

透過上面的例子,你看到了一些P-Code助記符; RVA是它們在虛擬機器中的相對地址偏移。 不幸的是,這些地址在不同版本的VB虛擬機器中不相同,但我們的Debugger可以透過啟發式搜尋方法處理地址跳轉表。 不同版本中的一些操作碼助記符可能不同,但功能不變。 應用我們已經掌握的資訊我們初步的做了一個偽指令操作碼反編譯器。它還僅能顯示正在被執行的操作碼,我們必須發現每一條指令的精確長度。這是全部工作中最艱鉅的任務: 必須分析每一個操作碼處理程式,在其開始和結束,檢查操作碼快取區尺寸。

我們分析了1000多個操作碼處理子程式,雖然有些很短,但花費了大量時間。 起初,我們假定操作碼的快取區尺寸是固定不變的,但不幸的是它們不同。 一些指令需要許多引數入棧,這使得它們的尺寸總是變化的。 這樣的操作碼不多,但如果我們不夠小心謹慎就會在對相應處理過程的分析中產生錯誤的認識。大概這就是為什麼JosephCo(在他的反編譯器Exdec中)選擇了不完全反編譯的理由。 反向工程質量的保證就是必須對需要分析的目標(具體到每一個基礎指令)進行最深入的理解,認識和解釋。

我們將工作進行了分解; 每一部分基於不同指令組的尺寸。 這個工作結束後,我們有了全部的虛擬碼的尺寸,我們作出了一個能夠產生比較象樣的反編譯程式碼的工具(WKTVBDE)。 勿容置疑地說,由於不可避免的人為錯誤,我們必須不斷修正操作碼的尺寸定義。甚至今天,我們覺得仍有一些錯誤存在。因為一些指令並沒有在所有的應用程式中被使用,全部測試它們是不可能的。到目前為止,對於我們的Debugger (WKTVBDE 1.3)中還沒有錯誤的操作碼被報告。1


* 第三步

增加Debugger的基本功能特性

這一步有更復雜的編碼和需要研究的問題。新增輸出表(Export tTble)並不太難,這一步由載入器(loader)透過分析PE 檔案頭來實現。 然後Debugger 將獲得跳轉地址表。 隨後,我們構造並建立一個斷點狀態表(啟用/非啟用/空)。 如此,我們可以在VB虛擬機器的任何API處設定斷點。 一個類似的方法是在偽指令上設定斷點。 與常見的一般偵錯程式(Debuggers)不同: 我們可以在任何一個給定的指令上設定斷點。因為我們的Debugger在每一個操作碼被虛擬機器執行前已經全面控制了它們。

隨後,我們在實際的程式碼中加入斷點。比如在一個給定了地址的操作碼上設定斷點。 這些斷點被儲存在我們的Debugger中一個動態連線表中。這有幾個優勢:斷點數量不受限制;對於大量的斷點資訊,可以動態調整記憶體的使用。

斷點的基本功能並不複雜。簡單地說,當Debugger獲得控制,它為了自身的應用,儲存操作碼地址快取區內容。 隨後Debugger顯示這個地址,並將儲存的斷點與之比較。如果這個操作碼地址已經存在於使用者斷點設定列表中(狀態為:活動),Debugger則停止被除錯的VB應用程式的執行。 記憶體編輯器/檢視器允許被除錯的程式被編輯、檢視和記憶體內容轉存。

我們已經嘗試儘量最佳化和確認指標操作的有效性。但我們依然無法全部避免和排除那些無效的記憶體訪問操作(雖然象這樣的非法操作在Debugger的記憶體編輯器中幾乎是不可能的發生的)。 有一種可能,當你在除錯程式進行記憶體修改時,毫無理由的程式突然中止。 這種情況可能發生在我們使用不當的操作碼替換原來的操作碼的時候(比如操作碼要求的引數個數不同),這種行為可能導致虛擬機器在解釋執行時發生執行錯誤中斷。

當使用者清楚自己的除錯行為時這種狀況一般不會發生。在Debugger的新版中,我們將控制這種狀況的出現,儲存發生錯誤之前的各種狀態,並允許程式繼續處理隨後的正常指令。無論如何這種狀況僅發生在使用SoftICE替換了錯誤的程式碼時。 如果我們搞亂了什麼,出現錯誤的結果是合乎邏輯的;-)。

現在,所有的處理過程已經被圖形化地表現在Debuggers Window視窗中。我們使用不同的顏色程式碼來標識我們已經設定了斷點的偽指令行。 我們的Debugger由顯示一個控制列表框開始。 有如你所見,我們採用了懷舊式的顏色調配方案。 為了方便使用者使用,快速上手,我們嘗試採用一種符合使用者平常使用習慣的主程式介面設計方案(我們的Debugger採用了基於綠色,黑色的配色方案)。

Mr. Silver
--------------------------------------------------------------------------------
& 1998-2003 CrackZ. May 2003。

相關文章