第22章 DLL注入和API攔截
本章内容
22.1 DLL注入的一個例子
22.2 使用系統資料庫來注入DLL
22.3 使用Windows挂鈎來注入DLL
22.4 使用遠端線程來注入DLL
22.5 使用木馬DLL來注入DLL
22.6 把DLL作為調試器來注入
22.7 使用CreateProcess來注入代碼
22.8 API攔截的例子
通常在作業系統中,每個程序都有自己獨立的虛拟位址空間,這樣不會破壞其他程序的資料。這樣要和其他程序通信或操控其他程序就比較麻煩。
應用程式需要跨程序邊界來通路另一個程序位址空間的情況如下:
1)想要從另一個程序建立的視窗派生之類視窗
2)需要确定另一個程序正在使用哪些DLL
3)給另一個程序安裝挂鈎(hook other processes)
22.1 DLL注入的一個例子
例如需要改變另一個程序所建立視窗執行個體的行為(subclass)。可以調用SetWindowLongPtr來修改其預設的視窗函數。(雖然MSDN上說無法直接subclass另一個程序建立的視窗,是因為無法跨越程序位址空間來通路另一個程序的資料)
例如執行以下代碼:
SetWindowLongPtr(hWnd, GWLP_WNDPROC, MySubclassProc);
告知系統所有發到hWnd視窗的消息,應該由MySubclassProc來處理,而不是用該視窗的标準視窗過程處理。
從另一個程序(subclass)視窗的問題在于,視窗過程在另一個程序的位址空間中。

