Windows訊息鉤取
一、鉤子和訊息鉤子
鉤子,英文Hook,泛指偷看或擷取資訊時所用的手段或工具。
Windows作業系統向使用者提供GUI,它是以事件驅動(Event Driven)方式工作。事件發生後,OS將事先定義好的訊息傳送給相應的應用程式,應用程式分析收到的訊息後執行相應動作。以敲擊鍵盤為例,
常規Windows訊息流:
- 發生鍵盤輸入事件,WM_KEYDOWN訊息被新增到OS訊息佇列;
- OS判斷哪個應用程式發生了事件,從OS訊息佇列中取出訊息,新增到相應應用程式的app訊息佇列;
- 應用程式監視自身的訊息佇列,發現新新增的WM_KEYDOWN訊息,呼叫相應的事件處理程式進行處理。
附帶鉤子的資訊流:
- 發生鍵盤輸入事件,WM_KEYDOWN訊息被新增到OS訊息佇列;
- OS判斷哪個應用程式發生了事件,從OS訊息佇列中取出訊息,傳送給應用程式;
- 鉤子程式擷取資訊,對訊息採取一定的動作(因鉤子目的而定);
- 如鉤子程式不攔截訊息,訊息最終傳輸給應用程式,此時的訊息可能經過了鉤子程式的修改。
二、SetWindowsHookEx()
這是一個實現訊息鉤子的API,其定義如下:
HHOOK SetWindowsHookEx(
int idHook, // hook type
HOOKpROC lpfn, // hook procedure
HINSTANCE hMod, //hook procedure所屬的DLL控制程式碼
DWORD dwThreadId //需要掛鉤的執行緒ID,為0時表示為全域性鉤子(Global Hook)
);
hook proceduce是由作業系統呼叫的回撥函式;安裝訊息鉤子時,鉤子過程需要存在於某個DLL內部,且該DLL的示例控制程式碼即為hMod。
使用SetWindowsHookEx()設定好鉤子後,在某個程式中生成指定訊息時,OS就會將相關的DLL檔案強制注入(injection)相應程式,然後呼叫註冊的鉤子程式。
三、鍵盤訊息鉤取
以下以書上例子進行練習,首先過程原理圖如下:
KeyHook.dll檔案是一個含有鉤子過程(KeyboardProc)的DLL檔案,HookMain.exe是最先載入KeyHook.dll並安裝鍵盤鉤子的程式。HookMain.exe載入KeyHook.dll後使用SetWindowsHookEx()安裝鍵盤鉤子;若其他程式(如圖中所示)發生鍵盤輸入事件,OS就會強制將KeyHook.dll載入到像一個程式的記憶體,然後呼叫KeyboardProc()函式。
實驗:HookMain.exe
關於實驗操作部分建議跟隨書上走一遍流程,體驗Hook的魅力。
四、原始碼分析
1. HookMain.cpp
HookMain程式的主要原始碼如下所示:
#include "stdio.h"
#include "conio.h"
#include "windows.h"
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();
void main()
{
HMODULE hDll = NULL;
PFN_HOOKSTART HookStart = NULL;
PFN_HOOKSTOP HookStop = NULL;
char ch = 0;
// 載入KeyHook.dll
hDll = LoadLibraryA(DEF_DLL_NAME);
if( hDll == NULL )
{
printf("LoadLibrary(%s) failed!!! [%d]", DEF_DLL_NAME, GetLastError());
return;
}
// 獲取匯出函式地址
HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
// 開始鉤取
HookStart();
// 等待,直到使用者輸入“q”
printf("press 'q' to quit!\n");
while( _getch() != 'q' ) ;
// 終止鉤子
HookStop();
// 解除安裝KeyHook.dll
FreeLibrary(hDll);
}
2. KeyHook.dll
KeyHook.dll原始碼:
#include "stdio.h"
#include "windows.h"
#define DEF_PROCESS_NAME "notepad.exe"
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch( dwReason )
{
case DLL_PROCESS_ATTACH:
g_hInstance = hinstDLL;
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szPath[MAX_PATH] = {0,};
char *p = NULL;
if( nCode >= 0 )
{
// bit 31 : 0 => press, 1 => release
if( !(lParam & 0x80000000) ) //釋放鍵盤按鍵時
{
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');
//比較當前程式名稱是否為notepad.exe,成立則訊息不傳遞給應用程
if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
return 1;
}
}
//如果不是notepad.exe,則呼叫CallNextHookEx()函式,將訊息傳遞給應用程式
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}
__declspec(dllexport) void HookStop()
{
if( g_hHook )
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
#ifdef __cplusplus
}
#endif
總體上程式碼相對簡單,呼叫匯出函式HookStart()時,SetWindowsHookEx()函式就會將KetyboardProc()新增到鍵盤鉤鏈。
3. 程式碼執行流程分析
安裝好鍵盤鉤子後,無論在哪個程式中,只要發生了鍵盤輸入事件,OS就會強制將KeyHook.dll注入到程式中,載入了KeyHook.dll的程式,發生鍵盤事件時會首先呼叫執行KeyHook.KetyboardProc()。
KetyboardProc()函式中發生鍵盤輸入事件時,會比較當前程式的名稱與“notepad.exe”是否相同,相同返回1,終止KetyboardProc()函式,意味著截獲並刪除了訊息,這樣鍵盤訊息就不會傳遞到notepad.exe程式的訊息佇列。
五、除錯
使用OD開啟HookMain.exe檔案:
###1. 查詢核心程式碼
我們關心的是核心的鍵盤鉤取部分的程式碼,如何查詢核心程式碼?
- 逐步跟蹤(除非迫不得已!)
- 檢索相關API
- 檢索相關字串
我們已經知道程式的功能,會在控制檯顯示字串“press ‘q’ to quit!”,所以先檢查程式匯入的字串(Search for -All referencen text strings):
地址40104d處引用了要查詢的字串,雙擊跳轉:
來到main函式處。
2. 除錯main函式
在401000處下斷,開始除錯,瞭解main函式中主要的程式碼流。401006地址處呼叫LoadLibraryA(Keyhook.dll),然後由40104b地址處的CALL EBX指令呼叫KeyHook.HookStart()函式。跟進檢視:
這裡的程式碼是被載入到HookMain.exe程式中的KeyHook.dll的HookStart()函式,第一句就是呼叫SetWindowsHookExW()函式,在進行引數入棧操作後,我們可以在棧中看到函式的4個引數值。
參考
《逆向工程核心原理》