NetRoc
本文從主站點轉貼過來的,附件和pdf請訪問http://www.DbgTech.net/下載
一、簡介
《Windows高級調試》第一章中提到了一個基於Microsoft Detours庫的內存泄露檢查工具LeakDiag。本文對這個庫進行一些介紹。
一句話來說,Detours是一個用來在二進制級別上對程序中的函數(Function)或者過程(Procedure)進行修改的工具庫。一般我們將這種技術稱爲"Hook"。Detours的實現原理是將目標函數的前幾個字節改爲jmp指令跳轉到自己的函數地址,以此接管對目標函數的調用,並插入自己的處理代碼。在現實中,這種技術可以應用在很多場景下。比如Hook某些Windows API,在實際調用到系統函數前進行一些過濾工作;軟件中使用到了一些沒有源代碼的第三方庫,但是又想增強其中某些函數的功能,等等。
圖1 Hook前後的程序執行流程對比。
圖2 Hook前後目標函數和跳板代碼的改變
Detours相對其他一些Hook庫和自己實現的代碼來說,通常有以下這些優點:
- 考慮全面,代碼非常穩定,並且經過了微軟自己衆多產品的驗證。
- 可以簡單的用純C/C++代碼實現對類的成員函數的Hook。
- 購買版權之後的Detours Professional還可以支持x64和IA64處理器。以此爲基礎編寫的代碼擁有更強的可移植性。
- 使用簡單,不需要了解彙編指令以及技術細節。
二、使用方法
一般來說,使用Detours的代碼都具有固定的模式。Detours 1.5和Detours 2.1的接口函數變了很多,這裏按照2.1版本對基本的使用方法進行說明。
常用的函數有下面幾個:
- DetourTransactionBegin() :開始一次Hook或者Unhook過程。
- DetourUpdateThread() :列入一個在DetourTransaction過程中要進行update的線程。這個函數的作用稍微有一些複雜,會在後面專門說明。
- DetourAttach() :添加一個要Hook的函數。
- DetourDetach () :添加一個要Unhook的函數。
- DetourTransactionCommit() :執行當前的Transaction過程。在這個函數中才會真正進行Hook或者Unhook操作。前面三個函數都只是做一些記錄工作。
在使用的時候,這幾個函數的調用步驟基本上也是按照上面列出來的順序。舉例來說,現在想Hook掉API函數MessageBoxA,將消息框彈出的消息修改掉,可以按下面的方法做。
進行Hook的步驟:
- 首先需要定義目標函數的原型。如果目標函數是Windows API,可以到MSDN中查閱,但是需要注意ANSI版本和Unicode版本的區別。如果沒有確切的原型聲明,或者目標函數是通過逆向工程找出來的,那麼需要定義一個和目標函數原型兼容的聲明,即參數個數和調用約定要相同。如MessageBoxA的原型是:
int MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
使用typedef定義如下:
typedef int (WINAPI *pfnMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
- 聲明一個指向目標函數的函數指針:
pfnMessageBoxA g_pMessageBoxA = ::MessageBoxA;
- 編寫Hook函數的代碼,用於替換目標函數。
- 調用DetourTransactionBegin開始一次Detours事務。
- 對進程中每個可能調用到目標函數的線程,都需要使用DetourUpdateThread加入到update隊列中。這是因爲Hook時修改目標函數的前幾個字節,如果某個線程剛好執行到這幾個字節的位置時,粗暴的修改掉會造成該線程出現異常。Detours事務處理時,會先枚舉並暫停update隊列中所有線程,獲取它們的指令指針,如果發現這種情況,則將指令指針修改到跳板代碼的對應字節上。這樣就避免出現崩潰的問題。
- 對每個需要Hook的函數,調用DetourAttach加入到事務列表中。
- 調用DetourTransactionCommit進行實際的Hook操作。
Unhook的過程和上面的流程基本一樣,只是第6步改爲調用DetourDetach函數。
Hook MessageBoxA的完整示例代碼如下:
//Hook函數的向前聲明
int WINAPI Hook_MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
//目標函數原型聲明
typedef int (WINAPI *pfnMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
//指向目標函數的指針
pfnMessageBoxA g_pMessageBoxA = ::MessageBoxA;
BOOL StartHook()
{//開始Hook
DetourTransactionBegin();
//只有一個線程,所以GetCurrentThread
DetourUpdateThread( GetCurrentThread());
//添加MessageBoxA的Hook
if( DetourAttach( &(PVOID&)g_pMessageBoxA, Hook_MessageBoxA) != NO_ERROR)
{
printf( "Hook MessageBoxA fail./n");
}
//完成事務
if( DetourTransactionCommit() != NO_ERROR)
{
printf( "DetourTransactionCommit fail/n");
return FALSE;
}
else
{
printf( "DetourTransactionCommit ok/n");
return TRUE;
}
}
BOOL StopHook()
{//停止Hook
DetourTransactionBegin();
DetourUpdateThread( GetCurrentThread());
if( DetourDetach( &(PVOID&)g_pMessageBoxA, Hook_MessageBoxA) != NO_ERROR)
{
printf( "Hook MessageBoxA fail./n");
}
if( DetourTransactionCommit() != NO_ERROR)
{
printf( "DetourTransactionCommit fail/n");
return FALSE;
}
else
{
printf( "DetourTransactionCommit ok/n");
return TRUE;
}
}
int WINAPI Hook_MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
//需要調用原函數時,可以直接使用前面定義的指針變量
return g_pMessageBoxA( hWnd, "MessageBox after hook.", "TestDetours", MB_OK);
}
在附件的示例代碼中還包含了Hook類成員函數的代碼。流程和上面基本一致,只是需要用一些強制轉換來對付編譯器的類型檢查。
另外,Detours還包含一系列其他函數,如果需要使用的話,可以參考Detours安裝目錄下的示例。
三、使用Detours的注意事項
總體來說,Detours庫的代碼是非常穩定的,但是如果使用方法不對,會造成一些問題。有下面一些地方需要特別注意:
- 一定要枚舉線程並調用DetourUpdateThread函數。否則可能出現很低機率的崩潰問題,這種問題很難被檢查出來。
- 如果Hook函數在DLL中,那麼絕大多數情況下不能在Unhook之後卸載這個DLL,或者卸載存在造成崩潰的危險。因爲某些線程的調用堆棧中可能還包含Hook函數,這時卸載掉DLL,調用堆棧返回到Hook函數時內存位置已經不是合法的代碼了。
- Detours庫設計時並沒有考慮到卸載的問題,這是因爲鉤子的卸載本身是不安全的。當Detours庫代碼存在於DLL中的時候,即使Unhook了所有函數,清理了所有自己使用到的函數,還是會佔用一些內存。卸載這個DLL會造成內存泄露,特別是反覆的進行加載DLL->Hook->Unhook->卸載DLL的過程,會讓這個問題變得非常嚴重。後面會用一篇專題文章來討論Detours內存泄露問題的調試和解決。
- 有一些非常短的目標函數是無法Hook的。因爲jmp指令需要佔用一定空間,有些函數太過短小,甚至不夠jmp指令的長度,自然是沒有辦法Hook掉的。
Detours不支持9x內核的Windows系統。因爲9x內核下的內存模型和NT內核下有非常大的差別。