鉤子的概念:
鉤子:是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
這裏不區分大小寫,但不能是縮寫。