windows鉤子

鉤子的概念:

鉤子:是windows消息處理機制中的一個監視點,應用程序可以在這裏安裝一個子程序(鉤子函數)以監視指定窗口某種類型的消息。

鉤子函數:是一個處理消息的程序段,通過調用相關的API函數(SetWindowsHookEx()),把他掛入系統,每當特定的消息發出,當沒有到達目的窗口前,鉤子程序就先捕獲該消息,這時,鉤子函數既可以加工處理該消息,也可以不作處理而繼續傳遞該消息。

鉤子的安裝與卸載:

SetWindowsHookEx函數把應用程序定義的鉤子函數安裝到系統中。該函數原型如下:

WINUSERAPI
HHOOK
WINAPI
SetWindowsHookExW(
    __in int idHook,
    __in HOOKPROC lpfn,
    __in_opt HINSTANCE hmod,
    __in DWORD dwThreadId);

idHook:指定了要安裝的鉤子類型;

dwThreadId:指定要與鉤子函數關聯的線程ID號,如果設爲0,那麼該鉤子就是系統範圍內的,即鉤子函數將關聯到系統內的所有線程。

lpfn:指定相應的鉤子過程,也就是鉤子函數的地址,如果dwThreadId參數爲0,或者指定一個由其它進程創建的線程ID,那麼參數lpfn指向的鉤子函數必須位於一個DLL中。這是因爲進程的地址空間是相互隔離的,發生事件的進程不能調用其它進程地址空間的鉤子函數。如果鉤子函數的實現代碼在DLL中,在相關事件發生時,系統會把這個DLL插入到發生事件的進程的地址空間,使它能夠調用鉤子函數。這種需要把鉤子函數寫入DLL以便掛鉤其它進程事件的鉤子稱爲遠程鉤子

hmod:指定鉤子函數所在DLL的句柄。如果不是遠程鉤子,那麼該參數就設爲NULL。

注意:多個鉤子過程形成鉤子鏈,最後安裝的鉤子過程總是排列在該鏈的前面。鉤子會是系統變慢,因爲增加了系統對每個消息的處理,應該在必要時才安裝鉤子,而且在不需要時應儘快移除。

函數UnhookWindowsHookEx(HHOOK hhk); 卸載鉤子 // hhk爲要卸載的鉤子句柄

CallNextHookEx(): 把鉤子信息傳遞給鉤子鏈中寫一個等待接收信息的鉤子過程。

 

 

進程內鉤子:

安裝進程內鉤子時,不需要用到DLL。

首先要聲明鉤子函數,下面分別聲明瞭鼠標鉤子函數和鍵盤鉤子函數:

LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);

以及它們的實現:

//鼠標鉤子函數
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	return 1;
}

//鍵盤鉤子函數
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	//if(VK_SPACE == wParam)
	if(VK_F4 == wParam &&(1 == (lParam>>29 & 1)))
		return 1;
	else
		return ::CallNextHookEx(g_hKeyboard, nCode, wParam, lParam);
}

需要注意的地方:如果鉤子函數返回非0值,表示已經對當前消息進行了處理,這樣系統就不會再將這個消息傳遞給目標窗口過程了。

爲了保存SetWindowsHookEx函數返回的鉤子過程句柄,我們要定義連個全局變量來保存鼠標和鍵盤的鉤子句柄:

HHOOK g_hMouse	 = NULL;
HHOOK g_hKeyboard = NULL;

 

接下來就是安裝鉤子了,下面我們分別安裝了鼠標和鍵盤鉤子:

//安裝鼠標鉤子
	g_hMouse =
		::SetWindowsHookExA(WH_MOUSE, MouseProc, NULL, ::GetCurrentThreadId());
	//安裝鍵盤鉤子
	g_hKeyboard = 
		::SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, NULL, ::GetCurrentThreadId());


其中GetCurrentThreadId()函數獲取當前線程的ID。

關於鍵盤消息的處理

參數wParam是產生當前按鍵消息的鍵盤按鍵的虛擬鍵代碼,這是Windows定義的,與設備無關。當按下鍵盤上的按鍵時,它實際上發送的是一個脈衝信號。Windows定義了一些虛擬鍵代碼來表示這些信號,並由鍵盤設備驅動程序負責解釋。 鍵盤虛擬鍵的宏都是以" VK_"開頭的。

怎麼判斷組合鍵???  例如 Alt+F4

在鍵盤鉤子函數中,還有另外一個參數lParam,它是一個32爲的整數,它的每一位或某些位表示特定的含義,用來指定按鍵重複的次數、掃描碼、擴展鍵標記、上下文代碼等標記。

判斷組合鍵Alt+F4:VK_F4 == wParam && (1 == (lParam>>29 & 1))

如何讓程序在按下某個特定的按鍵時退出???  例如  按下F2退出程序

爲了讓程序退出,可以利用函數SendMessage()向主程序發送WM_CLOSE消息。

定義一個全局變量 g_hWnd 來保存窗口句柄:

HWND g_hWnd = NULL;

保存窗口句柄:

g_hWnd = m_hWnd;

發送WM_CLOSE消息:

if(VK_F2 == wParam){
		::SendMessageA(g_hWnd, WM_CLOSE, 0, 0);
		::UnhookWindowsHookEx(g_hKeyboard);
		::UnhookWindowsHookEx(g_hMouse);
	}
	return 1;



全局鉤子:
 

如果想要屏蔽當前正在運行的所有進程的鼠標消息和鍵盤消息,那麼安裝鉤子過程的代碼必須放到動態鏈接庫中去實現。如果想要讓安裝的鉤子過程與所有進程相關,應該將SetWindowsHookEx函數的第四個參數設置爲0,並將它的第三個參數指定爲安裝鉤子過程的代碼所在的DLL的句柄。

