Hook也就是鉤子,在Windows中大部分的應用程序都是基于消息機制,會根據(jù)不同的消息使用消息過程函數(shù)完成不同的功能。而鉤子是一種消息處理機制,它可以比你的應用程序先獲得消息,可以用來截獲、監(jiān)視系統(tǒng)的消息,改變執(zhí)行流程實現(xiàn)特定的功能。對于全局鉤子來說,它會影響所有應用程序,所以鉤子函數(shù)必須在DLL中實現(xiàn)。
SetWindowsHookEx
作用:
將程序定義的鉤子函數(shù)安裝到掛鉤鏈中,安裝鉤子的程序可以監(jiān)視系統(tǒng)是否存在某些類型的時間,這些事件與特定線程或調(diào)用線程所在的桌面中的所有線程相關聯(lián)。
函數(shù)聲明:
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
)
參數(shù):
idHook:
安裝的鉤子程序的類型,具體值參考官方手冊
lpfn:
指向鉤子程序過程的指針,若參數(shù)dwThreadId為0或者指示了一個其他進程創(chuàng)建的線程之標識符,則參數(shù)lpfn必須指向一個動態(tài)鏈接中的掛鉤處理過程。否則,參數(shù)lpfn可以指向一個與當前進程相關的代碼中定義的掛鉤處理過程。
hMod:
包含由lpfn參數(shù)指向的鉤子過程的DLL句柄。
dwThreadId:
與鉤子程序關聯(lián)的線程標識符,如果為0,則鉤子過程與系統(tǒng)中所有線程相關聯(lián)。
返回值:
成功:返回鉤子過程句柄
失敗:返回NULL
UnhookWindowsHookEx
作用:
卸載鉤子
函數(shù)聲明:
BOOL WINAPI UnsetGlobalHook(
_In_ HHOOK hhk
)
參數(shù):
hhk:
卸載的鉤子句柄
使用IDE:VS2019
創(chuàng)建一個DLL項目
pch.h:
#include "framework.h"
extern "C" _declspec(dllexport) int SetGlobalHook();
extern "C" _declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
extern "C" _declspec(dllexport) BOOL UnsetGlobalHook();
#endif //PCH_H
dllmain.cpp
// dllmain.cpp : 定義 DLL 應用程序的入口點。
#include "pch.h"
HMODULE g_hDllModule=NULL;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: {
g_hDllModule=hModule;
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
鉤子過程:
SetGlobalHook(): 設置全局鉤子,WH_GETMESSAGE為監(jiān)視發(fā)送到消息隊列的消息的鉤子,第二個參數(shù)則為鉤子的回調(diào)函數(shù)。
GetMsgProc(): 鉤子的回調(diào)函數(shù),CallNextHookEx表示將當前鉤子傳遞給下一個鉤子,若返回值為0,表示中斷鉤子傳遞,對鉤子進行攔截。
UnsetGlobalHook(): 卸載鉤子
共享內(nèi)存: 由于全局鉤子是以DLL形式加載到進程中,進程都是獨立的,要將進程句柄傳遞給其他進程,可以使用共享內(nèi)存突破進程獨立性,使用"/SECTION:mydata,RWS"設置為可讀可寫可共享的數(shù)據(jù)段。
pch.cpp:
#include "pch.h"
#include #include extern HMODULE g_hDllModule;
// 共享內(nèi)存
#pragma data_seg("mydata")
HHOOK g_hHook=NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")
//鉤子回調(diào)函數(shù)
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam){
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}
// 設置鉤子
BOOL SetGlobalHook() {
g_hHook=SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
if (NULL==g_hHook) {
return FALSE;
}
return TRUE;
}
// 卸載鉤子
BOOL UnsetGlobalHook() {
if (g_hHook) {
UnhookWindowsHookEx(g_hHook);
}
return TRUE;
}
最終生成Dll1.dll
創(chuàng)建c++空項目
編譯下面代碼,將Dll1.dll放在生成的exe下,運行
hook.cpp:
#include #include typedef BOOL(*PEN_HOOKSTART)();
typedef BOOL(*PEN_HOOKSTOP)();
int main() {
//加載dll
HMODULE hDll=LoadLibrary(L"./Dll1.dll");
if (NULL==hDll)
{
printf("LoadLibrary Error[%d]\n", ::GetLastError());
return 1;
}
BOOL isHook=FALSE;
//導出函數(shù)地址
PEN_HOOKSTART SetGlobalHook=(PEN_HOOKSTART)GetProcAddress(hDll, "SetGlobalHook");
if (NULL==SetGlobalHook)
{
printf("SetGlobalHook:GetProcAddress Error[%d]\n", GetLastError());
return 2;
}
PEN_HOOKSTOP UnsetGlobalHook=(PEN_HOOKSTOP)GetProcAddress(hDll, "UnsetGlobalHook");
if (NULL==UnsetGlobalHook)
{
printf("UnsetGlobalHook:GetProcAddress Error[%d]\n", GetLastError());
return 3;
}
isHook=SetGlobalHook();
if (isHook) {
printf("Hook is ok!\n");
}
else {
printf("Hook is error[%d]\n", GetLastError());
}
system("pause");
UnsetGlobalHook();
FreeLibrary(hDll);
return 0;
}
使用Process Explorer查看dll:
可以看到已經(jīng)注入了Dll1.dll
參考文獻
【網(wǎng)絡安全學習攻略】
Windows subclassing - 這種場景比較適合你可以直接修改WndProc,你可以通過SetWindowLongPtr并且以GWLP_WNDPROC并且傳入你自己的WndProc指針,設置完成之后每次操作系統(tǒng)投遞消息給那個window,他會查詢那個 window所擁有的WndProc, 從而調(diào)用你自己的WndProc. 這個做法的缺點是這個操作僅能在進程內(nèi)部本身操作,也就是說你不能在別的進程去subclass其他進程的window class.通常這個方法在你通過add-in(DLL / In Proc COM component)的時候比較有效,比如你可以用BHO來subclass IE本身內(nèi)部的class,比如原來的pop-up menu
Proxy DLL(Trojan DLL) - 一個非常簡單的hack API方法就是替換一個DLL為自己的DLL并且擁有同樣的名字然后導出所有的symbol到原來的那個。這個方法可以直接用Windows提供的function forwarder來輕松達到。這個forwarder其實就是在DLL export section的一個條目來做代理轉發(fā) - #pragma comment(linker, "/export:DoSomething=DllImpl.ActualDoSomething")
Code overwriting - 其中一種最基本的思路就是track down所有的CALL指令,然后替換這個 call指令的調(diào)用地址為我們自己的函數(shù)地址, 另一種會更加復雜: 先定位原來API的地址,然后更改這個地址頭的幾個bytes為JMP指令來跳轉到自己的代碼里。這種方式比較牛逼的實現(xiàn)就是Detours
Spying by a debugger - 通過直接在target function下斷點來進行修改,但是他會讓所有線程被suspend,并且當debuger退出的時候,他的debugee也會被Windows殺掉
Import Address Table - 重點觀察.idata這個section,替換的函數(shù)簽名必須和原來的函數(shù)簽名一致,然后遵守如下步驟: 1) locate import section from IAT 2) 找到IMAGE_IMPORT_DESCRIPTOR這個chunk,我們一般會通過名字來查找DLL 3) 找到IMAGE_THUNK_DATA,他包含了import function的原來地址 4)把這個地址進行替換. 因為.idata并不一定要有寫權限,但是我們這個操作必須要保證.idata這個section是可寫的,我們可以通過VirtualProtect這個API來完成。 還有一個問題需要注意,就是GetProcAddress如果作用在Windows 9x/Me systems上,如果你沒用debugger來起這個程序,他返回的是真正的real pointer,但是如果你是在debugger里面去調(diào)用這個API,他返回的地址其實和之前的不同。原因在于如果你在debugger里去調(diào)用這個GetProcAddress,你所得到的指針其實是一個wrapper而不是real pointer. 通過GetProcAddress返回的value實際上指向的是一個PUSH指令,這個PUSH會緊跟著一個真正的real pointer.這就意味著如果我們在Windows 9x/Me上進行l(wèi)oop在thunks之間,我們需要檢查是否當前examine function是一個PUSH指令(0x68 on x86 platforms)并且得到合適的函數(shù)地址. Windows 9x/Me并沒有實現(xiàn)copy-on-write,所以操作系統(tǒng)會防止debugger進入到2GB的邊界。這也是為什么返回指針返回的是debug thunk而不是real pointer