Windows XP中的新型向量化異常處理

當我安裝完Windows XP Beta(以前代號爲“Whistler”)時,並沒有指望能夠看到許多新的API,結果卻驚喜地發現我錯了!在本月的專欄中,我就要講述這些新增功能其 中之一——向量化異常處理(Vectored Exception Handling)。
 
當運行我在1997年11 月MSJ 雜誌Under The Hood 專欄 中 介紹的PEDIFF程序時,我發現了向量化異常處理。你告訴PEDIFF兩個不同版本DLL的路徑,它就會返回這兩個DLL導出的不同函數。這一次我比較 了Windows 2000中的KERNEL32.DLL和Windows XP中的KERNEL32.DLL,結果發現了向量化異常處理。Windows XP中的KERNEL32.DLL中新增了許多API,但我一眼就注意到了AddVectoredExceptionHandler這個API。在最新的 MSDN® Library中有這個API的文檔,因此我也不需要挖掘這方面的信息了。
 
由於Beta 2版本中的WINBASE.H有一個問題,因此你需要安裝Platform SDK RC1發行版才能編譯本文中所講的代碼。
 
快速回顧結構化異常處理
到底什麼是向量化異常處理,爲什麼要關注它呢?首先讓我們來快速回顧一下通常的異常處理機制,這樣你就會明白向量化異常處理與它之間的區別了。如果你使用的是像C++那樣的支持異常處理的語言,你很可能已經知道Win32結構化異常處理( Structured Exception Handling ,SEH)了。在C++中是使用try/catch語句或者是Microsoft C/C++編譯器擴展的__try/__except語句來實現結構化異常處理的。關於SEH的內部工作機制,可以參考我在1997年1月的MSJ 雜誌上發表的文章“深入探索Win32 結構化異常處理 ”。
簡 單地說,結構化異常處理使用了基於堆棧的異常結點。當你使用try塊時,有關異常處理程序的信息被保存在當前過程(函數)的堆棧幀上。在x86平臺 上,Microsoft使用了保存在FS:[0]處的指針來指向當前的異常處理程序幀。這個幀中包含了異常發生時需要被調用的代碼的地址。
如 果你在一個try塊中調用另一個函數,這個新函數可能設置它自己的異常處理程序。此時在堆棧上就創建了一個新的異常處理程序幀並且有一個指針指向前一個異 常處理程序幀,如圖1所示。實際上,所有的SEH幀形成了一個鏈表,FS:[0]指向這個鏈表的頭部。這裏要注意的關鍵地方是:鏈表中每個後繼結點必須處 於線程堆棧上更高的位置。操作系統強制實行這個特別的規定,這就意味着你不能隨意地將自己的異常處理程序插入到這個鏈表中。
 
圖1 堆棧上的異常處理程序
 
所 有的異常處理幀在堆棧上以鏈表的形式存在並不是SEH中的一個小細節,它對SEH的正常工作是至關重要的。當異常發生時,操作系統從這個鏈表的頭部開始, 用類似於“現在發生了一個異常,你想處理它嗎?”這樣的代碼來調用這個異常處理程序。異常處理程序可以修復出錯的問題並返回 EXCEPTION_CONTINUE_EXECUTION來表示它處理了這個異常。
異 常處理程序也可以通過返回EXCEPTION_CONTINUE_SEARCH來表示它拒絕處理異常。當發生這種情況時,操作系統移向鏈表中的下一個結 點,並詢問同樣的問題。這個過程一直進行下去,直至某個異常處理程序選擇處理這個異常或者到鏈表末尾。我在這裏極大地簡化了SEH的細節,但這對於我們來 說已經足夠了。
SEH 如此設計導致的後果是什麼呢?一個很重要的地方是一個異常處理程序可以自由選擇某個異常發生時所採取的動作而不用顧及在它前面安裝的異常處理程序(在鏈表中位於後面)想採取的動作。但是,有時候這可能是個要關心的主要問題。下面這個例子解釋了其中的緣由。
 
假設你寫出了世界上最棒的異常處理程序。當異常發生時,你的異常處理程序可以診斷這個問題,記錄相關的細節並解決這個問題。並且你把它放到了你的main(或WinMain)函數中,以便它可以保護整個程序。
 
現 在,假設在你的程序的某個地方調用了一個外部組件,這個組件是不受你控制的。它也安裝了一個異常處理程序,但這個異常處理程序寫的不好。它遇到一個異常時 就讓進程退出了。這樣,你的異常處理程序根本就沒有得到機會執行,因爲在異常處理程序鏈表中一個出現在它前面的異常處理程序處理了這個異常。總之,SEH 這個極好的設計讓一個事實將其威力減弱了,那就是,只有在調用鏈中更深處的代碼不安裝它們的異常處理程序時當前的異常處理程序才能發揮作用。
 