首先,爲了安裝全局鉤子,我們必須先生成一個.dll文件,於是,創建一個DLL工程:Hook, 用來生成Hook.dll

在Hook工程中的dllmain.cpp文件中完成鉤子鼠標函數的定義和安裝鼠標鉤子:

//鼠標鉤子函數
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	return 1;
}

//安裝鼠標鉤子過程的函數
void SetHook()
{
	g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
}


爲了安裝鉤子過程,需要指定安裝鉤子過程所在DLL模塊的句柄(SetWindowsHookEx的第三個參數),我們有兩種方法來獲得。

第一種:爲DLL程序提供DllMain函數,當第一次加載DLL時,系統會調用這個函數,並傳遞當前DLL模塊的句柄。因此,我們可以定義一個全局的實例變量g_hInst來保存系統傳遞來的DLL模塊句柄,之後就可以在調用SetHook函數中使用這個句柄了。

第二種:調用GetModuleHandle函數來得到指定的DLL模塊的句柄。GetModuleHandle函數返回值是HMODULE類型,HMODULE和HINSTANCE類型可以通用。

	//第一種方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二種方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);


在Hook工程中要將SetHook函數聲明爲導出函數,並且要解決掉名字改變問題,所以我的SetHook函數如下:

extern "C" _declspec(dllexport) void SetHook()
{
	//第一種方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二種方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
}


整個dllmain.cpp文件如下:

// dllmain.cpp : 定義 DLL 應用程序的入口點。
#include "stdafx.h"
#include <Windows.h>

HHOOK g_hMouse = NULL;
HINSTANCE g_hInst;

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	g_hInst = hModule;
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

//鼠標鉤子函數
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	return 1;
}

//安裝鼠標鉤子過程的函數
extern "C" _declspec(dllexport) void SetHook()
{
	//第一種方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二種方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
}

最後,我們就可以生成Hook.dll文件了。

 

接下來就是測試這個DLL文件,我們新建一個基於對話框的MFC工程。

1.在工程中要調用Hook.dll文件中的SetHook函數,所以應在調用前進行聲明,並引入Hook.lib這個導入庫文件:

#pragma comment (lib, "F:\\windows\\000MFC\\HookTest\\Debug\\Hook.lib")
extern "C" _declspec(dllimport) void SetHook();


2.在需要的地方就可以直接調用SetHook函數了。例如在OnInitDialog函數中直接調用SetHook函數。

 

接下來,做一件有趣的事情,我們屏蔽掉所有鍵盤消息,但是除下F2鍵(我們要爲程序開個後門,讓程序退出),當按下F2鍵是程序退出,按下其他鍵不做任何處理。怎麼實現呢???

爲了退出程序,應該向調用進程的主窗口發送一個WM_CLOSE消息。但是在動態鏈接庫中如何才能得到調用進程的主窗口句柄呢

很好解決,我們在調用DLL中的導出函數時,可以將主窗口的句柄作爲參數傳遞給導出函數,我們在DLL中用一個全局變量將主窗口的句柄保存起來。這樣問題就輕鬆解決了!

DLL工程中:

HWND g_hWnd;  //用來保存主窗口的句柄

SetHook函數接收主窗口句柄作爲參數:

void SetHook(HWND hwnd)
{
	g_hWnd = hwnd;
	//第一種方法
	g_hMouse = ::SetWindowsHookExA(WH_MOUSE, MouseProc, g_hInst, 0);
	//第二種方法
	//g_hMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, ::GetModuleHandle("Hook"), 0);
	g_hKeyboard = ::SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, g_hInst, 0);
}

HookTest工程中:

SetHook(m_hWnd);   將主窗口句柄傳遞給DLL中的函數SetHook。

注意:上面的例子中,當切換到其他進程的情況下,按下F2鍵並不能終止HookTest程序,這是爲什麼呢?

我們知道,DLL可以被多個進程供享DLL的代碼和數據,但是如果多個進程共享同一份可寫入的數據的話,你可以想象一下會有什麼後果。爲了解決這個問題,windows中採用了寫入時複製機制。當DLL有一個數據被兩個進程共享,如果第二個進程想修改DLL數據頁面上的數據,操作系統會分配一個新的頁面,並將數據頁面上的數據複製一份到這個新的頁面中。

 

那麼,怎麼才能使在切換到其他進程的情況下,仍然可以使HookTest程序退出

解決方法:爲Hook.dll創建一個新的節,並將此節設置爲一個共享的節,然後將全局變量:g_hWnd放到此節中,讓該全局變量在多個進程間共享。

利用dumpbin -headers [.dll路徑] 查看dll中各節的列表信息。

接下來我們爲Hook.dll創建一個新的節:MySec,並將全局變量g_hWnd放到此節中。創建節可以用指令#pragma data_seg來實現。

#pragma data_seg("MySec")
HWND g_hWnd = NULL;
#pragma data_seg()
#pragma comment(linker, "/section:MySec,RWS")


注意:g_hwnd一定要初始化,否則不會放到新的節中。新建的節的權限爲read 和 write,我們要將它設置爲共享的節,可以使用#pragma comment(linker, "/section:MySec,RWS")。 這裏將指令類型指定爲linker,表明該行代碼用來指定連接選項。而字符串"/section:MySec,RWS"的含義是表明將MySec這個節設置爲讀(R)寫(W)共享(S)類型。

爲了將節設置爲共享類型,我們還可以使用def文件:在DEF文件中利用SEGMENTS關鍵字來實現。

例如:

SEGMENTS

MySec READ WRITE SHARED

這裏不區分大小寫,但不能是縮寫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章