微軟研究院Detour開發包之API攔截技術
我們截獲函式執行最直接的目的就是為函式增添功能,修改返回值,或者為除錯以及效能測試加入附加的程式碼,或者截獲函式的輸入輸出作研究,破解使用。通過訪 問原始碼,我們可以輕而易舉的使用重建(Rebuilding)作業系統或者應用程式的方法在它們中間插入新的功能或者做功能擴充套件。然而,在今天這個商業 化的開發世界裡,以及在只有二進位制程式碼釋出的系統中,研究人員幾乎沒有機會可以得到原始碼。本文主要討論Detour在Windows二進位制PE檔案基礎 上的API截獲技術。對於Linux平臺,作這件事情將會非常的簡單,由於最初的作業系統設計者引入了LD_PRELOAD。如果你設定 LD_PRELOAD=mylib.so ,那麼應用程式在載入 dll時,會先檢視mylib.so的符號表,在relocation 的時候會優先 使用mylib.so 裡的 symbol 。假如你在mylib.so裡有個printf() ,那麼這個printf就會替代libc的 printf。 而在mylib.so裡的這個printf可以直接訪問 libc.so裡的printf函式指標來獲得真正的 printf的入口地 址。 這樣,所有的dll的API HOOK在loader載入dll的時候就已經完成,非常自然,和平臺相關的部分全部交給loader去處理。
一、 Detour開發庫:
簡介
Detours是一個在x86平臺上截獲任意Win32函式呼叫的工具庫。中斷程式碼可以在執行時動態載入。Detours使用一個無條件轉移指令來替換目 標函式的最初幾條指令,將控制流轉移到一個使用者提供的截獲函式。而目標函式中的一些指令被儲存在一個被稱為“trampoline” (譯註:英文意為蹦 床,雜技)的函式中,在這裡我覺得翻譯成目標函式的部分克隆/拷貝比較貼切。這些指令包括目標函式中被替換的程式碼以及一個重新跳轉到目標函式的無條件分 支。而截獲函式可以替換目標函式,或者通過執行“trampoline”函式的時候將目標函式作為子程式來呼叫的辦法來擴充套件功能。
Detours是執行時被插入的。記憶體中的目標函式的程式碼不是在硬碟上被修改的,因而可以在一個很好的粒度上使得截獲二進位制函式的執行變得更容易。例如, 一個應用程式執行時載入的DLL中的函式過程可以被插入一段截獲程式碼(detoured),與此同時,這個DLL還可以被其他應用程式按正常情況執行(譯 注:也就是按照不被截獲的方式執行,因為DLL二進位制檔案沒有被修改,所以發生截獲時不會影響其他程序空間載入這個DLL)。不同於DLL的重新連結或者 靜態重定向,Detours庫中使用的這種中斷技術確保不會影響到應用程式中的方法或者系統程式碼對目標函式的定位。
如果其他人為了除錯或者在內部使用其他系統檢測手段而試圖修改二進位制程式碼,Detours將是一個可以普遍使用的開發包。據我所知,Detours是第一 個可以在任意平臺上將未修改的目的碼作為一個可以通過“trampoline”呼叫的子程式來保留的開發包。而以前的系統在邏輯上預先將截獲程式碼放到目 標程式碼中,而不是將原始的目的碼做為一個普通的子程式來呼叫。我們獨特的“trampoline”設計對於擴充套件現有的軟體的二進位制程式碼是至關重要的。
出於使用基本的函式截獲功能的目的,Detours同樣提供了編輯任何DLL匯入表的功能,達到向存在的二進位制程式碼中新增任意資料節表的目的,向一個新進 程或者一個已經執行著的程序中注入一個DLL。一旦向一個程序注入了DLL,這個動態庫就可以截獲任何Win32函式,不論它是在應用程式中或者在系統庫 中。
基本原理
1. WIN32程序的記憶體管理
眾所周知,WINDOWS NT實現了虛擬儲存器,每一WIN32程序擁有4GB的虛存空間, 關於WIN32程序的虛存結構及其操作的具體細節請參閱WIN32 API手冊, 以下僅指出與Detours相關的幾點:
(1) 程序要執行的指令也放在虛存空間中
(2) 可以使用QueryProtectEx函式把存放指令的頁面的許可權更改為可讀可寫可執行,再改寫其內容,從而修改正在執行的程式
(3) 可以使用VirtualAllocEx從一個程序為另一正執行的程序分配虛存,再使用 QueryProtectEx函式把頁面的許可權更改為可讀可寫可執行,並把要執行的指令以二進位制機器碼的形式寫入,從而為一個正在執行的程序注入任意的程式碼 。
2. 攔截WIN32 API的原理
Detours定義了三個概念:
(1) Target函式:要攔截的函式,通常為Windows的API。
(2) Trampoline函式:Target函式的部分複製品。因為Detours將會改寫Target函式,所以先把Target函式的前5個位元組複製儲存好,一方面仍然儲存Target函式的過程呼叫語義,另一方面便於以後的恢復。
(3) Detour 函式:用來替代Target函式的函式。
Detours在Target函式的開頭加入JMP Address_of_ Detour_ Function指令(共5個位元組)把對Target函式 的呼叫引導到自己的Detour函式, 把Target函式的開頭的5個位元組加上JMP Address_of_ Target _ Function+ 5共10個位元組作為Trampoline函式。請參考下面的圖1和圖2。
(圖1:Detour函式的過程)
(圖2: Detour函式的呼叫過程)
說明:
目標函式:
目標函式的函式體(二進位制)至少有5個位元組以上。按照微軟的說明文件Trampoline函式的函式體是拷貝前5個位元組加一個無條件跳轉指令的話(如果沒 有特殊處理不可分割指令的話),那麼前5個位元組必須是完整指令,也就是不能第5個位元組和第6個位元組是一條不可分割的指令,否則會造成Trampoline 函式執行錯誤,一條完整的指令被硬性分割開來,造成程式崩潰。對於第5位元組和第6個位元組是不可分割指令需要調整拷貝到雜技函式(Trampoline)的 位元組個數,這個值可以檢視目標函式的彙編程式碼得到。此函式是目標函式的修改版本,不能在Detour函式中直接呼叫,需要通過對Trampoline函式 的呼叫來達到間接呼叫。
Trampoline函式:
此函式預設分配了32個位元組,函式的內容就是拷貝的目標函式的前5個位元組,加上一個JMP Address_of_ Target _ Function+5指令,共10個位元組。
此函式僅供您的Detour函式呼叫,執行完前5個位元組的指令後再絕對跳轉到目標函式的第6個位元組繼續執行原功能函式。
Detour函式:
此函式是使用者需要的截獲API的一個模擬版本,呼叫方式,引數個數必須和目標函式相一致。如目標函式是__stdcall,則Detour函式宣告也必須 是__stdcall,引數個數和型別也必須相同,否則會造成程式崩潰。此函式在程式呼叫目標函式的第一條指令的時候就會被呼叫(無條件跳轉過來的),如 果在此函式中想繼續呼叫目標函式,必須呼叫Trampoline函式(Trampoline函式在執行完目標函式的前5個位元組的指令後會無條件跳轉到目標 函式的5個位元組後繼續執行),不能再直接呼叫目標函式,否則將進入無窮遞迴(目標函式跳轉到Detour函式,Detour函式又跳轉到目標函式的遞迴, 因為目標函式在記憶體中的前5個位元組已經被修改成絕對跳轉)。通過對Trampoline函式的呼叫後可以獲取目標函式的執行結果,此特性對分析目標函式非 常有用,而且可以將目標函式的輸出結果進行修改後再傳回給應用程式。
Detour提供了向執行中的應用程式注入Detour函式和在二進位制檔案基礎上注入Detour函式兩種方式。本章主要討論第二種工作方式。通過 Detours提供的開發包可以在二進位制EXE檔案中新增一個名稱為Detour的節表,如下圖3所示,主要目的是實現PE載入器載入應用程式的時候會自 動載入您編寫的Detours DLL,在Detours Dll中的DLLMain中完成對目標函式的Detour。
(圖3)
二、 Detours提供的截獲API的相關介面
Detours的提供的API 介面可以作為一個共享DLL給外部程式呼叫,也可以作為一個靜態Lib連結到您的程式內部。
Trampoline函式可以動態或者靜態的建立,如果目標函式本身是一個連結符號,使用靜態的trampoline函式將非常簡單。如果目標函式不能在連結時可見,那麼可以使用動態trampoline函式。
要使用靜態的trampoline函式來截獲目標函式,應用程式生成trampoline的時候必須使用
DETOUR_TRAMPOLINE巨集。DETOUR_TRAMPOLINE有兩個輸入引數:trampoline的原型和目標函式的名字。
注意,對於正確的截獲模型,包括目標函式,trampoline函式,以及截獲函式都必須是完全一致的呼叫形式,包括引數格式和呼叫約定。當通過 trampoline函式呼叫目標函式的時候拷貝正確引數是截獲函式的責任。由於目標函式僅僅是截獲函式的一個可呼叫分支(截獲函式可以呼叫 trampoline函式也可以不呼叫),這種責任幾乎就是一種下意識的行為。
使用相同的呼叫約定可以確保暫存器中的值被正確的儲存,並且保證呼叫堆疊在截獲函式呼叫目標函式的時候能正確的建立和銷燬。
可以使用DetourFunctionWithTrampoline函式來截獲目標函式。這個函式有兩個引數:trampoline函式以及截獲函式的指標。因為目標函式已經被加到trampoline函式中,所有不需要在引數中特別指定。
我們可以使用DetourFunction函式來建立一個動態的trampoline函式,它包括兩個引數:一個指向目標函式的指標和一個截獲函式的指標。DetourFunction分配一個新的trampoline函式並將適當的截獲程式碼插入到目標函式中去。
當目標函式不是很容易使用的時候,DetourFindFunction函式可以找到那個函式,不管它是DLL中匯出的函式,或者是可以通過二進位制目標函式的除錯符號找到。
DetourFindFunction接受兩個引數:庫的名字和函式的名字。如果DetourFindFunction函式找到了指定的函式,返回該函式 的指標,否則將返回一個NULL指標。DetourFindFunction會首先使用Win32函式LoadLibrary 和 GetProcAddress來定位函式,如果函式沒有在DLL的匯出表中找到,DetourFindFunction將使用ImageHlp庫來搜尋有 效的除錯符號(譯註:這裡的除錯符號是指Windows本身提供的除錯符號,需要單獨安裝,具體資訊請參考Windows的使用者診斷支援資訊)。 DetourFindFunction返回的函式指標可以用來傳遞給DetourFunction以生成一個動態的trampoline函式。
我們可以呼叫DetourRemoveTrampoline來去掉對一個目標函式的截獲。
注意,因為Detours中的函式會修改應用程式的地址空間,請確保當加入截獲函式或者去掉截獲函式的時候沒有其他執行緒在程序空間中執行,這是程式設計師的責任。一個簡單的方法保證這個時候是單執行緒執行就是在載入Detours庫的時候在DllMain中呼叫函式。
三、 使用Detours實現對API的截獲的兩種方法
建立一個MFC對話方塊工程,在對話方塊的OK按鈕的單擊事件中加入對MessageBoxA函式的呼叫,編譯後的程式名稱MessageBoxApp,效果如圖。
(圖4)
靜態方法
建立一個Dll工程,名稱為ApiHook,這裡以Visual C++6.0開發環境,以截獲ASCII版本的MessageBoxA函式來說明。在Dll的工程加入:
DETOUR_TRAMPOLINE(int WINAPI Real_Messagebox(HWND hWnd ,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType), ::MessageBoxA);
生成一個靜態的MessageBoxA的Trampoline函式,在Dll工程中加入目標函式的Detour函式:
int WINAPI MessageBox_Mine( HWND hWnd ,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType)
{
CString tmp= lpText;
tmp+=” 被Detour截獲”;
return Real_Messagebox(hWnd,tmp,lpCaption,uType);
// return ::MessageBoxA(hWnd,tmp,lpCaption,uType); //Error
}
在Dll入口函式中的載入Dll事件中加入:
DetourFunctionWithTrampoline((PBYTE)Real_Messagebox, (PBYTE)MessageBox_Mine);
在Dll入口函式中的解除安裝Dll事件中加入:
DetourRemove((PBYTE)Real_Messagebox, (PBYTE)MessageBox_Mine);
動態方法
建立一個Dll工程,名稱為ApiHook,這裡以Visual C++6.0開發環境,以截獲ASCII版本的MessageBoxA函式來說明。在Dll的工程加入:
//宣告MessageBoxA一樣的函式原型
typedef int (WINAPI * MessageBoxSys)( HWND hWnd ,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType);
//目標函式指標
MessageBoxSys SystemMessageBox=NULL;
//Trampoline函式指標
MessageBoxSys Real_MessageBox=NULL;
在Dll工程中加入目標函式的Detour函式:
int WINAPI MessageBox_Mine( HWND hWnd ,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType)
{
CString tmp= lpText;
tmp+=” 被Detour截獲”;
return Real_Messagebox(hWnd,tmp,lpCaption,uType);
// return ::MessageBoxA(hWnd,tmp,lpCaption,uType); //Error
}
在Dll入口函式中的載入Dll事件中加入:
SystemMessageBox=(MessageBoxSys)DetourFindFunction("user32.dll","MessageBoxA");
if(SystemMessageBox==NULL)
{
return FASLE;
}
Real_MessageBox=(MessageBoxSys)DetourFunction((PBYTE)SystemMessageBox, (PBYTE)MessageBox_Mine);
在Dll入口函式中的解除安裝Dll事件中加入:
DetourRemove((PBYTE)Real_Messagebox, (PBYTE)MessageBox_Mine);
重寫二進位制可執行檔案
使用Detours自帶的SetDll.exe重寫二進位制可執行檔案,可以在需要截獲的程式中加入一個新的Detours的PE節表。對於本文就是新建一個批處理檔案呼叫SetDll.exe。
@echo off
if not exist MessageBoxApp.exe (
echo 請將檔案解壓到MessageBoxApp.exe的安裝目錄, 然後執行補丁程式
) else (
setdll /d:ApiHook.dll MessageBoxApp.exe
)
Pause
呼叫後使用depends.exe(微軟VC6.0開發包的工具之一)觀察MessageBoxApp.exe前後變化, 可以看到Setdll已經重寫MessageBoxApp.exe
成功,加入了對ApiHook.dll的依賴關係。
(執行SetDll.exe前) (執行SetDll.exe後)
執行SetDll.exe重寫後的MessageBoxApp.exe,點選確定後可以看到結果如下:
至此,MessageBoxApp.exe對MessageBoxA函式的呼叫已經被截獲,彈出的對話方塊內容已經明顯說明這一點。