Windows訊息鉤取

Mi1k7ea發表於2018-06-13

Windows訊息鉤取

簡單地說,訊息鉤取就是偷看、擷取資訊。

常規Windows訊息流:

1、發生鍵盤輸入事件時,WM_KEYDOWN訊息被新增到[OS message queue];

2、OS判斷哪個應用程式中發生了事件,然後從[OS message queue]中取出訊息,新增到相應應用程式的[application message queue]中;

3、應用程式監視自身的[application message queue],發現新新增的WM_KEYDOWN訊息後,呼叫相應的事件處理程式處理。


訊息鉤子:

Windows OS向使用者提供GUI,其是以事件驅動的方式進行工作的。每當發生這樣的事件時,OS會將事先定義好的訊息傳送給相應的應用程式,應用程式分析收到的資訊後執行相應動作。即在敲擊鍵盤時,訊息從OS傳遞到應用程式,此過程中訊息鉤子可以偷看其中的資訊。

訊息鉤子是Windows OS提供的基本功能,其中最具代表性的是VS Visual Studio中提供的SPY++,其是一個功能強大的訊息鉤取程式,能夠檢視OS中來往的所有訊息。

如下圖,OS訊息佇列和應用程式訊息佇列之間存在一條鉤鏈(Hook Chain),設定好鍵盤訊息鉤子後,處於鉤鏈中的鍵盤訊息鉤子會比應用程式先一步看到相應資訊。在鍵盤訊息鉤子函式的內部,除了可以檢視訊息之外,還可以修改訊息本身,而且還能對訊息實施攔截,阻止訊息傳遞。可以同時設定多個相同的鍵盤訊息鉤子,按照設定的順序依次呼叫,從而組成的鏈條稱為鉤鏈。



Windows訊息鉤取的實現——SetWindowsHookEx():

在Windows程式設計中,使用SetWindowsHookEx() API可以簡便地實現訊息鉤子,其用於將指定的鉤子註冊到鉤鏈中,無論在DLL內部或外部都可呼叫。

SetWindowsHookEx() API定義如下:

HHOOK SetWindowsHookEx(
    int idHook,			//hook type
    HOOKPROC lpfn,		//hook procedure
    HINSTANCE hMod,		//hook procedure所屬的DLL控制程式碼
    DWORD dwThreadId	//將要掛鉤的目標執行緒ID
);

HHOOK:返回值,鉤子控制程式碼,需要保留,等不使用鉤子時通過UnhookWindowsHookEx函式解除安裝鉤子。

idHook:鉤子的攔截訊息型別,選擇鉤子程式的攔截範圍,具體值參考文章結尾的訊息型別。

Lpfn:訊息的回撥函式地址,一般是填函式名。

hMod:鉤子函式所在的例項的控制程式碼。對於執行緒鉤子,該引數為NULL;對於系統鉤子,該引數為鉤子函式所在的DLL控制程式碼。在dll中可通過AfxInitExtensionModule(MousehookDLL, hInstance)獲得DLL控制程式碼。

dwThreadId:鉤子所監視的執行緒的執行緒號,可通過GetCurrentThreadId()獲得執行緒號。對於全域性鉤子,該引數為NULL(或0)。


鉤子過程(hook procedure)是由OS呼叫的回撥函式。安裝訊息鉤子時,鉤子過程需要存在於某個DLL內部,並且該DLL的例項控制程式碼即是hMod。

使用SetWindowsHookEx()設定好鉤子後,在某個程式中生成指定訊息時,OS會將相關的DLL檔案強制注入相應的程式,然後呼叫註冊的鉤子過程。


鍵盤訊息鉤子練習:

KeyHook.dll檔案是含有鉤子過程的DLL檔案,HookMain.exe是最先載入KeyHook.dll並安裝鍵盤鉤子的程式。HookMain.exe載入KeyHook.dll檔案後使用SetWindowsHookEx()安裝鍵盤鉤子。若其他程式中發生鍵盤輸入事件,則OS會強制將KeyHook.dll載入到相應程式的記憶體,然後呼叫KeyboardProc()函式。注意的是,OS會將KeyHook.dll載入到發生鍵盤輸入事件的所有程式,即訊息鉤取技術是一種DLL注入技術。

現在開始編寫程式碼。

KeyHook.cpp

//KeyHook.cpp

#include "stdio.h"
#include "windows.h"

//定義目標程式名為notepad.exe
#define DEF_PROCESS_NAME "notepad.exe"

//定義全域性變數
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;

//DllMain()函式在DLL被載入到程式後會自動執行
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,則訊息不會傳遞給應用程式或下一個鉤子函式
			//_stricmp()函式用於比較字串,i表示不區分大小寫,若兩個值相等則返回0
			if( !_stricmp(p + 1, DEF_PROCESS_NAME) ){
				return 1;
			}
		}
	}

	//比較當前程式名稱,若非notepad.exe,則訊息傳遞給應用程式或下一個鉤子函式
	return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