請 允許我在開始講述向量化異常處理之前再提一些與SEH有關的內容。當一個正在被調試的程序中產生異常時,異常處理過程會增加一些步驟。首先,調試器被給予 第一次機會來處理這個異常,或者讓其子進程(即被調試程序)處理。如果子進程處理這個異常,那接下來就是前面講的步驟。如果子進程中沒有相應的異常處理程 序來處理這個異常,調試器就會接到第二次處理機會的通知。(通常調試器彈出一個“未處理異常”對話框就是在這個時候。)此時,只有結束進程了。
向量化異常處理
    簡單地說,向量化異常處理類似於正常的SEH,但有以下三點關鍵區別:
  • 異常處理程序既不針對於特定的函數,也不針對於堆棧幀。
  • 編譯器中並不存在將新的異常處理程序添加到異常處理程序鏈表中的關鍵字(例如try或catch)。
  • 向量化異常處理程序是由你在自己的代碼中明確添加的,而不是伴隨try/catch之類的語句而產生的。
新增的AddVectoredExceptionHandler 這個API將一個指向函數的指針作爲參數,把這個函數的地址添加到已註冊的異常處理程序鏈表中。由於系統使用一個鏈表來保存向量化異常處理程序,因此一個程序想安裝多少個向量化異常處理程序都可以。
 
向 量化異常處理與結構化異常處理是如何共存的呢?在Windows XP中,當一個進程中產生異常時,向量化異常處理程序鏈表先於正常的SEH鏈表被處理。這可以很好地解決了現存代碼的兼容性問題。如果向量化異常處理程序 鏈表在SEH鏈表之後被處理,SEH異常處理程序可能處理那個異常,這樣向量化異常處理程序就沒有機會處理它了。
 
在調試方面,向量化異常處理與結構化異常處理類似。也就是說,當一個程序被調試時,調試器仍然能夠在目標進程(也就是被調試程序)處理異常之前得到第一次通知。只有當調試器選擇將異常傳遞給子進程時(通常是這樣),向量化異常處理程序才被調用。
 
AddVectoredExceptionHandler 函數被聲明在WINBASE.H文件中:
WINBASEAPI PVOID WINAPI AddVectoredExceptionHandler(
                           ULONG FirstHandler,
                            PVECTORED_EXCEPTION_HANDLER VectoredHandler );
(  Windows Server™ 2003 和Windows Vista  SDK 的頭文件中此函數的參數名稱分別爲First和Handler,但相應的文檔卻仍是上面這個樣子)
 
此 函數的第一個參數告訴系統這個異常處理程序應該放在異常處理程序鏈表的開頭還是結尾。異常處理程序鏈表並不針對於任何線程,它對進程來說是全局的。因此雖 然你可以請求系統將它放在鏈表的開頭,但卻無法保證它會被首先調用。因爲如果其它代碼在你調用此函數之後才調用此函數並也請求被放在鏈表開頭,那你就不會 被首先調用。無論AddVectoredExceptionHandler何時被調用,新安裝的異常處理程序總是要麼被放在鏈表開頭,要麼被放在鏈表末 尾。
 
第二個參數是一個異常處理函數的地址。它的原型如下:
LONG NTAPI VectoredExceptionHandler(PEXCEPTION_POINTERSExceptionInfo



);

ExceptionInfo 參 數是一個指針,它指向一個結構,這個結構中包含了這個函數可以獲取的關於異常的所有信息,例如異常類型、地址以及異常產生時寄存器的值。這個函數或者返回 EXCEPTION_CONTINUE_SEARCH,或者返回EXCEPTION_CONTINUE_EXECUTION。
 
當 返回EXCEPTION_CONTINUE_EXECUTION時,系統會嘗試繼續執行出錯進程。如果是這樣的話,出現在這個處理程序後面的向量化異常處 理程序以及所有的結構化異常處理程序都不會被調用。當返回EXCEPTION_CONTINUE_SEARCH時,系統移向下一個向量化異常處理程序。在 所有的向量化異常處理程序都被調用之後,系統開始處理結構化異常處理程序鏈表。
 
與AddVectoredExceptionHandler這個API相對的還有一個RemoveVectoredExceptionHandler,它從鏈表中移去前面安裝的處理程序。這個函數並沒有什麼特別的地方,爲了完整起見,我纔在這裏提起它。
 
搶佔正常的SEH處理過程是許多系統級的程序員長期以來夢寐以求的。然而直到使用向量化異常處理才使它完全變成了現實。 向量化異常處理程序可以返回 EXCEPTION_CONTINUE_EXECUTION , 從而導致鏈表中後續的處理程序都不被調用。但是某一部分代碼可能希望處理某些異常,這樣如果你不把異常傳遞過去可能會引入錯誤。Microsoft已經引 進了向量化異常處理這個極強的新功能,因此你不能粗心地認爲你的向量化異常處理程序是惟一註冊的異常處理程序而將它搞得一團糟。
 