例如圖22-1是一個簡化的視圖,顯示了視窗過程如何處理收到的消息。程序A正在運作,它已經建立了一個視窗。user32.dll被映射到程序A的位址空間中,負責對發到和發往程序A的任何視窗的消息進行接收和派送。User32.dll檢查收到一個消息的時候,會先确定該視窗的WndProc位址,然後調用它并在參數中傳入視窗句柄,消息,以及wParam和lParam。 WndProc處理完畢以後,User32.dll會進入下一輪詢并等待對下一條視窗消息的處理。
假設程序B要從程序A對其一個子視窗Subclass. 需要執行以下步驟:
1)需要獲得程序A的需要被SubClass的視窗句柄。例如FindWindow
2)接着調用SetWindowLongPtr“試圖”改變WndProc的位址。 因為SetWindowLongPtr會檢查試圖修改的視窗句柄是否屬于另一個程序,若是則傳回。
假設能讓SetWindowLongPtr修改成功,那麼發送到這個視窗上的消息都會重新轉發到MySubclassProc上。可是MySubclassProc函數是屬于程序B的位址空間,而在程序A中并不存在這個函數。于是這就會導緻一個記憶體通路違規。
想讓系統知道MySubclassProc在程序B的位址空間,并在調用之類視窗的時候切換CONTEXT
處于一下原因MS沒有實作這個功能。
1)應用程式很少需要從其他程序的視窗subclass.大多數應用程式隻處理自己的視窗
2)切換活動程序會耗費非常多的CPU時間
3)程序B中的一個線程必須執行MySubclassProc的代碼,系統應該嘗試使用哪個線程?一個已有線程還是建立一個線程?
4)User32.dll怎樣才能知道與該視窗關聯的視窗過程的位址在另一個程序中還是目前程序中?
通過某種方法,讓MySubclassProc的進入到A的位址空間中,就能夠調用SetWindowLongPtr并把MySubclassProc的位址傳給他。這稱為dll注入
22.2 使用系統資料庫來注入DLL
在系統資料庫項目:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\
AppInit_Dlls鍵值可能包含一個DLL的檔案名或一組DLL的檔案名(通過空格或逗号分離)
第一個DLL的檔案名可以包含路徑,其他DLL包含的路徑将被忽略。最好将自己的DLL放到Windows的系統目錄,這樣就不必指定路徑。
一下例子使用C:\MyLib.dll
并把LoadAppInit_DLLs的值修改為1 。
當User32.dll被映射到一個程序時,會收到DLL_PROCESS_ATTACH通知,當User32.dll對其處理時,會取得上述系統資料庫的鍵值,并調用LoadLibrary來載入這個字元串指定的每個DLL。系統載入每個DLL的時候,會調用它們的DllMain函數并将參數設為DLL_PROCESS_ATTACH,這樣每個DLL可以對其進行初始化。
(在DLL初始化調用Kernel32.dll可能沒問題,而調用其他DLL可能會導緻錯誤,甚至藍屏)
這種方法有一些缺點:
1)DLL隻會被映射到使用了user32.dll的程序,所有基于GUI的應用都會用到user32.dll。而CUI應用不會。這種方法就無法注入到編譯器或連接配接器
2)DLL會被注入到所有的GUI程式,有時候僅想注入到一個或少數幾個應用。(如果DLL本身代碼存在bug,例如死循環這将對所有正常運作的應用程式造成影響)
3)如果DLL被映射到所有基于GUI的應用程式,在應用程式終止前,它将一直存在位址空間。
22.3 使用Windows挂鈎來注入DLL
可以使用挂鈎(HOOK)來将一個DLL注入到程序的位址空間中。
例如:程序A(類似Spy++)為了檢視系統中各視窗處理了哪些消息,安裝了一個WH_GETMESSAGE挂鈎。這個挂鈎是通過調用SetWindowsHookEx來安裝的,例如
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0);
第一個參數 WH_GETMESSAGE表示要安裝挂鈎的類型。
第二個參數GetMsgProc是一個函數的位址(在目前程序的位址空間)在視窗即将處理一條消息的時候,系統應該調用這個函數
第三個參數hInstDll辨別一個DLL,這個DLL包含了GetMsgProc函數。hInstDll的值就是該DLL被映射到目前程序位址空間的虛拟位址
第四個參數0表示要給哪個線程安裝挂鈎。通常傳入0表示給所有GUI線程安裝挂鈎
接下來看看會發生什麼:
1)程序B中的一個線程準備向一個視窗派送一條消息
2)系統檢查該線程是否已經安裝了WH_GETMESSAGE挂鈎
3)系統檢查GetMsgProc所在的DLL是否已經被映射到程序B的位址空間中
4)如果DLL尚未被映射,那麼系統是強制将該DLL映射到程序B的位址空間中,并将程序B中該DLL的鎖計數器(lock count)遞增
5)由于DLL的hInstDll是在程序B中映射的,是以系統會對它程序檢查,看它與該DLL在程序A中的位置是否相同。
如果hInstDll相同, 那麼兩個程序位址空間中,GetMsgProc位于相同的位置。這樣系統可以在程序A的位址空間調用GetMsgProc
如果hInstDll不同,系統必須确定GetMsgProc函數在程序B的位址空間中的虛拟記憶體位址。這個二位址通過下面公式得出:
GetMsgProc B = (hInstDll B - hInstDll A)+ GetMsgProc A //這裡筆者将公式做了一個修改更容易了解
6)系統在程序B中遞增該DLL的鎖計數器
7)系統在程序B的位址空間調用GetMsgProc函數
8)當GetMsgProc傳回的時候,系統遞減該DLL在程序B中的鎖計數器
注意:當系統把挂鈎過濾函數(hook filter function)所在的DLL注入或映射到位址空間時,會映射整個DLL,而不僅僅是挂鈎過濾函數。這使得DLL内的所有函數存在與程序B,能夠被程序B中的任何線程調用。
這樣為了從另一個程序的視窗來subclass。 可以先給建立該視窗的線程設定一個WH_GETMESSAGE挂鈎,然後當GetMsgProc函數被調用的時候,就可以調用SetWindowLongPtr來進行視窗的Subclass。(當然視窗過程必須和GetMsgProc函數在同一個DLL中)
和系統資料庫的方法相比,該方法允許我們在不需要該DLL的時候從程序的位址空間撤銷對其映射。調用一下函數
WINUSERAPI
BOOL
WINAPI
UnhookWindowsHookEx(
_In_ HHOOK hhk);
當一個線程調用 UnhookWindowsHookEx的時候,系統會周遊自己内部的一個已經注入過該DLL的程序清單,并将該DLL的鎖計數器遞減,當所計數器減到0的時候,系統會自動從程序的位址空間撤掉對該DLL的映射。步驟6)中系統在調用GetMsgProc函數之前,會遞增該DLL的鎖計數器。可以防止記憶體通路違規。
如果鎖計數器沒有遞增,那麼程序B中的線程試圖執行GetMsgProc的時候,系統中的另一個線程可能會調用UnhookWindowsHookEx函數,進而引起記憶體通路違規
Desktop Item Position Saver(DIPS)工具
DIPS.exe使用視窗挂鈎将一個DLL注入到Explorer.exe(桌面)的位址空間中。
當切換了桌面分辨率以後桌面上的圖示排序混亂了。作者設計了DIPS這個工具 給DIPS的指令行參數是S的時候,會為桌面上每個圖示都添加一個系統資料庫項:
HKEY_CURRENT_USER\Software\Wintellect\Desktop Item Position Saver
DIPS會為每個圖示儲存一個位置。比如要玩遊戲修改分辨率,在修改分辨率以前運作DIPS S 然後打完遊戲以後運作DIPS R,這時候DIPS會打開系統資料庫找到那些儲存過位置的圖示并将其位置恢複到運作DIPS S的位置
(Windows桌面原來就是一個ListView控件,但是很多消息LVM_GETITEM和LVM_GETITEMPOSITION是不能誇程序邊界的)
因為LVM_GETITEM消息要求在其LPARAM參數中傳入一個LV_ITEM資料結構。這個記憶體位址隻對發送消息的程序有意義,接受消息的程序是無法使用的。為了讓DIPS能夠正常工作必須将代碼注入到Explorer.exe這樣才能成功将LVM_GETITEM和LVM_GETITEMPOSITION消息發送到桌面的ListView控件。
DIPS.exe運作的時候,先擷取桌面的ListView控件的視窗句柄:
// The Desktop ListView window is the
// grandchild of the ProgMan Window.
HWND hWndLV = GetFirstChild(GetFirstChild(FindWindow(TEXT("ProgMan"), NULL)));
使用Spy++可以捕獲到一下這層關系。
捕獲一個一個類為ProgMan的視窗,(Program Manager是為了相容老版本的Windows設計的應用程式)Progman隻有一個類别為SHELLDLL_DefView的子視窗。該視窗也隻有一個子視窗類别是SysListView32 .這個SysListView32就是桌面的ListView控件視窗
接着可以通過GetWindowThreadProcessId來确定建立該視窗的線程辨別符。然後把線程辨別符傳遞給SetDIPSHook(在DIPSLib.cpp内實作)
該函數會給線程安裝一個WH_GETMESSAGE挂鈎,并調用下面的函數來強制喚醒Windows資料總管線程
PostThreaMessage(dwThreadId, WM_NULL, 0, 0);
由于已經安裝了WH_GETMESSAGE挂鈎系統會将DIPSLib.dll注入到Windows資料總管的位址空間并調用GetMsgProc函數。
該函數會檢查是否被第一次調用,如果是第一個調用,會建立一個标題為"Wintellect DIPS"的隐藏視窗(由資料總管所建立)
DIPS線程已經從SetDIPSHook調用中傳回并接着調用以下函數
GetMessage(&msg, NULL, 0, 0);
這個調用将線程切換到睡眠狀态,直到消息隊列中出現消息為止。
使用線程消息隊列來進行線程同步(比起核心對象還更容易)
當DIPS可執行檔案中的線程被喚醒,它知道伺服器對話框"Wintellect DIPS"已經建立完成,就調用FindWindow來找到該視窗。現在就可以通過視窗消息在客戶(DIPS)和伺服器(隐藏視窗)之間通信了。
為了告訴我們對話框去儲存或恢複桌面圖示的位置,隻需要發送一條消息
SendMessage(hWndDIPS, WM_APP, (WPARAM)hWndLV, bSave);
對話框收到該消息。WPARAM參數是一個視窗句柄,表示要操作ListView控件,LPARAM是一個布爾值表示是要保持圖示位置還是恢複圖示位置。
當與對話框通信完成以後,為了讓伺服器終止(Wintellect DIPS),像對話框發送了一條WM_CLOSE消息,讓其銷毀自己。
最後在DIPS終止之前,再次調用SetDIPSHook,告知函數把已經安裝的WH_GETMESSAGE挂鈎清除。之後作業系統會自動從Explorer.exe的位址空間将DIPSLib.dll解除安裝。
(必須先銷毀對話框再清除挂鈎,否則對話框收到下一條消息會導緻Windows資料總管記憶體通路違規。)
源代碼如下:
DIPS.cpp
/******************************************************************************
Module: DIPS.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <tchar.h>
#include "resource.h"
#include "..\DIPSLib\DIPSLib.h"
//
BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {
chSETDLGICONS(hWnd, IDI_DIPS);
return TRUE;
}
//
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
switch (id) {
case IDC_SAVE:
case IDC_RESTORE:
case IDCANCEL:
EndDialog(hWnd, id);
break;
}
}
//
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);
}
return FALSE;
}
//
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {
// Convert command-line character to uppercase.
CharUpperBuff(pszCmdLine, 1);
TCHAR cWhatToDo = pszCmdLine[0];
if ((cWhatToDo != TEXT('S')) && (cWhatToDo != TEXT('R'))) {
// An invalid command-line argument; prompt the user.
cWhatToDo = 0;
}
if (cWhatToDo == 0) {
// No command-line argument was used to tell us what to
// do; show usage dialog box and prompt the user.
switch (DialogBox(hInstExe, MAKEINTRESOURCE(IDD_DIPS), NULL, Dlg_Proc)) {
case IDC_SAVE:
cWhatToDo = TEXT('S');
break;
case IDC_RESTORE:
cWhatToDo = TEXT('R');
break;
}
}
if (cWhatToDo == 0) {
// The user doesn't want to do anything.
return 0;
}
// The Desktop ListView window is the grandchild of the ProgMan window.
HWND hWndLV = GetFirstChild(GetFirstChild(
FindWindow(TEXT("ProgMan"), NULL)));
chASSERT(IsWindow(hWndLV));
// Set hook that injects our DLL into the Explorer's address space. After
// setting the hook, the DIPS hidden modeless dialog box is created. We
// send messages to this window to tell it what we want it to do.
chVERIFY(SetDIPSHook(GetWindowThreadProcessId(hWndLV, NULL)));
// Wait for the DIPs server window to be created.
MSG msg;
GetMessage(&msg, NULL, 0, 0);
// Find the handle of the hidden dialog box window.
HWND hWndDIPS = FindWindow(NULL, TEXT("Wintellect DIPS"));
// Make sure that the window was created.
chASSERT(IsWindow(hWndDIPS));
// Tell the DIPS window which ListView window to manipulate
// and whether the items should saved or restored.
BOOL bSave = (cWhatToDo == TEXT('S'));
SendMessage(hWndDIPS, WM_APP, (WPARAM)hWndLV, bSave);
// Tell the DIPS window to destroy itself. Use SendMessage
// instead of PostMessage so that we know the window is
// destroyed before the hook is removed.
SendMessage(hWndDIPS, WM_CLOSE, 0, 0);
// Make sure that the window was destroyed.
chASSERT(!IsWindow(hWndDIPS));
// Unhook the DLL, removing the DIPS dialog box procedure
// from the Explorer's address space.
SetDIPSHook(0);
return 0;
}
動态庫代碼:
DIPSLib.h
/******************************************************************************
Module: DIPSLib.h
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#if !defined(DIPSLIBAPI)
#define DIPSLIBAPI extern "C" __declspec(dllimport)
#endif
///
// External function prototypes
DIPSLIBAPI BOOL WINAPI SetDIPSHook(DWORD dwThreadId);
End of File //
DIPSLib.cpp
/******************************************************************************
Module: DIPSLib.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <commctrl.h>
#define DIPSLIBAPI extern "C" __declspec(dllexport)
#include "DIPSLib.h"
#include "resource.h"
//
#ifdef _DEBUG
// This function forces the debugger to be invoked
void ForceDebugBreak() {
__try{ DebugBreak(); }
__except (UnhandledExceptionFilter(GetExceptionInformation())) {}
}
#else
#define ForceDebugBreak()
#endif
//
// Forward reference
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam);
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
//
// Instruct the compiler to put the g_hHook data variable in
// its own data section called Shared. We then instruct the
// linker that we want to share the data in this section
#pragma data_seg("Shared")
HHOOK g_hHook = NULL;
DWORD g_dwThreadIdDIPS = 0;
#pragma data_seg()
// Instruct the linker to make the Shared section
// readable, writable, and shared.
#pragma comment(linker, "/section:Shared,rws")
//
// Nonshared variables
HINSTANCE g_hInstDll = NULL;
//
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// DLL is attaching to the address space of the current process.
g_hInstDll = hInstDll;
break;
case DLL_THREAD_ATTACH:
// A new thread is being created in the current process.
break;
case DLL_THREAD_DETACH:
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// The calling process is detaching the DLL from its address space.
break;
}
return TRUE;
}
//
BOOL WINAPI SetDIPSHook(DWORD dwThreadId) {
BOOL bOk = FALSE;
if (dwThreadId != 0) {
// Make sure that the hook is not already installed.
chASSERT(g_hHook == NULL);
// Save our thread ID in a shared variable so that our GetMsgProc
// function can post a message back to the thread when the server
// window has been created.
g_dwThreadIdDIPS = GetCurrentThreadId();
// Install the hook on the specified thread
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hInstDll,
dwThreadId);
bOk = (g_hHook != NULL);
if (bOk) {
// The hook was installed successfully; force a begin message to
// the thread's queue so that the hook function gets called.
bOk = PostThreadMessage(dwThreadId, WM_NULL, 0, 0);
}
}
else {
// Make sure that a hook has been installed.
chASSERT(g_hHook != NULL);
bOk = UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
return bOk;
}
//
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam) {
static BOOL bFirstTime = TRUE;
if (bFirstTime) {
// The DLL just got injected.
bFirstTime = FALSE;
// Uncomment the line below to invoke the debugger
// on the process that just got the injected DLL.
//ForceDebugBreak();
// Create the DIPS Server window to handle the client request.
CreateDialog(g_hInstDll, MAKEINTRESOURCE(IDD_DIPS), NULL, Dlg_Proc);
// Tell the DIPS application that the server is up
// and ready to handle requests.
PostThreadMessage(g_dwThreadIdDIPS, WM_NULL, 0, 0);
}
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
//
void Dlg_OnClose(HWND hWnd) {
DestroyWindow(hWnd);
}
//
static const TCHAR g_szRegSubKey[] =
TEXT("Software\\Wintellect\\Desktop Item Position Saver");
//
void SaveListViewItemPosition(HWND hWndLV) {
int nMAxItems = ListView_GetItemCount(hWndLV);
// When saving new positions, delete the old position
// information that is currently in the registry.
LONG l = RegDeleteKey(HKEY_CURRENT_USER, g_szRegSubKey);
// Create the registry key to hold the info
HKEY hKey;
l = RegCreateKeyEx(HKEY_CURRENT_USER, g_szRegSubKey, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL);
chASSERT(l == ERROR_SUCCESS);
for (int nItem = 0; nItem < nMAxItems; nItem++) {
// Get the name and position of a ListView item.
TCHAR szName[MAX_PATH];
ListView_GetItemText(hWndLV, nItem, 0, szName, _countof(szName));
POINT pt;
ListView_GetItemPosition(hWndLV, nItem, &pt);
// Save the name and position in the registry.
l = RegSetValueEx(hKey, szName, 0, REG_BINARY, (PBYTE)&pt, sizeof(pt));
chASSERT(l == ERROR_SUCCESS);
}
RegCloseKey(hKey);
}
//
void RestoreListViewItemPosition(HWND hWndLV) {
HKEY hKey;
LONG l = RegOpenKeyEx(HKEY_CURRENT_USER, g_szRegSubKey,
0, KEY_QUERY_VALUE, &hKey);
if (l == ERROR_SUCCESS) {
// If the ListView has AutoArrange on, temporarily turn it off.
DWORD dwStyle = GetWindowStyle(hWndLV);
if (dwStyle & LVS_AUTOARRANGE)
SetWindowLong(hWndLV, GWL_STYLE, dwStyle & ~LVS_AUTOARRANGE);
l = NO_ERROR;
for (int nIndex = 0; l != ERROR_NO_MORE_ITEMS; nIndex++) {
TCHAR szName[MAX_PATH];
DWORD cbValueName = _countof(szName);
POINT pt;
DWORD cbData = sizeof(pt);
DWORD nItem;
// Read a value name and position from the registry.
DWORD dwType;
l = RegEnumValue(hKey, nIndex, szName, &cbValueName,
NULL, &dwType, (PBYTE)&pt, &cbData);
if (l == ERROR_NO_MORE_ITEMS)
continue;
if ((dwType == REG_BINARY) && (cbData == sizeof(pt))) {
// The value is something that we recognize; try to find
// an item in the ListView control that matches the name.
LV_FINDINFO lvfi;
lvfi.flags = LVFI_STRING;
lvfi.psz = szName;
nItem = ListView_FindItem(hWndLV, -1, &lvfi);
if (nItem != -1) {
// We found a match; change the item's position.
ListView_SetItemPosition(hWndLV, nItem, pt.x, pt.y);
}
}
}
// Turn AutoArrange back on if it was originally on.
SetWindowLong(hWndLV, GWL_STYLE, dwStyle);
RegCloseKey(hKey);
}
}
//
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_CLOSE, Dlg_OnClose);
case WM_APP:
// Uncomment the line below to invoke the debugger
// on the process that just got the injected DLL.
//ForceDebugBreak();
if (lParam)
SaveListViewItemPosition((HWND)wParam);
else
RestoreListViewItemPosition((HWND)wParam);
break;
}
return FALSE;
}
End of File //
編譯好以後發現SetDIPSHook函數可以調用成功,可是DLL總是收不到注冊的回調函數的消息也不會建立之視窗。說明DLL并未注入目标成功。
應該也不是權限的問題,因為GetLastError也沒有傳回錯誤。
後來在網上查到了一篇資料:
http://bbs.csdn.net/topics/390950459
原來我使用32位代碼來編譯,exe和dll而本機是Win7 x64系統。explorer.exe(桌面)程序也是x64的。不能将32bit的dll注入到64bit的程序中。
是以改用編譯x64的dll再嘗試注入explorer.exe以後就成功了。
檢視該工具已經在系統資料庫中建立了桌面圖示的坐标值。
工具運作界面:
22.4 使用遠端線程來注入DLL
注入DLL的第三種方法是遠端線程(Remote Thread)具有高靈活性。這種方法需要了解windows許多特性,程序,線程,線程同步,虛拟記憶體管理,DLL以及Unicode。
通常Windows隻允許程序對自己進行操作。某些函數可以讓一個程序對另一個程序進行操作。(為調試器設計的)
使用以下函數在目标程序中建立一個線程
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateRemoteThread(
_In_ HANDLE hProcess,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
CreateRemoteThread比CreateThread多了一個hProcess參數,表明新建立的線程歸哪個程序所有。pfnStartAddr是線程函數的記憶體位址(這個位址必須在遠端序的位址空間中)
接着需要調用LoadLibrary來加載dll(注意區分Unicode和ANSI版本)
線程函數原型
DWORD WINAPI ThreadFunc(PVOID pvParam);
LoadLibrary函數原型
WINBASEAPI
_Ret_maybenull_
HMODULE
WINAPI
LoadLibraryA(
_In_ LPCSTR lpLibFileName
);
WINBASEAPI
_Ret_maybenull_
HMODULE
WINAPI
LoadLibraryW(
_In_ LPCWSTR lpLibFileName
);
這兩個函數原型比較接近。 接着需要執行類似這樣的代碼。
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
LoadLibraryW, L"C:\\MyLib.dll", 0, NULL);
或者
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
LoadLibraryA, "C:\\MyLib.dll", 0, NULL);
線程在遠端程序中被建立時,會立即調用 LoadLibraryW(或LoadLibraryA)并傳入DLL路徑名的位址。
還存在一些問題:
在編譯和連結時,生成的二進制檔案會包含導入段。這個段由一系列轉換函數(thunk)構成,這些轉換函數用來跳轉到導入函數。是以在代碼調用比如LoadLibraryW之類的函數時,連接配接器會生成一個調用,來調用導入段中的一個轉換函數,這個轉換函數再跳轉到實際的函數。
是以在CreateRemoteThread的時候直接傳入LoadLibraryW 會被解析為我們子產品導入段中LoadLibraryW轉換函數的位址。如果将這個轉換函數的位址作為遠線程的起始位址傳入,結果是未定義的(UB)甚至是通路違規。為了強制代碼略過轉換函數并直接調用LoadLibraryW函數,必須通過GetProcAddress來獲得确切的位址。
對CreateRemoteThread的調用假定在本地程序和遠端程序中,Kernel32.dll被映射到位址空間中的同一記憶體位址。每個應用程式都需要Kernel32.dll(通常也是相同的基位址)
在Vista系統加入了位址空間布局随機化(Address Space Layout Randomization, ASLR)是以應該這樣來調用CreateRemoteThread
// Get the real address of LoadLibraryW in Kernel32.dll
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
pfnThreadRtn, L"C:\\MyLib.dll", 0, NULL);
ANSI版本
// Get the real address of LoadLibraryW in Kernel32.dll
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
pfnThreadRtn, "C:\\MyLib.dll", 0, NULL);
但是以上代碼還存在一個問題。 傳遞給 LoadLibrary的字元串的位址是目前程序位址空間内的,遠端序位址空間内并不存在此字元串。是以 LoadLibrary的調用可能會造成記憶體通路違規。
這樣需要把DLL的路徑的字元串存放到遠端序的位址空間去。然後再CreateRemoteThread的時候傳入遠端序位址空間中存放的字元串位址。
使用VirtualAllocEx可以讓程序在另一個程序的位址空間配置設定一塊記憶體。
WINBASEAPI
_Ret_maybenull_ _Post_writable_byte_size_(dwSize)
LPVOID
WINAPI
VirtualAllocEx(
_In_ HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
使用另一個函數可以釋放這塊記憶體
WINBASEAPI
BOOL
WINAPI
VirtualFreeEx(
_In_ HANDLE hProcess,
_Pre_notnull_ _When_(dwFreeType == MEM_DECOMMIT, _Post_invalid_) _When_(dwFreeType == MEM_RELEASE, _Post_ptr_invalid_) LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD dwFreeType
);
一旦給字元串配置設定了記憶體,還需要把字元串從程序位址空間中複制到遠端序位址空間中去。使用以下函數。
可以讀寫另一個程序位址空間中的記憶體位址。
WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
ReadProcessMemory(
_In_ HANDLE hProcess,
_In_ LPCVOID lpBaseAddress,
_Out_writes_bytes_to_(nSize, *lpNumberOfBytesRead) LPVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_opt_ SIZE_T * lpNumberOfBytesRead
);
WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_reads_bytes_(nSize) LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_opt_ SIZE_T * lpNumberOfBytesWritten
);
hProcess是遠端序句柄。lpBaseAddress是要讀寫的(遠端程序的)記憶體位址,lpBuffer(本程序的)記憶體位址,nSize要傳輸的位元組數
lpNumberOfBytesRead/lpNumberOfBytesWritten 傳回讀取/寫入成功的位元組數
總結一下必須采取的步驟:
1)用VirtualAllocEx在遠端序位址空間配置設定一塊記憶體
2)用WriteProcessMemory函數把DLL的路徑名複制到第一步配置設定的記憶體
3)用GetProcAddress獲得LoadLibraryW或LoadLibraryA函數(在Kernell32.dll)的位址
4)用CreateRemoteThread函數在遠端序建立一個線程,讓新線程調用LoadLibrary并傳入在步驟1中配置設定的記憶體位址。
這時DLL已經被注入到遠端序的位址空間,DLL的DllMain函數會收到DLL_PROCESS_ATTACH通知可以執行想要執行的代碼。
DllMain傳回的時候,遠端線程會從LoadLibrary調用傳回到BaseThreadStart函數,BaseThreadStart調用ExitThread使遠端線程終止。
(但是DLL還在遠端程序的位址空間,還有一塊之前配置設定的記憶體塊)
5)用VirtualFreeEx來釋放第一步配置設定的記憶體
6)用GetProcAddress來得到FreeLibrary函數的實際位址
7)用CreateRemoteThread函數在遠端程序中建立一個線程,讓該線程調用FreeLibrary并在參數中傳入遠端DLL的HMODULE
22.4.1 Inject Library示例程式
輸入程序的辨別符,通過任務管理器來獲得。
程式會試圖打開這個正在運作的程序句柄
WINBASEAPI
HANDLE
WINAPI
OpenProcess(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwProcessId
);
如果傳回NULL,說明程式所在的安全上下文(security context)不允許打開目标程序的句柄。一些程序是本地賬号運作的,比如WinLogon,svchost和csrss,登陸使用者是無法對這些程序修改的。
如果我們已經獲得調試安全特權(debug security privilege)授權,并且也啟用了調試安全特權,那麼也許可以打開這些程序的句柄。參考第四章ProcessInfo
OpenProcess調用成功以後,會先用帶注入的DLL的完整路徑初始化緩存,然後調用InjectLib并傳入遠端程序的句柄以及要帶注入DLL的路徑名。當InjectLib傳回的時候,程式會顯示一個消息框,表示DLL已經成功載入到遠端程序中, 然後關閉程序句柄。就是整個過程
如果程序ID為0,則Inject會注入到自己的程序中。将InjLib注入到自己,使得代碼調試容易了許多。
InjectLib是一個宏,支援Unicode和ANSI版本。
InjectLib.cpp
/******************************************************************************
Module: InjLib.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <stdio.h>
#include <tchar.h>
#include <malloc.h>
#include <TlHelp32.h>
#include <strsafe.h>
#include "Resource.h"
//
#ifdef UNICODE
#define InjectLib InjectLibW
#define EjectLib EjectLibW
#else
#define InjectLib InjectLibA
#define EjectLib EjectLibA
#endif
//
BOOL WINAPI InjectLibW(DWORD dwProcessId, PCWSTR pszLibFile) {
BOOL bOk = FALSE; // Assume that the function filas
HANDLE hProcess = NULL, hThread = NULL;
PWSTR pszLibFileRemote = NULL;
__try{
// Get a handle for the target process.
hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION | // Required by Alpha
PROCESS_CREATE_THREAD | // For CreateRemoteThread
PROCESS_VM_OPERATION | // For VirtualAllocEx/VirtualFreeEx
PROCESS_VM_WRITE, // For WriteProcessMemory
FALSE, dwProcessId
);
if (hProcess == NULL) __leave;
// Calculate the number of bytes needed for the DLL's pathname
int cch = 1 + lstrlenW(pszLibFile);
int cb = cch * sizeof(wchar_t);
// Allocate space in the remote process for the pathname
pszLibFileRemote = (PWSTR)
VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);
if (pszLibFileRemote == NULL) __leave;
// Copy the DLL's pathname to the remote process' address space
if (!WriteProcessMemory(hProcess, pszLibFileRemote,
(PVOID)pszLibFile, cb, NULL))
__leave;
// Get the real address of LoadLibraryW in Kernell32.dll
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
if (pfnThreadRtn == NULL) __leave;
// Create a remote thread that calls LoadLibraryW(DLLPathname)
hThread = CreateRemoteThread(hProcess, NULL, 0,
pfnThreadRtn, pszLibFileRemote, 0, NULL);
if (hThread == NULL) __leave;
// Wait for the remote thread to termintae
WaitForSingleObject(hThread, INFINITE);
bOk = TRUE; // Everything executed successfully
}
__finally{ // Now, we can clean everything up
// Free the remote memory that contained the DLL's pathname
if (pszLibFileRemote != NULL)
VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
if (hThread != NULL)
CloseHandle(hThread);
if (hProcess != NULL)
CloseHandle(hProcess);
}
return bOk;
}
//
BOOL WINAPI InjectLibA(DWORD dwProcessId, PCSTR pszLibFile) {
// Allocate a (stack) buffer for the Unicode version of the pathname
SIZE_T cchSize = lstrlenA(pszLibFile) + 1;
PWSTR pszLibFileW = (PWSTR)
_alloca(cchSize * sizeof(wchar_t));
// Convert the ANSI pathname to its Unicode equivalent
StringCchPrintfW(pszLibFileW, cchSize, L"%S", pszLibFile);
// Call the unicode version of the function to actually do the work.
return InjectLibW(dwProcessId, pszLibFileW);
}
//
BOOL WINAPI EjectLibW(DWORD dwProcessId, PCWSTR pszLibFile) {
BOOL bOk = FALSE; // Assume that the function fails
HANDLE hthSnapshot = NULL;
HANDLE hProcess = NULL, hThread = NULL;
__try {
// Grab a new snapshot of the process
hthSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
if (hthSnapshot == INVALID_HANDLE_VALUE) __leave;
// Get the HMODULE of the desired library
MODULEENTRY32W me = { sizeof(me) };
BOOL bFound = FALSE;
BOOL bMoreMods = Module32FirstW(hthSnapshot, &me);
for (; bMoreMods; bMoreMods = Module32NextW(hthSnapshot, &me)) {
bFound = (_wcsicmp(me.szModule, pszLibFile) == 0) ||
(_wcsicmp(me.szExePath, pszLibFile) == 0);
if (bFound) break;
}
if (!bFound) __leave;
// Get a handle for the target process.
hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION, // For CreateRemoteThread
FALSE, dwProcessId);
if (hProcess == NULL) __leave;
// Get the real address of FreeLibrary in Kernel32.dll
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "FreeLibrary");
if (pfnThreadRtn == NULL) __leave;
// Create a remote thread that calls FreeLibrary()
hThread = CreateRemoteThread(hProcess, NULL, 0,
pfnThreadRtn, me.modBaseAddr, 0, NULL);
if (hThread == NULL) __leave;
// Wait for the remote thread to terminate
WaitForSingleObject(hThread, INFINITE);
bOk = TRUE; // Everything executed successfully
}
__finally { // Now we can clean everything up
if (hthSnapshot != NULL)
CloseHandle(hthSnapshot);
if (hThread != NULL)
CloseHandle(hThread);
if (hProcess != NULL)
CloseHandle(hProcess);
}
return bOk;
}
//
BOOL WINAPI EjectLibA(DWORD dwProcessId, PCSTR pszLibFile) {
// Allocate a (stack) buffer for the Unicode version of the pathname
SIZE_T cchSize = lstrlenA(pszLibFile) + 1;
PWSTR pszLibFileW = (PWSTR)
_alloca(cchSize * sizeof(wchar_t));
// Convert the ANSI pathname to its Unicode equivalent
StringCchPrintfW(pszLibFileW, cchSize, L"%S", pszLibFile);
// Call the Unicode version of the function to actually do the work.
return EjectLibW(dwProcessId, pszLibFileW);
}
//
BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {
chSETDLGICONS(hWnd, IDI_INJLIB);
return TRUE;
}
//
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
switch (id) {
case IDCANCEL:
EndDialog(hWnd, id);
break;
case IDC_INJECT:
DWORD dwProcessId = GetDlgItemInt(hWnd, IDC_PROCESSID, NULL, FALSE);
if (dwProcessId == 0) {
// A process ID of 0 causes everything to take place in the
// local process; this makes things easier for debugging.
dwProcessId = GetCurrentProcessId();
}
TCHAR szLibFile[MAX_PATH];
GetModuleFileName(NULL, szLibFile, _countof(szLibFile));
PTSTR pFilename = _tcsrchr(szLibFile, TEXT('\\')) + 1;
_tcscpy_s(pFilename, _countof(szLibFile) - (pFilename - szLibFile),
TEXT("ImgWalk.DLL"));
if (InjectLib(dwProcessId, szLibFile)) {
chVERIFY(EjectLib(dwProcessId, szLibFile));
chMB("DLL Injection/Ejection successful.");
}
else {
chMB("DLL Injection/Ejection failed.");
}
break;
}
}
//
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);
}
return FALSE;
}
//
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {
DialogBox(hInstExe, MAKEINTRESOURCE(IDD_INJLIB), NULL, Dlg_Proc);
return 0;
}
End of File //
接着InjectLib.exe将對目标程序注入一個叫ImgWalk.dll的動态庫
可以報告該程序正在使用的所有DLL。周遊一個程序位址空間并查找已映射的檔案映像,ImgWalk.dll會反複調用VirtualQuery來得到一個MEMORY_BASIC_INFORMATION結構
每次疊代的時候ImgWalk會把檔案名的路徑與一個字元床起來,并将得到的字元顯示在消息框中。
ImgWalk.cpp
/******************************************************************************
Module: ImgWalk.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <tchar.h>
//
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
if (fdwReason == DLL_PROCESS_ATTACH) {
char szBuf[MAX_PATH * 100] = { 0 };
PBYTE pb = NULL;
MEMORY_BASIC_INFORMATION mbi;
while (VirtualQuery(pb, &mbi, sizeof(mbi)) == sizeof(mbi)) {
int nLen;
char szModName[MAX_PATH];
if (mbi.State == MEM_FREE)
mbi.AllocationBase = mbi.BaseAddress;
if ((mbi.AllocationBase == hInstDll) ||
(mbi.AllocationBase != mbi.BaseAddress) ||
(mbi.AllocationBase == NULL)
) {
// Do not add the module name to the list
// if any of the following is true:
// 1. If this region contains this DLL
// 2. If this block is NOT the beginning of a region
// 3. If the address is NULL
nLen = 0;
}
else {
nLen = GetModuleFileNameA((HINSTANCE)mbi.AllocationBase,
szModName, _countof(szModName));
}
if (nLen > 0) {
wsprintfA(strchr(szBuf, 0), "\n%p-%s",
mbi.AllocationBase, szModName);
}
pb += mbi.RegionSize;
}
// NOTE: Normally, you should not display a message box in DllMain
// due to the loader lock described in Chapter 20. However, to keep
// this sample application, I am violating this rule.
chMB(&szBuf[1]);
}
return TRUE;
}
End of File //
22.5 使用木馬DLL來注入DLL
方法一,如果知道目标程序必然會載入一個DLL。比如xyz.dll, 可以建立一個自己的DLL并起一個統一的名字,原來的xyz.dll可以改成别的名稱
接着使用Dumpbin工具檢視原來dll中所有導出的符号。(參考20章)然後使用函數轉發器,轉發到原來dll中的符号。
但是這種方法不能适應版本變化,例如xyz.dll被更新了,後來又在該DLL中新增加了函數。那麼我們的DLL中将不會有這些新的函數轉發器。引用了這些新函數的應用程式将無法被載入和執行。
方法二,如果隻想應用特定的應用程式,可以給自己的DLL起一個獨一無二的名稱,并修改應用程式exe的子產品的導入段。導入段中包含一個子產品所需的所有DLL的名稱,可以在檔案的導入段中找到那個要被替換的DLL名稱,然後對齊修改,這樣載入程式就會載入我們自己的DLL。(必須相當熟悉exe和DLL的檔案格式)
22.6 把DLL作為調試器來注入
調試器可以在被調試程序中執行許多特殊的操作。系統載入一個被調試程式(debuggee)的時候,會在被調試程式的位址空間準備完畢之後,但被調試程式的主線程尚未開始執行任何代碼之前,自動通知調試器。這時,調試器可以強制将一些代碼注入到被調試程式的位址空間(比如使用WriteProcessMemory)然後讓主線程去執行這些代碼。
這種方法要求對被調試線程的CONTEXT結構操作(例如注入代碼到特定位址以後(賦予PAGE_EXECUTE頁面屬性)同時修改主線程的CS,IP 指向注入代碼的位址)
這種方法具有平台依賴性,還必須手工編寫想讓被調試程式執行的機器語言指令。
通常如果調試器終止,那麼Windows會自動終止被調試程式。調試器可以調用DebugSetProcessKillOnExit并傳入FALSE來改變預設行為。
在不終止一個程序的前提下停止調試器也是可能的,歸功于DebugActiveProcessStop函數。
22.7 使用CreateProcess來注入代碼
通過目前程序來生成(spawn)目标程序,在子程序啟動之前先将其挂起。同時會得到其主線程的句柄。通過這個句柄可以對線程執行的代碼進行修改。例如設定指令指針(IP)讓他執行記憶體映射檔案中的代碼,可以解決前一節的問題。
以下是讓一個程序對其子程序的主線程進行控制的一種方法
1)讓程序生成一個被挂起的子程序
2)從exe子產品的檔案頭中得到主線程的起始記憶體位址
3)将位于該記憶體位址處的機器指令儲存起來
4)強制将一些手動編寫的機器指令寫入到該記憶體位址處。這些指令應該調用一個LoadLibrary來載入一個DLL
5)讓子程序的主線程恢複運作,進而讓這些指令得到執行。
6)把儲存起來的原始指令恢複到起始位址處
7)讓程序從起始位址繼續執行,就好像什麼都沒發生一樣。
這需要修改正在執行的代碼(參考以前用彙編語言實作過的x86機器的引導程式。)
該方法有一些好處:1)在執行之前得到位址空間。 2)應用程式不是調試器,可以非常容易對應用程式和注入的DLL進行調試。這種方法同時适用于CUI和GUI程式。
缺點:控制程序必須是目标程序的父程序。同時也需要編寫cpu依賴的代碼,需要為不同平台的cpu做出相應的修改。
22.8 API攔截的一個例子
将一個DLL注入到程序的位址空間是一個很好的方法,可以讓我們了解程序内部的各種資訊。但是簡單的注入并不能提供足夠的資訊,通常需要知道某個程序中的線程具體是怎麼調用各種函數的,可能還想對一個Windows函數的行為修改。
作者講述了一個案例:
一家公司開發了一個DLL。這個DLL會被資料庫産品載入,它的工作是對該資料庫産品的功能進行增強和擴充。當該資料庫産品終止的時候,這個DLL會收到一個DLL_PROCESS_DETACH通知,他會執行所有的清理代碼。這個DLL會調用其他DLL中的一些函數來關閉套接字,檔案以及其他資源,可是當它收到DLL_PROCESS_DETACH通知的時候,程序位址空間的其他DLL已經收到了它們各自的DLL_PROCESS_DETACH(被解除安裝了)。這個DLL試圖進行的清理工作可能會調用失敗。
作者建議對ExitProcess函數進行攔截。ExitProcess可以確定被調用時,該公司的DLL會立即得到通知。(這個通知發生在其他任何DLL得到DLL_PROCESS_DETACH通知之前,這些DLL還存在程序位址空間中,其相關的導出函數是可用的)這時該公司的DLL可以正常執行清理工作。然後作業系統的ExitProcess函數會被調用且繼續執。其他所有DLL都會收到DLL_PROCESS_DETACH通知并進行各自的清理。
這個例子不必關心DLL注入的問題(DLL是會被資料庫應用程式主動載入的)該DLL載入以後,必須周遊所有已經載入的可執行檔案和DLL子產品來尋找對ExitProcess的所有調用。當該DLL找到對ExitProcess的調用以後,它必須對各子產品進行修改,使得他們調用該公司DLL中的一個函數而不是作業系統的ExitProcess函數。
一旦該公司用來代替ExitProcess的函數(通常叫做攔截函數,hook function)執行完它的清理代碼以後,就會調用作業系統的ExitProcess函數(kernel32.dll)
-------------
筆者認為。作者的這個案例隻是為了描述API攔截的應用,如果資料庫應用程式本身的代碼是可控的,為什麼不在解除安裝所有其他Socket,檔案操作相關的DLL之前,顯式調用FreeLibrary來解除安裝該公司的DLL并現執行清理工作。而繞一個大圈子讓DLL自己執行API攔截什麼的來完成這個工作呢?當然除非資料庫應用程式是第三方的源代碼不可控。
22.8.1 通過覆寫代碼來攔截API
以上例子中攔截ExitProcess的一個方案是用代碼覆寫來攔截。工作方式如下:
1)在記憶體中,對想要攔截的函數(假設是Kernel32.dll中的ExitProcess)進行定位,進而得到它的記憶體位址。
2)把這個函數起始的幾個位元組儲存到自己的記憶體中
3)用CPU的一條JUMP指令來覆寫這個函數的起始幾個位元組,這條JUMP指令用來跳轉到我們替代函數的記憶體位址。所有替代函數的簽名必須要與攔截函數的簽名完全相同:所有參數必須相同,傳回值必須相同,調用約定也必須相同。
4)當線程調用被攔截函數(hooked function)的時候,跳轉指令實際上會跳轉到我們的替代函數。這是可以執行自己想要執行的代碼。
5)為了撤銷對該函數的攔截,必須把步驟2)中儲存下來的直接放回被攔截函數的起始位元組。
6)我們調用被攔截函數,讓該函數正常處理
7)當原來的函數傳回時,再次執行步驟2和步驟3,這樣我們的代替函數将來還好被調用到。
在16位windows上這種方法可以正常運作。但在如今有嚴重的不足。
1)它對cpu有依賴性x86, x64, ia-64其他cpu的JUMP指令各不相同,為了讓這種方法工作必須手工編寫機器指令。
2)其次,這種方法在搶占式,多線程環境下根本不能工作。一個線程覆寫一個函數起始位置的代碼是需要時間的,這個過程如果另一個線程試圖調用同一個函數,結果是災難性的。(除非保證任何時候隻有一個線程會調用該函數)
22.8.2 通過修改子產品的導入段來攔截API
另一種攔截API的方法可以解決以上提出的兩個問題。不僅容易實作而且具有健壯性。當然需要了解動态連結的工作方式。
一個子產品的導入段包含一組DLL,為了讓子產品能夠運作,這些DLL是必須的。此外導入段還包含一個符号表,列出了該子產品從各DLL中導入的符号。當該子產品調用一個導入函數的時候,實際上是先從該子產品的導入表中得到相應的導入函數的位址,然後再跳轉到那個位址。
是以為了攔截一個特定的函數,需要做的就是修改它在子產品的導入段中的位址。完全不存在對cpu的依賴性。而且并不需要修改函數的代碼,也不用擔心線程同步問題。
一下函數會在子產品的導入段查找對一個符号的引用,如果存在這個引用,就會修改該符号的位址。
void ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,
PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller) {
// Get the address of the module's import section
ULONG ulSize;
// An exception was trigger by Explorer (when browsing the content of
// a folder) into imagehlp.dll. It looks like one module was unloaded...
// Maybe some threading problem: the list of modules from Toolhelp might
// not be accurate if FreeLibrary is called during the enumeration.
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL;
__try{
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(
hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
}
__except (InvalidReadExceptionFilter(GetExceptionInformation())) {
// Nothing to do in here, thread continues to run normally
// with NULL for pImportDesc
}
if (pImportDesc == NULL)
return; // This module has no import section or is no longer loaded.
// Find the import descriptor containing references to callee's functions
for (; pImportDesc->Name; pImportDesc++) {
PSTR pszModName = (PSTR)((PBYTE)hmodCaller + pImportDesc->Name);
if (lstrcmpiA(pszModName, pszCalleeModName) == 0) {
// Get caller's import address table (IAT) for the callee's functions
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)
((PBYTE)hmodCaller + pImportDesc->FirstThunk);
// Replace current function address with new function address
for (; pThunk->u1.Function; pThunk++) {
// Get the address of the function address
PROC *ppfn = (PROC*)&pThunk->u1.Function;
// Is this function we're looking for?
BOOL bFound = (*ppfn == pfnCurrent);
if (bFound) {
// write memory failed due to the page protect attributes.
if (!WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,
sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError())) {
DWORD dwOldProtect;
// change the page protect to write/copy temp.
if (VirtualProtect(ppfn, sizeof(pfnNew), PAGE_WRITECOPY,
&dwOldProtect)) {
WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,
sizeof(pfnNew), NULL);
VirtualProtect(ppfn, sizeof(pfnNew), dwOldProtect,
&dwOldProtect);
}
}
return; // We did it ,get out
}
}
} // Each import section is parsed until the right entry is found and patched.
}
}
案例:
假設有一個Database.exe子產品,該子產品調用了Kernel32.dll中的ExitProcess函數。想讓它調用DbExtend.dll子產品中的 MyExitProcess函數。為了達到以下目的應該這樣調用 ReplaceIATEntryInOneMod函數
PROC pfnOrig = GetProcAddress(GetModuleHandle(TEXT("Kernel32")),
"ExitProcess");
HMODULE hmodCaller = GetModuleHandle("Database.exe");
ReplaceIATEntryInOneMod(
"Kernel32.dll", // Module containing the function (ANSI)
pfnOrig, // Address of the function in callee
MyExitProcess, // Address of the new function to be called
hmodCaller // Handle of module that should call the new function
);
接下來解釋一下 ReplaceIATEntryInOneMod函數
首先調用ImageDirectoryEntryToData并傳入IMAGE_DIRECTORY_ENTRY_IMPORT 為了對hmodCaller的導入段進行定位。
如果ImageDirectoryEntryToData函數傳回NULL,說明Database.exe被映射的虛拟位址(hmodCaller)沒有導入段,不需要執行任何操作。
ImageDirectoryEntryToData是用ImageHlp.dll提供,為了捕獲從這個函數抛出的任何異常需要使用__try/__except代碼塊。把這個函數的調用進行保護。
比如若最後一個hmodCaller傳入NULL,會觸發0xC0000005異常。該情況發生在資料總管中。資料總管會在另一個線程中快速動态載入和解除安裝DLL,進而導緻ReplaceIATEntryInOneMod所線上程引用的子產品句柄變成無效的。
如果Database.exe有一個導入段,那麼ImageDirectoryEntryToData會傳回導入段的位址,這是一個PIMAGE_IMPORT_DESCRIPTOR類型的指針。
接着在子產品導入段查找要修改的導入函數所在的DLL。這裡要查找的是"Kernel32.dll" 利用For循環對DLL子產品的名稱進行搜尋。子產品導入段所有字元格式都是ANSI(絕對不會是Unicode)是以要使用lstrcmpiA來做字元串比較
如果直到循環結束都找不到"Kernel32.dll"中的任何符号的引用,函數便會傳回。不執行任何操作。如果子產品的導入段确實引用了"Kernel32.dll"中的符号,會得到一個位址,這個位址指向一個由IMAGE_THUNK_DATA結構組成的數組。包含與導入符号有關的資訊。接着對每個符符合要求的導入段(borland delphi會生成多個導入段),周遊所有從"Kernel32.dll"中導入的符号,來查找一個與符号目前位址相比對的位址。本例是查找和ExitProcess函數位址相比對的位址。
如果沒有找到要的位址,這個子產品肯定沒有導入我們需要找的符号,ReplaceIATEntryInOneMod直接傳回。如果找到了位址,ReplaceIATEntryInOneMod會調用WriteProcessMemory來将位址修改為代替函數的位址。如果有錯誤發生,會嘗試用VirtualProtect來修改頁面保護屬性,然後再修改一次位址,最後用VirtualProtect來恢複頁面保護屬性。
現在開始任何線程中調用Database.exe子產品的ExitProcess的代碼時,會調用我們的代替函數。如果想要真正讓的ExitProcess處理, 可以輕易得到位于Kernel32.dll中真正的ExitProcess函數位址并調用它。
ReplaceIATEntryInOneMod函數修改的函數調用都來自同一個子產品。位址空間中另一個DLL也可能會調用ExitProcess.如果Database.exe之外的一個子產品也試圖調用ExitProcess,那麼它會成功調用位于Kernel32.dll中的ExitProcess函數
如果想要将修改應用到所有子產品,必須周遊載入的所有子產品并分别調用ReplaceIATEntryInOneMod (作者編寫了一個ReplaceIATEntryInAllMods的通過Toolhelp函數枚舉載入到程序的子產品位址,并分别調用ReplaceIATEntryInOneMod)
以上方法也存在一些問題,如果一個線程在運作時調用LoadLibrary來載入一個新的DLL,新的DLL可能會調用ExitProcess,而我們還沒有攔截這些調用。
是以還必須攔截LoadLibraryA, LoadLibraryW和LoadLibraryExA LoadLibraryExW。
同時如果載入的DLL子產品存在隐式載入别的DLL子產品。應該調用ReplaceIATEntryInAllMods重新更新所有子產品中的攔截函數。
最後一個和GetProcAddress有關的問題。
如果任何一個線程中存在這一的一個代碼。
typedef int (WINAPI * PFNEXITPROCESS) (UINT uExitCode);
PFNEXITPROCESS pfnExitProcess = (PFNEXITPROCESS)GetProcAddress(
GetModuleHandle(TEXT("Kernel32")), "ExitProcess"
);
pfnExitProcess(0);
這将直接調用ExitProcess而不會調用攔截函數。
是以還必須攔截GetProcAddress
以下例子展示了如何攔截API,并解決所有與LoadLibrary和GetProcAddress相關的問題
22.8.3 Last MessageBox Info示例程式
該例子攔截了所有對MessageBox(位于User32.dll中)的調用,為了在所有程序中攔截這個函數,應用程式使用了Windows挂鈎技術來注入DLL。
應用程式啟動以後界面如下:
攔截API是難點所在。作者建立了一個CAPIHook類來進行攔截。使用一下代碼來進行攔截:
CAPIHook g_MessageBoxA("User32.dll", "MessageBoxA",
(PROC)Hook_MessageBoxA, TRUE);
CAPIHook g_MessageBoxA("User32.dll", "MessageBoxW",
(PROC)Hook_MessageBoxW, TRUE);
該類的構造内部會調用ReplaceIATEntryInAllMods來進行攔截。
在析構函數中會調用ReplaceIATEntryInAllMods來把每個子產品中該符号的位址重置為原來的位址。(撤銷攔截)
Hook_MessageBoxA代碼
int WINAPI Hook_MessageBoxA(HWND hWnd, PCSTR pszText,
PCSTR pszCaption, UINT uType) {
int nResult = ((PFNMessageBoxA)(PROC)g_MessageBoxA)
(hWnd, pszText, pszCaption, uType);
SendLastMsgBoxInfo(FALSE, (PVOID)pszCaption, (PVOID)pszText, nResult);
return nResult;
}
注意,由于CAPIHook是在構造函數運作時來攔截特定子產品中的函數的。如果子產品是延遲載入的,那麼CAPIHook的構造函數并不能獲得函數的位址。GetModuleHandleA傳回NULL,GetProcAddress會失敗。 因為延遲載入子產品所提供的優化是知道延遲載入的導出函數真正的被調用,才載入對應的子產品。是以CAPIHook無法處理延遲載入子產品的情況。(筆者的疑問,是否可以攔截__delayLoadHelper2?)
一種方案是,用被攔截的LoadLibrary*函數,來檢測何時一個子產品的一個導出函數應該攔截但尚未攔截,然後執行以下操作:
1)再次對已經載入的子產品的導入表進行攔截,因為現在可以調用GetProcAddress并得到被攔截函數原來的位址。(需要在構造函數中将函數名作為類的一個成員儲存)
2)ReplaceEATEntryInOneMod 函數中顯示的那樣,直接對子產品的導出位址表中的被攔截函數進行更新。這樣是以調用了被攔截函數的新子產品都會調用我們代替的函數。
思考:
如果被攔截函數的子產品由于FreeLibrary調用已經被解除安裝了,會發生什麼?
如果該子產品後來又被重新載入,又會發生什麼?
研究性課題。
源代碼。
LastMsgBoxInfoLib.dll子產品
APIHook.cpp
/******************************************************************************
Module: APIHook.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <ImageHlp.h>
#pragma comment(lib, "ImageHlp")
#include "APIHook.h"
#include "..\CommonFiles\Toolhelp.h"
#include <strsafe.h>
//
// The head of the linked-list of CAPIHook objects
CAPIHook * CAPIHook::sm_pHead = NULL;
// By default, the module containing the CAPIHook() is not hooked
BOOL CAPIHook::ExcludeAPIHookMod = TRUE;
//
CAPIHook::CAPIHook(PSTR pszCalleeModName, PSTR pszFuncName, PROC pfnHook) {
// Note: the function can be hooked only if the exporting module
// is already loaded. A solution could be to store the function
// name as a member; then, in the hooked LoadLibrary* handlers, parse
// the list of CAPIHook instances, check if pszCalleeModName
// is the name of the loaded module to hook its export table and
// re-hook the import tables of all loaded modules.
m_pNext = sm_pHead; // The next node was at the head
sm_pHead = this;
// Save information about this hooked function
m_pszCalleeModName = pszCalleeModName;
m_pszFuncName = pszFuncName;
m_pfnHook = pfnHook;
m_pfnOrig =
GetProcAddressRaw(GetModuleHandleA(pszCalleeModName), m_pszFuncName);
// If function does not exit,... bye bye
// This happens when the module is not already loaded
if (m_pfnOrig == NULL) {
wchar_t szPathname[MAX_PATH];
GetModuleFileNameW(NULL, szPathname, _countof(szPathname));
wchar_t sz[1024];
StringCchPrintfW(sz, _countof(sz),
L"[%4u - %s] impossible to find %S\r\n",
GetCurrentProcessId(), szPathname, pszFuncName);
OutputDebugString(sz);
return;
}
#ifdef _DEBUG
// This section was used for debugging sessions when Explorer died as
// a folder content was requested
//
//static BOOL s_bFirstTime = TRUE;
//if (s_bFirstTime)
//{
// s_bFirstTime = FALSE;
// wchar_t szPathname[MAX_PATH];
// GetModuleFileNameW(NULL, szPathname, _countof(szPathname));
// wchar_t* pszExeFile = wcsrchr(szPathname, L'\\') + 1;
// OutputDebugStringW(L"Injected in ");
// OutputDebugStringW(pszExeFile);
// if (_wcsicmp(pszExeFile, L"Explorer.EXE") == 0)
// {
// DebugBreak();
// }
// OutputDebugStringW(L"\n --> ");
// StringCchPrintfW(szPathname, _countof(szPathname), L"%S", pszFuncName);
// OutputDebugStringW(szPathname);
// OutputDebugStringW(L"\n");
//}
#endif
// Hook this function in all currently loaded modules
ReplaceIATEntryInAllMods(m_pszCalleeModName, m_pfnOrig, m_pfnHook);
}
//
CAPIHook::~CAPIHook() {
// Unhook this function from all modules
ReplaceIATEntryInAllMods(m_pszCalleeModName, m_pfnHook, m_pfnOrig);
// Remove this object from the linked list
CAPIHook *p = sm_pHead;
if (p == this) { // Removing the head node
sm_pHead = p->m_pNext;
}
else {
BOOL bFound = FALSE;
// Walk list from head and fix pointers
for (; !bFound && (p->m_pNext != NULL); p = p->m_pNext) {
if (p->m_pNext == this) {
// Make the node that points to us point to our next node
p->m_pNext = p->m_pNext->m_pNext;
bFound = TRUE;
}
}
}
}
//
// NOTE: This function must NOT be inlined
__declspec(noinline)
FARPROC CAPIHook::GetProcAddressRaw(HMODULE hmod, PCSTR pszProcName) {
return (::GetProcAddress(hmod, pszProcName));
}
//
// Returns the HMODULE that contains the specified memory address
static HMODULE ModuleFromAddress(PVOID pv) {
MEMORY_BASIC_INFORMATION mbi;
return (VirtualQuery(pv, &mbi, sizeof(mbi)) != 0)
? (HMODULE)mbi.AllocationBase : NULL;
}
//
void CAPIHook::ReplaceIATEntryInAllMods(PCSTR pszCalleeModName,
PROC pfnCurrent, PROC pfnNew) {
HMODULE hmodThisMod = ExcludeAPIHookMod
? ModuleFromAddress(ReplaceIATEntryInAllMods) : NULL;
// Get the list of modules in this process
CToolhelp th(TH32CS_SNAPMODULE, GetCurrentProcessId());
MODULEENTRY32 me = { sizeof(me) };
for (BOOL bOk = th.ModuleFirst(&me); bOk; bOk = th.ModuleNext(&me)) {
// NOTE: We don't hook functions in our own module
if (me.hModule != hmodThisMod) {
// Hook this function in this module
ReplaceIATEntryInOneMod(
pszCalleeModName, pfnCurrent, pfnNew, me.hModule);
}
}
}
//
// Handle unexpected exception if the module is unloaded
LONG WINAPI InvalidReadExceptionFilter(PEXCEPTION_POINTERS pep) {
// handle all unexpected exceptions because we simply don't path
// any module in that case
LONG lDisposition = EXCEPTION_EXECUTE_HANDLER;
// Note: pep->ExceptionRecord->ExceptionCode has 0xc0000005 as a value
return lDisposition;
}
void CAPIHook::ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,
PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller) {
// Get the address of the module's import section
ULONG ulSize;
// An exception was trigger by Explorer (when browsing the content of
// a folder) into imagehlp.dll. It looks like one module was unloaded...
// Maybe some threading problem: the list of modules from Toolhelp might
// not be accurate if FreeLibrary is called during the enumeration.
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL;
__try{
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(
hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
}
__except (InvalidReadExceptionFilter(GetExceptionInformation())) {
// Nothing to do in here, thread continues to run normally
// with NULL for pImportDesc
}
if (pImportDesc == NULL)
return; // This module has no import section or is no longer loaded.
// Find the import descriptor containing references to callee's functions
for (; pImportDesc->Name; pImportDesc++) {
PSTR pszModName = (PSTR)((PBYTE)hmodCaller + pImportDesc->Name);
if (lstrcmpiA(pszModName, pszCalleeModName) == 0) {
// Get caller's import address table (IAT) for the callee's functions
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)
((PBYTE)hmodCaller + pImportDesc->FirstThunk);
// Replace current function address with new function address
for (; pThunk->u1.Function; pThunk++) {
// Get the address of the function address
PROC *ppfn = (PROC*)&pThunk->u1.Function;
// Is this function we're looking for?
BOOL bFound = (*ppfn == pfnCurrent);
if (bFound) {
// write memory failed due to the page protect attributes.
if (!WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,
sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError())) {
DWORD dwOldProtect;
// change the page protect to write/copy temp.
if (VirtualProtect(ppfn, sizeof(pfnNew), PAGE_WRITECOPY,
&dwOldProtect)) {
WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,
sizeof(pfnNew), NULL);
VirtualProtect(ppfn, sizeof(pfnNew), dwOldProtect,
&dwOldProtect);
}
}
return; // We did it ,get out
}
}
} // Each import section is parsed until the right entry is found and patched.
}
}
//
void CAPIHook::ReplaceEATEntryInOneMod(HMODULE hmod, PCSTR pszFunctionName,
PROC pfnNew) {
// Get the address of the module's export section
ULONG ulSize;
PIMAGE_EXPORT_DIRECTORY pExportDir = NULL;
__try{
pExportDir = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryEntryToData(
hmod, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &ulSize);
}
__except (InvalidReadExceptionFilter(GetExceptionInformation())) {
// Nothing to do in here, thread continues to run normally
// with NULL for pExportDir
}
if (pExportDir == NULL)
return; // This module has no export section or is unloaded
PDWORD pdwNamesRvas = (PDWORD)((PBYTE)hmod + pExportDir->AddressOfNames);
PWORD pdwNameOrdinals = (PWORD)
((PBYTE)hmod + pExportDir->AddressOfNameOrdinals);
PDWORD pdwFunctionAddresses = (PDWORD)
((PBYTE)hmod + pExportDir->AddressOfFunctions);
// Walk the array of this module's function names
for (DWORD n = 0; n < pExportDir->NumberOfNames; n++) {
// Get the function name
PSTR pszFuncName = (PSTR)((PBYTE)hmod + pdwNamesRvas[n]);
// If not the specified function, try the next function
if (lstrcmpiA(pszFuncName, pszFunctionName) != 0) continue;
// We found the specified function
// --> Get this function's ordinal value
WORD ordinal = pdwNameOrdinals[n];
// Get the address of this function's address
PROC * ppfn = (PROC*)&pdwFunctionAddresses[ordinal];
// Turn the new address into an RVA
pfnNew = (PROC)((PBYTE)pfnNew - (PBYTE)hmod);
// Replace current function address with new function address
if (!WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,
sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError())) {
DWORD dwOldProtect;
if (VirtualProtect(ppfn, sizeof(pfnNew), PAGE_WRITECOPY,
&dwOldProtect)) {
WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew,
sizeof(pfnNew), NULL);
VirtualProtect(ppfn, sizeof(pfnNew), dwOldProtect, &dwOldProtect);
}
}
break; // We did it, get out
}
}
//
// Hook LoadLibrary functions and GetProcAddress so that hooked functions
// are handled correctly if these functions are called.
CAPIHook CAPIHook::sm_LoadLibraryA("Kernel32.dll", "LoadLibraryA",
(PROC)CAPIHook::LoadLibraryA);
CAPIHook CAPIHook::sm_LoadLibraryW("Kernel32.dll", "LoadLibraryW",
(PROC)CAPIHook::LoadLibraryW);
CAPIHook CAPIHook::sm_LoadLibraryExA("Kernel32.dll", "LoadLibraryExA",
(PROC)CAPIHook::LoadLibraryExA);
CAPIHook CAPIHook::sm_LoadLibraryExW("Kernel32.dll", "LoadLibraryExW",
(PROC)CAPIHook::LoadLibraryExW);
CAPIHook CAPIHook::sm_GetProcAddress("Kernel32.dll", "GetProcAddress",
(PROC)CAPIHook::GetProcAddress);
//
void CAPIHook::FixupNewlyLoadedModule(HMODULE hmod, DWORD dwFlags) {
// If a new module is loaded, hook the hooked functions
if ((hmod != NULL) && // Do not hook our own module
(hmod != ModuleFromAddress(FixupNewlyLoadedModule)) &&
((dwFlags & LOAD_LIBRARY_AS_DATAFILE) == 0) &&
((dwFlags & LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE) == 0) &&
((dwFlags & LOAD_LIBRARY_AS_IMAGE_RESOURCE) == 0)
) {
for (CAPIHook * p = sm_pHead; p != NULL; p = p->m_pNext) {
if (p->m_pfnOrig != NULL) {
ReplaceIATEntryInAllMods(p->m_pszCalleeModName,
p->m_pfnOrig, p->m_pfnHook);
}
else {
#ifdef _DEBUG
// We should never end up here
wchar_t szPathname[MAX_PATH];
GetModuleFileNameW(NULL, szPathname, _countof(szPathname));
wchar_t sz[1024];
StringCchPrintfW(sz, _countof(sz),
TEXT("[%4u - %s] impossible to find %S\r\n"),
GetCurrentProcessId(), szPathname, p->m_pszCalleeModName);
OutputDebugString(sz);
#endif
}
}
}
}
//
HMODULE WINAPI CAPIHook::LoadLibraryA(PCSTR pszModulePath) {
HMODULE hmod = ::LoadLibraryA(pszModulePath);
FixupNewlyLoadedModule(hmod, 0);
return hmod;
}
//
HMODULE WINAPI CAPIHook::LoadLibraryW(PCWSTR pszModulePath) {
HMODULE hmod = ::LoadLibraryW(pszModulePath);
FixupNewlyLoadedModule(hmod, 0);
return hmod;
}
//
HMODULE WINAPI CAPIHook::LoadLibraryExA(PCSTR pszModulePath,
HANDLE hFile, DWORD dwFlags) {
HMODULE hmod = ::LoadLibraryExA(pszModulePath, hFile, dwFlags);
FixupNewlyLoadedModule(hmod, dwFlags);
return hmod;
}
//
HMODULE WINAPI CAPIHook::LoadLibraryExW(PCWSTR pszModulePath,
HANDLE hFile, DWORD dwFlags) {
HMODULE hmod = ::LoadLibraryExW(pszModulePath, hFile, dwFlags);
FixupNewlyLoadedModule(hmod, dwFlags);
return hmod;
}
//
FARPROC WINAPI CAPIHook::GetProcAddress(HMODULE hmod, PCSTR pszProcName) {
// Get the true address of the function
FARPROC pfn = GetProcAddressRaw(hmod, pszProcName);
// Is it one of the functions that we want hooked?
CAPIHook *p = sm_pHead;
for (; (pfn != NULL) && (p != NULL); p = p->m_pNext) {
if (pfn == p->m_pfnOrig) {
// The address to return matches an address we want to hook
// Return the hook function address instead
pfn = p->m_pfnHook;
break;
}
}
return pfn;
}
End of File //
APIHook.h
/******************************************************************************
Module: APIHook.h
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#pragma once
//
class CAPIHook {
public:
// Hook a function in all modules
CAPIHook(PSTR pszCalleeModName, PSTR pszFuncName, PROC pfnHook);
// Unhook a function from all modules
~CAPIHook();
// Returns the original address of the hooked function
operator PROC() { return m_pfnOrig; }
// Hook module w/CAPIHook implementation?
// I have to make it static because I need to use it
// in ReplaceIATEntryInAllMods
static BOOL ExcludeAPIHookMod;
public:
// Calls the real GetProcAddress
static FARPROC WINAPI GetProcAddressRaw(HMODULE hmod, PCSTR pszProcName);
private:
static PVOID sm_pvMaxAppAddr; // Maximum private memory address
static CAPIHook * sm_pHead; // Address of first object
CAPIHook * m_pNext; // Address of the next object
PCSTR m_pszCalleeModName; // Module containing the function (ANSI)
PCSTR m_pszFuncName; // Function name in callee (ANSI)
PROC m_pfnOrig; // Original function address in callee
PROC m_pfnHook; // Hook function address
private:
// Replaces a symbol's address in a module's import section
static void WINAPI ReplaceIATEntryInAllMods(PCSTR pszCalleeModName,
PROC pfnOrig, PROC pfnHook);
// Replaces a symbol's address in all modules' import sections
static void WINAPI ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,
PROC pfnOrig, PROC pfnHook, HMODULE hmodCaller);
// Replaces a symbol's address in a module's export sections
static void ReplaceEATEntryInOneMod(HMODULE hmod, PCSTR pszFunctionName, PROC pfnNew);
private:
// Used when a DLL is newly loaded after hooking a function
static void WINAPI FixupNewlyLoadedModule(HMODULE hmod, DWORD dwFlags);
// Used to trap when DLLs are newly loaded
static HMODULE WINAPI LoadLibraryA(PCSTR pszModulePath);
static HMODULE WINAPI LoadLibraryW(PCWSTR pszModulePath);
static HMODULE WINAPI LoadLibraryExA(PCSTR pszModulePath,
HANDLE hFile, DWORD dwFlags);
static HMODULE WINAPI LoadLibraryExW(PCWSTR pszModulePath,
HANDLE hFile, DWORD dwFlags);
// Returns address of replacement function if hooked function is requested
static FARPROC WINAPI GetProcAddress(HMODULE hmod, PCSTR pszProcName);
private:
// Instantiates hooks on these functions
static CAPIHook sm_LoadLibraryA;
static CAPIHook sm_LoadLibraryW;
static CAPIHook sm_LoadLibraryExA;
static CAPIHook sm_LoadLibraryExW;
static CAPIHook sm_GetProcAddress;
};
End of File //
LastMsgBoxInfoLib.cpp
/******************************************************************************
Module: LastMsgBoxInfoLib.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <tchar.h>
#include <stdio.h>
#include "APIHook.h"
#define LASTMSGBOXINFOLIBAPI extern "C" __declspec(dllexport)
#include "LastMsgBoxInfoLib.h"
#include <strsafe.h>
//
// Prototypes for the hooked functions
typedef int(WINAPI *PFNMESSAGEBOXA) (HWND hWnd, PCSTR pszText,
PCSTR pszCaption, UINT uType);
typedef int(WINAPI *PFNMESSAGEBOXW) (HWND hWnd, PCWSTR pszText,
PCWSTR pszCaption, UINT uType);
// We need to reference these variables before we create them.
extern CAPIHook g_MessageBoxA;
extern CAPIHook g_MessageBoxW;
//
// This function sends the MessageBoxinfo to our main dialog box
void SendLastMsgBoxInfo(BOOL bUnicode,
PVOID pvCaption, PVOID pvText, int nResult) {
// Get the pathname of the process displaying the message box
wchar_t szProcessPathname[MAX_PATH];
GetModuleFileNameW(NULL, szProcessPathname, MAX_PATH);
// Convert the return value into a human-readable string
PCWSTR pszResult = L"(Unknown)";
switch (nResult) {
case IDOK: pszResult = L"Ok"; break;
case IDCANCEL: pszResult = L"Cancel"; break;
case IDABORT: pszResult = L"Abort"; break;
case IDRETRY: pszResult = L"Retry"; break;
case IDIGNORE: pszResult = L"Ignore"; break;
case IDYES: pszResult = L"Yes"; break;
case IDNO: pszResult = L"No"; break;
case IDCLOSE: pszResult = L"Close"; break;
case IDHELP: pszResult = L"Help"; break;
case IDTRYAGAIN: pszResult = L"Try Again"; break;
case IDCONTINUE: pszResult = L"Continue"; break;
}
// Construct the string to send to the main dialog box
wchar_t sz[2048];
StringCchPrintfW(sz, _countof(sz), bUnicode
? L"Process: (%d) %s\r\nCaption: %s\r\nMessage: %s\r\nResult: %s"
: L"Process: (%d) %s\r\nCaption: %S\r\nMessage: %S\r\nResult: %s",
GetCurrentProcessId(), szProcessPathname,
pvCaption, pvText, pszResult);
// Send the string to the main dialog box
COPYDATASTRUCT cds = { 0, ((DWORD)wcslen(sz) + 1) * sizeof(wchar_t), sz };
FORWARD_WM_COPYDATA(FindWindow(NULL, TEXT("Last MessageBox Info")),
NULL, &cds, SendMessage);
}
//
// This is the MessageBoxW replacement function
int WINAPI Hook_MessageBoxW(HWND hWnd, PCWSTR pszText, LPCWSTR pszCaption,
UINT uType) {
// Call the original MessageBoxW function
int nResult = ((PFNMESSAGEBOXW)(PROC)g_MessageBoxW)
(hWnd, pszText, pszCaption, uType);
// Send the information to the main dialog box
SendLastMsgBoxInfo(TRUE, (PVOID)pszCaption, (PVOID)pszText, nResult);
// Return the result back to the caller
return nResult;
}
//
// This is the MessageBoxA replacement function
int WINAPI Hook_MessageBoxA(HWND hWnd, PCSTR pszText, PCSTR pszCaption,
UINT uType) {
// Call the original MessageBoxA function
int nResult = ((PFNMESSAGEBOXA)(PROC)g_MessageBoxA)
(hWnd, pszText, pszCaption, uType);
// Send the information to the main dialog box
SendLastMsgBoxInfo(FALSE, (PVOID)pszCaption, (PVOID)pszText, nResult);
// Return the result back to the caller
return nResult;
}
//
// Hook the MessageBoxA and MessageBoxW functions
CAPIHook g_MessageBoxA("User32.dll", "MessageBoxA",
(PROC)Hook_MessageBoxA);
CAPIHook g_MessageBoxW("User32.dll", "MessageBoxW",
(PROC)Hook_MessageBoxW);
HHOOK g_hhook = NULL;
//
static LRESULT WINAPI GetMsgProc(int code, WPARAM wParam, LPARAM lParam) {
return CallNextHookEx(g_hhook, code, wParam, lParam);
}
//
// Returns the HMODULE that contains the specified memory address
static HMODULE ModuleFromAddress(PVOID pv) {
MEMORY_BASIC_INFORMATION mbi;
return (VirtualQuery(pv, &mbi, sizeof(mbi)) != 0)
? (HMODULE)mbi.AllocationBase : NULL;
}
//
BOOL WINAPI LastMsgBoxInfo_HookAllApps(BOOL bInstall, DWORD dwThreadId) {
BOOL bOk;
if (bInstall) {
chASSERT(g_hhook == NULL); // Illegal to install twice in a row
// Install the Windows' hook
g_hhook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc,
ModuleFromAddress(LastMsgBoxInfo_HookAllApps), dwThreadId);
bOk = (g_hhook != NULL);
}
else {
chASSERT(g_hhook != NULL); // Can't uninstall if not installed
bOk = UnhookWindowsHookEx(g_hhook);
g_hhook = NULL;
}
return bOk;
}
End of File //
LastMsgBoxInfoLib.h
/******************************************************************************
Module: LastMsgBoxInfoLib.h
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#ifndef LASTMSGBOXINFOLIBAPI
#define LASTMSGBOXINFOLIBAPI extern "C" __declspec(dllimport)
#endif
///
LASTMSGBOXINFOLIBAPI BOOL WINAPI LastMsgBoxInfo_HookAllApps(BOOL bInstall,
DWORD dwThreadId);
End of File //
LastMsgBoxInfo.exe子產品
LastMsgBoxInfo.cpp
/******************************************************************************
Module: LastMsgBoxInfo.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <tchar.h>
#include "resource.h"
#include "..\LastMsgBoxInfoLib\LastMsgBoxInfoLib.h"
//
BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {
chSETDLGICONS(hWnd, IDI_LASTMSGBOXINFO);
SetDlgItemText(hWnd, IDC_INFO,
TEXT("Waitting for a Message Box to be dismissed"));
return TRUE;
}
//
void Dlg_OnSize(HWND hWnd, UINT state, int cx, int cy) {
SetWindowPos(GetDlgItem(hWnd, IDC_INFO), NULL,
0, 0, cx, cy, SWP_NOZORDER);
}
//
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
switch (id) {
case IDCANCEL:
EndDialog(hWnd, id);
break;
}
}
//
BOOL Dlg_OnCopyData(HWND hWnd, HWND hWndFrom, PCOPYDATASTRUCT pcds) {
// Some hooked process sent us some message box info, display it
SetDlgItemTextW(hWnd, IDC_INFO, (PCWSTR)pcds->lpData);
return TRUE;
}
//
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hWnd, WM_SIZE, Dlg_OnSize);
chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);
chHANDLE_DLGMSG(hWnd, WM_COPYDATA, Dlg_OnCopyData);
}
return FALSE;
}
//
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {
DWORD dwThreadId = 0;
LastMsgBoxInfo_HookAllApps(TRUE, dwThreadId);
DialogBox(hInstExe, MAKEINTRESOURCE(IDD_LASTMSGBOXINFO), NULL, Dlg_Proc);
LastMsgBoxInfo_HookAllApps(FALSE, 0);
return 0;
}
End of File //