//在C++中呼叫C的庫檔案,用extern "C"告知編譯器,因為C++支援函式過載而C不支援,兩者的編譯規則不同
#ifdef __cplusplus
extern "C"{
#endif
	//__declspec,針對編譯器的關鍵字,用於指出匯出函式
	//當呼叫匯出函式HookStart()時,SetWindowsHookEx()函式就會將KeyboardProc()新增到鍵盤鉤鏈
	__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

因為要生成的是KeyHook.dll檔案,因而在開始建立專案時應先選擇Win 32控制檯應用程式,再到應用程式型別中勾選DLL,編寫好cpp檔案後選擇Release再生成檔案即可得到DLL檔案。

當呼叫匯出函式HookStart()時,SetWindowsHookEx()函式就會將KeyboardProc()新增到鍵盤鉤鏈。安裝好鍵盤鉤子後,無論哪個程式,只要發生鍵盤輸入事件,OS都會強制將KeyHook.dll注入相應的程式中。

KeyboardProc()函式中發生鍵盤輸入事件時,會比較當前程式名稱和“notepad.exe”是否一致,若一致則返回1,終止KeyboardProc()函式,即截獲並刪除訊息,從而實現對notepad.exe程式的鍵盤輸入事件進行鉤取並截獲刪除、鍵盤訊息不能傳遞到notepad.exe的訊息佇列中。

KeyboardProc()函式定義如下:

LRESULT CALLBACK KeyboardProc(
    int code,			//HC_ACTION(0), HC_NOREMOVE(3)
    WPARAM wParam,		//virtual-key code
    LPARAM lParam		//extra information
);

其中wParam指使用者按下的鍵盤按鍵的虛擬鍵值。


HookMain.cpp

//HookMain

#include "stdio.h"
#include "windows.h"
//Console Input/Output,定義了通過控制檯進行資料輸入和資料輸出的函式
//主要是一些使用者通過按鍵盤產生的對應操作,比如getch()函式等等
#include "conio.h"

//定義一些常量
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"

//定義兩個引數為空、返回值為void即沒有的函式指標
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();

void main(){
	//定義及初始化控制程式碼變數和函式指標
	HMODULE hDll = NULL;
	PFN_HOOKSTART HookStart = NULL;
	PFN_HOOKSTOP HookStop = NULL;

	//載入KeyHook.dll
	hDll = LoadLibraryA(DEF_DLL_NAME);

	//若載入不成功,則輸出錯誤資訊
	if( hDll == NULL ){
		printf("[-]無法載入%s [%d]\n", DEF_DLL_NAME, GetLastError());
		return;
	}

	//獲取匯出函式地址
	HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
	HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);

	//開始鉤取
	HookStart();

	//直至使用者輸入“q”退出鉤取
	printf("[*]等待輸入 'q' 來停止鉤取...\n");
	while( _getch() != 'q' );

	//終止鉤取
	HookStop();

	//解除安裝KeyHook.dll
	FreeLibrary(hDll);
}

先載入KeyHook.dll,再呼叫HookStart()函式開始鉤取,當獲取到使用者輸入“q”後呼叫HookStop()函式終止鉤取。

由於檔案頭包含了“conio.h”庫檔案進來,一開始使得程式無法生成exe檔案:


conio即console input/output,顧名思義,可知其是需要Console進行操作的而非Windows,需要將連結器中的系統的子系統從Windows修改為Console:



執行除錯

將HookMain.exe和KeyHook.dll放在同一目錄,在cmd中執行HookMain.exe:


開啟notepad.exe,輸入內容,發現並沒有輸入進去,到Process Explorer中檢視notepad.exe程式,可以看到KeyHook.dll已經載入到其中,最後在cmd視窗中輸入q退出鉤取即可:


再開啟notepad++.exe檔案進行輸入,發現可以輸入內容,且也強制載入了KeyHook.dll檔案:



除錯Windows訊息鉤取

1、除錯HookMain.exe:

使用Ollydbg開啟,可以看到是典型的VC++啟動函式:


接著使用字串檢索法檢視核心程式碼,由於輸出顯示的內容是中文,因而找不到輸出內容字串(如果改為英文就方便多了),但是可以看到有定義變數時用到的字串如“HookStart”:


點選“HookStart”部分進入程式碼即是HookMain.exe的main()函式:


程式先在011A1006地址處呼叫LoadLibraryA(KeyHook.dll),然後在011A104E地址處的CALLEBX指令呼叫KeyHook.HookStart()函式。跟蹤進入該函式檢視:


在74C210EF地址處可以看到,呼叫了SetWindowsHookExW()函式,其上方兩條PUSH指令將該函式的第一、第二兩個引數壓入棧。其中第一個引數idHook值為2,即WH_KEYBOARD,第二個引數lpfn值為74C21020,該值即為鉤子過程的地址。

返回main()函式之後的程式碼即為接收使用者輸入的q後即終止鉤取。


2、除錯notepad.exe程式內的KeyHook.dll:

使用Ollydbg開啟notepad.exe,再F9使其執行起來。當然也可以先執行notepad.exe再開啟Ollydbg來attach該程式。

在Ollydbg中的Options>Debugging options>Events中勾選如下選項,即當新的DLL載入被除錯程式時會自動暫停除錯:


執行HookMain.exe,到notepad.exe中進行鍵盤輸入,此時Ollydbg暫停除錯並彈出Executable modules視窗:


但是卻沒有發現有KeyHook.dll。

根據系統環境不同,有時不會先顯示KeyHook.dll,而是先載入其他DLL庫。此時可以F9直至KeyHook.dll載入完成。但可能有的系統不能正常顯示,這時可以使用Ollydbg 2.0來檢視。

再次F9執行,發現了KeyHook:


可以看到,鉤子過程的地址為74C21498。

取消之前的Break on new module(DLL)設定,轉到該地址檢視,向起始地址處設定斷點:


可以發現,當每次notepad.exe程式中發生鍵盤輸入事件時,除錯都會停止在該斷點處。

相關文章