全面展示向量化異常處理
 
       對於那些開發跟蹤和診斷工具的人來說,在需要時獲取控制權的經典方法是使用斷點。不幸的是,使用斷點就意味着要處理異常,具體來說就是斷點異常和單步異常。使用結構化異常處理機制來處理這些異常並不可行,因爲你並不能保證你的異常處理程序總能捕獲這些異常。
 
一些工具(例如 Mutek 的BugTrapper)通過覆蓋NTDLL中部分用戶模式下的異常處理代碼來解決這個問題。一個可能覆蓋的地方就是NTDLL中的KiUserExceptionDispatcher函數,我在前面提到的MSJ 中那篇有關結構化異常處理方面的文章中已經講過這個函數。雖然覆蓋KiUserExceptionDispatcher可以達到目的,但這種方案比較脆弱。一旦出現新版本的NTDLL,它很可能就失效了。
 
使用向量化異常處理就不需要做這些複雜的工作。如果所有的異常處理程序都按照我前面所說的那樣做的話,向量化異常處理就是一個整潔的、容易擴展的捕獲這些異常的方法。爲了演示向量化異常處理,我創建了一個使用 斷點來監視對LoadLibrary調用情況的小程序。在這個程序中,只要LoadLibrary被調用,它就可以打印出將要加載的DLL的名稱。
 
一 些高級用戶可能想到爲導入地址表(Import Address Table,IAT)打“補丁”來解決這個問題。它當然可以和我這裏使用的基於斷點的方法達到同樣效果。雖然在這種情況下你可以使用爲IAT打“補丁”的 方法來解決問題,但是它需要編寫許多代碼。你必須爲所有用到的DLL都打“補丁”,包括那些通過調用LoadLibrary來動態加載的DLL。請相信 我,這些工作實際做起來比看起來困難的多。使用斷點是所有這些方法中比較簡單的。
 
爲IAT打“補丁”遇到的第二個問題就是,它只適用於導出的函數。但是斷點技術卻適用於任何代碼,而不僅僅是導出的函數。因此它對於像攔截靜態鏈接到運行時庫(相對於使用MSVCRT.DLL)的代碼對malloc的調用之類的事情是非常有用的。
 
圖2 是 使用向量化異常處理來監視對LoadLibrary調用情況的那個DLL的代碼。每當LoadLibrary被調用時,VectoredExcBP就把要 加載的DLL的名稱寫到標準輸出中。這個DLL是自包含的,它並不需要任何特別的外部初始化。你只需要鏈接到它惟一的導出函數上就可以拿它做實驗了。
 
我也寫了一個調用LoadLibrary來加載幾個常見DLL的演示程序,它叫做TestVE(圖3 )。TestVE鏈接到VectoredExcBP.DLL導出的一個空函數上,這使得這個DLL在程序初始化時會被加載到內存當中。
 
當 VectoredExcBP被加載時,它的DllMain函數調用我的SetupLoadLibraryExWCallback函數。這個函數使用 AddVectoredExceptionHandler這個API來註冊一個異常處理程序。另外,它查找KERNEL32.DLL中的 LoadLibraryExW函數的地址,在這個函數的第一條指令上設置一個斷點。
 
VectoredExcBP 中 的主要部分是LoadLibraryBreakpointHandler函數。傳遞給AddVectoredExceptionHandler函數的就是 它的地址。每當異常發生時,這個函數就獲取控制權。它只處理兩個特別的異常。對於任何不感興趣的異常,它都返回 EXCEPTION_CONTINUE_SEARCH,以便讓其它異常處理程序來處理它。
 
我 在這裏並不過多地描述調試器方面的理論知識,只簡要地描述一下當斷點被觸發時和程序恢復執行時的事件順序。當CPU執行斷點指令時,首先生成一個 STATUS_BREAKPOINT異常。當它發生時,目標函數中的任何代碼都尚未執行。因此這個時候是檢查函數參數和進行其它操作的最佳時機。
 
由於斷點已經覆蓋了原來的指令,因此接下來就是恢復原來的指令以便它可以正常執行。一般來說,這並不是什麼大問題。但是這裏有一個問題。如果你恢復了原來的指令並讓它繼續執行,那麼你的斷點就不存在了,這樣你以後就不能再中斷在這個地方了。
 
解 決辦法(至少是在x86處理器上)是讓CPU每次執行一條指令(單步),然後將控制權返回給你,這樣你就可以重新插入斷點。在x86處理器上要想單步執行 需要設置CPU的EFlags寄存器中的跟蹤標誌(TF,有時也稱爲自陷標誌或陷阱標誌,值爲0x100)。當跟蹤標誌置位時,CPU每次僅執行一條指令 ,然後就生成一個STATUS_SINGLE_STEP異常。在接收到STATUS_SINGLE_STEP異常之後,將TF標誌清除,以便恢復程序的正常執行。
 
當你仔細查看LoadLibraryBreakpointHandler函數代碼時,會發現它與上面講的算法完全吻合。這段代碼在執行時相當謹慎,它檢查異常發生的地址是否是它所要處理的。該講的我在前面已經講過了,勿庸贅述。這段代碼包含了許多註釋,很容易理解。
 
在 對STATUS_BREAKPOINT的處理代碼中,LoadLibraryBreakpointHandler調用了一個我定義的名爲 BreakpointCallback的函數。這個函數使用異常發生時堆棧指針中的值來尋找參數的值。對於LoadLibrary函數來說,它只有一個參 數,那就是指向它要加載的DLL的名稱字符串的指針。BreakpointCallback函數獲取這個指針的值並輸出相應的字符串。(如果你想將這個 DLL用於非控制檯模式的應用程序的話,你當然也可以將printf改成類似OutputDebugString之類的函數。)
 
你 可能想知道我爲什麼會選擇監視LoadLibraryExW函數。有一個很好的理由可以解釋其中的緣由。因爲LoadLibrary函數帶一個字符串參 數,它同時有ANSI和UNICODE這兩個版本。LoadLibrary最常用的形式是LoadLibraryA。但實際上,LoadLibraryA 只是對LoadLibraryExA進行了簡單的封裝,而LoadLibraryExA只是對LoadLibraryExW進行了簡單的封裝。同 樣,LoadLibraryW也只是對LoadLibraryExW進行了簡單的封裝。這樣,所有調用DLL的方法最終都歸結爲調用 LoadLibraryExW函數。只需要在這個API上設置一個斷點,我就可以捕獲所有對LoadLibrary函數及其變種的調用。
 
如 果想實驗一下VectoredExcBP,確保你使用的是Windows XP Beta 2或者更新版本的操作系統,然後運行TestVE程序。TestVE自身只是通過調用LoadLibrary來加載兩個DLL(MFC42.DLL和 WININET.DLL)。但是這兩個DLL內部也要加載其它DLL,因此你會看到其它一些附加的對LoadLibrary的調用。如果一切正常,你會看 到類似下面的輸出結果:
 
LoadLibrary called on: MFC42
LoadLibrary called on: MSVCRT.DLL
LoadLibrary called on: G:/WINDOWS/System32/MFC42LOC.DLL
LoadLibrary called on: WININET
LoadLibrary called on: kernel32.dll
LoadLibrary called on: advapi32.dll
LoadLibrary called on: kernel32.dll
 
向量化異常處理的實現
Windows XP Beta 2 中的向量化異常處理在實現上相當簡單。雖然表面上 AddVectoredExceptionHandler 這個API是在KERNEL32.DLL中,但是它實際上轉發到了NTDLL中的RtlAddVectoredExceptionHandler函數上。圖4 是我爲這個函數寫的僞代碼。
向量化異常處理程序鏈表是一個循環鏈表(譯者注 )。每個已註冊的異常處理程序用一個12字節的結點表示,這個結點所佔用的內存在進程堆上分配。同時用一個臨界區對象來保護將異常處理程序結點插入到鏈表的頭部或尾部的這部分代碼。如果 FirstHandler 參數不爲0,則新的異常處理程序結點被插入到鏈表的頭部;否則它被插入到鏈表的尾部。就這麼簡單!它並沒有檢查這是不是以前曾經註冊過的某個結點進行的重複註冊,這樣某個異常處理程序可以註冊(因此也會被調用)多次。
在向量化異常處理的實現中另一個值得注意的地方是這些向量化異常處理程序被調用的方式。正如在我那篇關於SEH的文章中講到的那樣,KiUserExceptionDispatcher(在NTDLL中)調用了RtlDispatchException。圖5 說 明瞭有關向量化異常處理的代碼是如何被添加到NTDLL中的RtlDispatchException函數中的。如果你把它和我以前的那篇文章中的僞代碼 比較一下,會發現它僅在RtlDispatchException函數開頭添加了一個函數調用(調用 RtlCallVectorExceptionHandlers函數)。這充分證明了向量化異常處理程序是在結構化異常處理程序之前被調用的。
圖6 是RtlCallVectorExceptionHandlers函數的僞代碼。同樣,這段代碼也十分簡單。它使用一個臨界區對象來保護一個while循環。在這個循環中,它遍歷異常處理程序鏈表並調用相應的異常處理程序。如果異常處理程序返回EXCEPTION_CONTINUE_EXECUTION ,這個循環就直接退出而不再調用後續的處理程序。它負責返回一個值來指示RtlDispatchException是否應該去調用結構化異常處理程序。
正如你所料,我認爲向量化異常處理是Windows XP中添加的非常重要的功能。我已經期盼這個功能被添加到Win32中很長時間了。在本文中,我已經向你演示了使用向量化異常處理的極大優點,希望這個功能在以後會有新的應用。
 
附錄
 
圖2 VectoredExcBP
//===========================================================================
// VectoredExcBP – Matt Pietrek 2001
// MSDN Magazine, September 2001
//
// !^!^!^! 警告 警告 警告 !^!^!^!
//  這段代碼只能運行在Windows XP及其後繼操作系統上。
//  要想正確地編譯它,你必須做到:
//      A) 擁有一個定義了AddVectoredExceptionHandler函數的WINBASE.H文件
//      B) 確保編譯器最先找到的WINBASE.H文件是前面提到的那個而不是其它的。
//         如果你不能確定如何做,請參考編譯器文檔。
//      C) 定義_WIN32_WINNT=0x0500(或更高)
//
// 這段代碼在Windows XP Beta 2下用Visual C++ 6.0可以通過編譯並正常運行。
// 在寫這段代碼時,Windows XP還處於beta測試階段,因此某些內容可能改變,
// 其中包括API的行爲等等。不能保證它可以在將來的Windows系統上正常運行。
//===========================================================================
#include “stdafx.h”
 
#ifndef _M_IX86
#error “This code only runs on an x86 architecture CPU”
#endif
 
LONG NTAPI LoadLibraryBreakpointHandler(PEXCEPTION_POINTERS pExceptionInfo );
void BreakpointCallback( PVOID pCodeAddr, PVOID pStackAddr );
void SetupLoadLibraryExWCallback(void);
BYTE SetBreakpoint( PVOID pAddr );
void RemoveBreakpoint( PVOID pAddr, BYTE bOriginalOpcode );
 
// 全局變量
PVOID g_pfnLoadLibraryAddress = 0;
BYTE g_originalCodeByte;
 
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
 
BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
// 我們不需要接收有關線程啓動/停止時的通知,因此禁用它們
DisableThreadLibraryCalls( (HINSTANCE)hModule );
 
// 在進程啓動時設置斷點,進程退出時移除斷點
if ( DLL_PROCESS_ATTACH == ul_reason_for_call )
        SetupLoadLibraryExWCallback();
else if ( DLL_PROCESS_DETACH == ul_reason_for_call )
        RemoveBreakpoint( g_pfnLoadLibraryAddress, g_originalCodeByte );
 
return TRUE;
}
 
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
 
void SetupLoadLibraryExWCallback(void)
{
// 獲取LoadLibraryExW函數的地址。
// 所有對LoadLibraryA/W/ExA的調用最終都轉到了LoadLibraryExW
g_pfnLoadLibraryAddress=(PVOID)GetProcAddress(GetModuleHandle(“KERNEL32”),
                                                    “LoadLibraryExW” );
 
// 爲我們的斷點添加一個向量化異常處理程序
AddVectoredExceptionHandler( 1, LoadLibraryBreakpointHandler );
 
// 在LoadLibraryExW上設置斷點
g_originalCodeByte = SetBreakpoint( g_pfnLoadLibraryAddress );
}
 
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
// 這是LoadLibraryExW斷點的處理程序。當斷點被觸發時,調用相應的回調
// 函數(BreakpointCallback)。然後單步執行原來的指令並繼續執行下去。
// 實際上這需要處理兩個異常,正如代碼中所處理的那樣。
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
 
LONG NTAPI LoadLibraryBreakpointHandler(PEXCEPTION_POINTERS
                                        pExceptionInfo )
{
// printf( “In LoadLibraryBreakpointHandler: EIP=%p/n”,
//          pExceptionInfo->ExceptionRecord->ExceptionAddress );
 
LONG exceptionCode = pExceptionInfo->ExceptionRecord->ExceptionCode;
 
if ( exceptionCode == STATUS_BREAKPOINT)
{
        // 檢查它是否是我們設置的斷點。如果不是,將它傳遞給其它處理程序
        if ( pExceptionInfo->ExceptionRecord->ExceptionAddress
            != g_pfnLoadLibraryAddress )
        {
            return EXCEPTION_CONTINUE_SEARCH;
        }
 
        // 我們需要單步執行原來的指令,因此必須臨時移去斷點
        RemoveBreakpoint( g_pfnLoadLibraryAddress, g_originalCodeByte );
 
        // 調用我們的代碼以執行所需的任務
        BreakpointCallback( pExceptionInfo->ExceptionRecord->ExceptionAddress,
                            (PVOID)pExceptionInfo->ContextRecord->Esp );
 
        // 設置EFlags寄存器中的跟蹤標誌,以便我們在捕獲
        // STATUS_SINGLE_STEP 異常之前只執行一條指令(看下面)
        pExceptionInfo->ContextRecord->EFlags |= 0x00000100;
 
        return EXCEPTION_CONTINUE_EXECUTION;    // 重新執行出錯指令
}
else if ( exceptionCode == STATUS_SINGLE_STEP )
{
        // 檢查異常地址是否是上面產生的那個異常的地址的下一個地址
        // 如果不是,將它傳遞給其它異常處理程序
        // 譯者注: 由於Windows XP SP2增強了系統的安全性,實際上許多
        // 系統API的prolog代碼已經發生了變化。通常情況下系統API的第
        // 一條指令爲PUSH EBP,它的機器碼爲0x55,佔一個字節,因此作者
        // 才說要檢查異常地址是否是上面產生的那個異常的地址的下一個地址
        // (+1)。而在Windows XP SP2中,LoadLibraryExW的第一條指令變成
        // 了PUSH 34h,它的機器碼爲0x6A34,佔兩個字節,因此我們需要將
        // 其改爲+2才能正確執行
        if ( pExceptionInfo->ExceptionRecord->ExceptionAddress
            != (PVOID)((DWORD_PTR)g_pfnLoadLibraryAddress+2 ) )
        {
            return EXCEPTION_CONTINUE_SEARCH;
        }
 
        // printf( “In STATUS_SINGLE_STEP handler/n” );
 
        // 我們已經單步執行完了原來的指令,因此重新設置斷點
        SetBreakpoint( g_pfnLoadLibraryAddress );
 
        // 清除前面設置的跟蹤標誌
        pExceptionInfo->ContextRecord->EFlags &= ~0x00000100;
 
        return EXCEPTION_CONTINUE_EXECUTION;    // 繼續運行!
}
else    // 不是斷點異常或單步異常。它不是我們處理的目標!
{
        return EXCEPTION_CONTINUE_SEARCH;
}
}
 
/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
// 當LoadLibraryExW被調用時就調用此函數。它的參數爲斷點的地址和堆棧指針。
// 堆棧指針可以用來獲取堆棧上的參數值。此時我們想獲取的是表示將要加載的
// DLL 的名稱的字符串(UNICODE格式)。
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
void BreakpointCallback( PVOID pCodeAddr, PVOID pStackAddr )
{
DWORD nBytes;
 
LPWSTR pwszDllName;
 
// pStackAddr+0 == 返回地址
// pStackAddr+4 == 第一個參數
ReadProcessMemory( GetCurrentProcess(),
                        (PVOID)((DWORD_PTR)pStackAddr+4),
                        &pwszDllName, sizeof(pwszDllName),
                        &nBytes );
   
printf( “LoadLibrary called on: %ls/n”, pwszDllName );
}
 
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
// 在指定地址處設置斷點,返回這個地址處原來的內容。
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
BYTE SetBreakpoint( PVOID pAddr )
{
DWORD nBytes;
BYTE bOriginalOpcode;
 
// 讀取指定地址處的內容
ReadProcessMemory( GetCurrentProcess(), pAddr,
                        &bOriginalOpcode, sizeof(bOriginalOpcode),
                        &nBytes);
 
// 設置斷點
BYTE bpOpcode = 0xCC;
WriteProcessMemory( GetCurrentProcess(), pAddr,
                        &bpOpcode, sizeof(bpOpcode),
                        &nBytes );
 
return bOriginalOpcode;
}
 
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
// 將斷點處原來的內容寫回到那個地址
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
void RemoveBreakpoint( PVOID pAddr, BYTE bOriginalOpcode )
{
DWORD nBytes;
 
WriteProcessMemory( GetCurrentProcess(), pAddr,
                        &bOriginalOpcode, sizeof(bOriginalOpcode),
                        &nBytes );
}
 
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
extern “C” void __declspec(dllexport) VectoredExcBP_ExportedAPI(void)
{
// 這個函數什麼也不做。導出它以便其它EXE可以鏈接到這個DLL上。
}
 
圖3 TestVE
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
 
// 由VectoredExcBP.DLL導出的函數的原型聲明
Extern “C” void VectoredExcBP_ExportedAPI(void);
 
int main()
{
// 加載兩個DLL,在它們內部會加載其它DLL。
LoadLibrary( “MFC42” );
 
LoadLibrary( “WININET” );
 
// 調用VectoredExcBP.DLL導出的函數。
// 這條語句可以強制加載這個DLL
VectoredExcBP_ExportedAPI();
 
return 0;
}
 
圖4 RtlAddVectoredExceptionHandler函數僞代碼
 
struct _VECTORED_EXCEPTION_NODE
{
DWORD   m_pNextNode;
DWORD   m_pPreviousNode;
PVOID   m_pfnVectoredHandler;
}
 
CRITICAL_SECTION RtlpCalloutEntryLock;
_VECTORED_EXCEPTION_NODE *RtlpCalloutEntryList;
 
RtlAddVectoredExceptionHandler( ULONG FirstHandler,
                               PVECTORED_EXCEPTION_HANDLER VectoredHandler )
{
// 爲新結點分配空間
PVOID pExcptNode = HeapAlloc( GetProcessHeap(), 0, 0xC );
if ( !pExcptNode )
        return 0;
 
pExcptNode->m_pfnVectoredHandler = VectoredHandler;
   
RtlEnterCriticalSection( &RtlpCalloutEntryLock );
 
if ( FirstHandler )
{
        pExcptNode->m_pNextNode = RtlpCalloutEntryList->m_pNextNode;
        pExcptNode->m_pPreviousNode = &RtlpCalloutEntryList;
        RtlpCalloutEntryList->m_pNextNode->m_pPreviousNode = pExcptNode;
        RtlpCalloutEntryList->m_pNextNode = pExcptNode;
}
else
{
        pExcptNode->m_pNextNode = &RtlpCalloutEntryList;
        RtlpCalloutEntryList->m_pPreviousNode->m_pNextNode = pExcptNode;
        pExcptNode->m_pPreviousNode = RtlpCalloutEntryList->m_pPreviousNode;
        RtlpCalloutEntryList->m_pPreviousNode = pExcptNode;
}
 
RtlLeaveCriticalSection( &RtlpCalloutEntryLock );
 
return pExcptNode;
}
 
圖5 RtlDispatchException 函數的僞代碼
 
RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
{
DWORD    stackUserBase;
DWORD    stackUserTop;   
PEXCEPTION_REGISTRATION pRegistrationFrame;
DWORD hLog;
 
// 這一句是新增加的代碼
RtlCallVectoredExceptionHandlers( pExcptRec, pContext );
 
// 從FS:[4]和FS:[8]處獲取堆棧界限
RtlpGetStackLimits( &stackUserBase, &stackUserTop );
 
pRegistrationFrame = RtlpGetRegistrationHead();
 
// 其餘部分省略……
 
圖6 RtlCallVectoredExceptionHandlers 函數的僞代碼
 
// 此函數由RtlDispatchException調用
RtlCallVectoredExceptionHandlers( PEXCEPTION_RECORD pExcptRec,CONTEXT * pContext )
{
bool bContinueExecution = FALSE;
 
// 用臨界區保護異常回調
RtlEnterCriticalSection( &RtlpCalloutEntryLock );
 
// 獲取鏈表頭部
pCurrentNode = RtlpCalloutEntryList;
 
// 當我們還沒有處理這個結點時……
while ( pCurrentNode != RtlpCalloutEntryList )
{
        // 調用異常處理函數
        EXCEPTION_POINTERS pExceptionPointers
        LONG disposition = pCurrentNode->m_pfnVectoredHandler( &pExcptRec );
 
        // 如果處理程序允許恢復執行,中斷我們的循環
        if ( disposition == EXCEPTION_CONTINUE_EXECUTION )
        {
            bContinueExecution = TRUE;
            break;
        }
 
        // 移向下一個結點
        pCurrentNode = pCurrentNode->m_pNextNode;
}
 
RtlLeaveCriticalSection( &RtlpCalloutEntryLock );
 
return bContinueExecution;
}
 
譯者注:
 
在Windows XP SP2中,譯者發現向量化異常處理程序鏈表並不是循環鏈表,而是一個雙向鏈表,這個鏈表的頭結點是一個全局變量。如果你對Windows中的鏈表處理比較 熟悉的話,你會知道,在Windows中遇到鏈表時一般會使用SINGLE_LIST_ENTRY和LIST_ENTRY這兩個結構並結合系統自身提供的 API(例如InsertHeadList 和RemoveHeadList等,實際上它們都是內聯函數或宏, NTDDK.H和WDM.H中都有它們的源代碼)來處理,而且系統要求所處理的鏈表都必須有頭結點。下面是我根據作者的僞代碼爲Windows XP SP2中的這些函數寫的僞代碼。
 
VEH.H 文件
 
typedef struct _VECTORED_EXCEPTION_NODE {
    LIST_ENTRY ListEntry;
    PVECTORED_EXCEPTION_HANDLER pfnHandler; // 該指針出於安全目的已經被加密
} VECTORED_EXCEPTION_NODE, *PVECTORED_EXCEPTION_NODE;
 
// 下面兩個全局變量由Windows加載器進行初始化
// 實際進行初始化的是LdrpInitializeProcess函數
 
// 此變量負責保護鏈表的插入與刪除操作,由Windows加載器調用
// RtlInitializeCriticalSection 對其進行初始化
RTL_CRITICAL_SECTION RtlpCalloutEntryLock;
 
// 此變量是向量化異常處理鏈表的頭結點,由Windows加載器調用
// InitializeListHead 對其進行初始化
// 關於Windows中鏈表操作方面的詳細信息請參考DDK文檔中
// "Singly- and Doubly-Linked Lists" 一節
LIST_ENTRY RtlpCalloutEntryList;
 
VEH.C 文件
 
 
#include <windows.h>
#include "veh.h"
 
// 此原型來自於Windows Server 2003 R2 SDK中的WINBASE.H文件
PVOID WINAPI RtlAddVectoredExceptionHandler (
                    ULONG First,
                    PVECTORED_EXCEPTION_HANDLER Handler)
{
    PVECTORED_EXCEPTION_NODE pCurrentNode = (PVECTORED_EXCEPTION_NODE)
        RtlAllocateHeap( GetProcessHeap(), 0, sizeof(VECTORED_EXCEPTION_NODE) );
 
    if (!pCurrentNode)
    {
        return 0;
    }
 
    // EncodePointer 函數實際轉發到了NTDLL.DLL中的RtlEncodePointer函數上
    pCurrentNode->pfnHandler = RtlEncodePointer(Handler);   // 出於安全目的加密
 
    // EnterCriticalSection 函數實際轉發到了NTDLL.DLL中的
// RtlEnterCriticalSection 函數上
    RtlEnterCriticalSection(&RtlpCalloutEntryLock);
 
    if (First)
    {
        InsertHeadList(&RtlpCalloutEntryList, &pCurrentNode->ListEntry);
    }
    else
    {
        InsertTailList(&RtlpCalloutEntryList, &pCurrentNode->ListEntry);
    }
 
    RtlLeaveCriticalSection(&RtlpCalloutEntryLock);
 
    return pCurrentNode;
}
 
 
ULONG WINAPI RemoveVectoredExceptionHandler (PVOID Handler)
{
    PVECTORED_EXCEPTION_NODE pCurrentNode, pRemovedNode;
    BOOL bHandlerExist = FALSE;
 
    RtlEnterCriticalSection(&RtlpCalloutEntryLock);
 
    for ((PLIST_ENTRY)pCurrentNode = RtlpCalloutEntryList.Flink;
         (PLIST_ENTRY)pCurrentNode !=&RtlpCalloutEntryList;
         (PLIST_ENTRY)pCurrentNode=pCurrentNode->ListEntry.Flink)
    {
        if (pCurrentNode == (PVECTORED_EXCEPTION_NODE)Handler)
        {
            pRemovedNode = pCurrentNode;
            RemoveEntryList(&pCurrentNode->ListEntry);
 
            bHandlerExist = TRUE;
            break;
        }
    }
 
    RtlLeaveCriticalSection(&RtlpCalloutEntryLock);
 
    if (bHandlerExist)
    {
        RtlFreeHeap(GetProcessHeap(), 0, (LPVOID)pRemovedNode);
 
        return 1;
    }
 
    return 0;
}
 
 
int WINAPI RtlCallVectoredExceptionHanlers (
                PEXCEPTION_RECORD pExcptRec,
                PCONTEXT pContext)
{
    EXCEPTION_POINTERS ExceptionPointers;
 
    // 檢查鏈表是否爲空
    if (RtlpCalloutEntryList.Flink == &RtlpCalloutEntryList)
    {
        return 0;
    }
    else
    {
        PVECTORED_EXCEPTION_NODE pCurrentNode;
        BYTE retValue = 0;
 
        ExceptionPointers.ExceptionRecord = pExcptRec;
        ExceptionPointers.ContextRecord   = pContext;
 
        RtlEnterCriticalSection(&RtlpCalloutEntryLock);
 
        for((PLIST_ENTRY)pCurrentNode=RtlpCalloutEntryList.Flink;
            (PLIST_ENTRY)pCurrentNode != &RtlpCalloutEntryList;
            (PLIST_ENTRY)pCurrentNode=pCurrentNode->ListEntry.Flink)
        {
LONG disposition = ((PVECTORED_EXCEPTION_HANDLER)
(RtlDecodePointer(pCurrentNode->pfnHandler)))
(&ExceptionPointers);
            if (disposition == EXCEPTION_CONTINUE_EXECUTION)
            {
                retValue = 1;
                break;
            }
        }
 
        RtlLeaveCriticalSection(&RtlpCalloutEntryLock);
 
        return retValue;
    }
}
 
RtlDispatchException.c 文件
 
RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
{
    DWORD    stackUserBase;
    DWORD    stackUserTop;   
    PEXCEPTION_REGISTRATION pRegistrationFrame;
    DWORD hLog;
  BYTE retValue = 0;
 
    // 這一句是新增加的代碼
    if (RtlCallVectoredExceptionHandlers( pExcptRec, pContext ))
    {
      retValue = 1;
    }
  else
  {
    // 從FS:[4]和FS:[8]處獲取堆棧界限
      RtlpGetStackLimits( &stackUserBase, &stackUserTop );
 
      pRegistrationFrame = RtlpGetRegistrationHead();
      // 其餘部分省略……
  }
 
  return retValue;
}
 
對於上面的僞代碼,我要說的有以下幾點:
一、 對全局變量RtlpCalloutEntryList類型的識別。這個變量的名稱是公開的,當我用   SoftICE 定位到這個變量的地址時看到以下內容: 
很明顯,這個變量佔8個字節,然後結合Windows處理鏈表時對鏈表的要求以及
RtlAddVectoredExceptionHandler 的反彙編代碼可以得出它是一個LIST_ENTRY結構。
二、 這兩個全局變量的初始化。在這兩個變量位置上設置內存寫斷點很容易找出是哪段代碼對其進行初始化的,並且是如何初始化的。前面註釋中已經給出,不再贅述。
三、 作者給出的僞代碼中的循環是用的while語句,根據這些函數的反彙編代碼,其中進入循環時的語句形式爲
MOV ……(一條或多條MOV指令)
JMP ……
這明顯是編譯器爲for語句生成的指令序列,故我在僞代碼中使用了for循環語句。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章