Windows核心程式設計_Hook

17歲boy想當攻城獅發表於2018-06-22

一、前言

HookWindows下的一個機制,Hook的中文意思是鉤子的意思,顧名思義,鉤子就是用來鉤東西的,就好像釣魚一樣,你把魚鉤放入魚塘裡,釣到了某條魚,即便我們不把魚釣上來,我們可以通過魚鉤知道魚在做什麼,比如魚飛速遊動,魚鉤上的魚線會做出反應,或者魚原地不動,我們都可以通過魚鉤知道魚在做什麼!

Windows就像一個魚塘,而程式,就是魚塘裡的魚,而用來監視這些魚的魚鉤就是Hook

眾所周知,Windows平臺上的程式是以事件驅動和訊息為基礎工作的,事件與訊息是關聯的,訊息的觸發來響應事件,比如我單擊了一個應用程式上的按鈕,那麼此時這個應用程式會觸發一個訊息即為MK_LBUTTON訊息被髮送到系統的訊息佇列裡(觸發過程是由作業系統根據滑鼠點選某個視窗上某個控制元件來觸發的,原理上是由作業系統觸發的訊息),這些訊息都是以資料結構形式儲存的,每個佇列裡不僅儲存訊息還有觸發的視窗控制程式碼等引數資訊,Windows會把這些訊息在轉發給指定視窗程式下的訊息佇列,然後由訊息佇列來處理或者由作業系統來預設處理,學過Win32程式設計和MFC程式設計的應該比較熟悉,這裡的訊息流程只是僅限於Windows提供的WindowsSDK庫下的API介面開發的程式!


就好像控制元件一樣,控制元件其實就相當於一個視窗,只是一個需要容器的視窗,也有自己的訊息迴圈,學過COM元件開發的都知道基於COM元件開發ActiveX控制元件裡也有一套訊息迴圈機制和對應的響應事件,比如按鈕的獲取焦點對應的繪圖函式!


其它框架不一定使用此方法,比如QT的訊號和槽,但QT內部封裝了Windows訊息和事件驅動模型,原理上來說,你單擊QT上的一個按鈕還是會產生MK_LBUTTON訊息並返回給對應的QT程式,還是會返回到程式的訊息迴圈佇列裡去,只是QT用了訊號和槽的方式來代替訊息與事件,比如你用第三方軟體給QT程式上的某個按鈕傳送MK_LBUTTON訊息還是會響應對應的事件,因為QT上的控制元件都是基於Windows下的SDK介面來實現完成的,QT的核心還是:觸發訊息>系統訊息>應用訊息,和Windows一樣,只是內部封裝起來了!


.鉤子執行機制

1.1 鉤子連結串列與鉤子

鉤子是Windows平臺下的一個用於截獲訊息的程式段,Windows內部也在用它,當我們使用特定函式來安裝一個鉤子時,作業系統會給這個鉤子分配一個鉤子連結串列,並且這個鉤子就相當於一小段程式,稱為鉤子子程式,這段程式會根據鉤子型別的不同,來實現不同程度的訊息截獲,並且這個鉤子連結串列裡包含了這個鉤子程式的地址,型別,回撥函式的地址!

並且鉤子子程式的優先順序會高於應用程式,在接受訊息時會被鉤子子程式先行截獲,作業系統會先把訊息傳送給鉤子,由鉤子決定這些訊息是否傳送下去,鉤子可以攔截這些訊息,可以決定這些訊息的作用,甚至可以遮蔽這些訊息不讓傳遞到指定應用程式當中!

上面說過了每個程式上的鉤子是由一個鉤子連結串列來維護的,多個鉤子那麼連結串列上就會有多個子節點,當作業系統把訊息傳遞給鉤子時,會首先傳遞給連結串列首節點的鉤子子程式,首節點的鉤子可以決定這個訊息是否傳遞給下一個鉤子!

在安裝鉤子的時候是由順序之分的,連結串列遵循的是先進後出,也就是說鉤子連結串列的首節點始終是最後一個安裝鉤子的鉤子子程式,並且每個程式下只能有一個鉤子,如果重複安裝鉤子會安裝失敗!


1.2 鉤子型別

1.1 全域性鉤子

全域性鉤子即用於鉤系統訊息佇列裡的訊息,上面說過,所有程式觸發的訊息都會被作業系統傳送到訊息佇列裡(這樣做的原因是根據滑鼠點選螢幕畫素點區域來確定點選的是哪個視窗,視窗上的哪個控制元件),在由作業系統將訊息佇列裡的訊息傳送給指定視窗下的訊息佇列,全域性鉤子的作用就是鉤系統級的訊息佇列,當作業系統將訊息佇列裡的訊息傳送出去時會率先傳送給全域性鉤子,並且由全域性鉤子截獲,全域性鉤子決定這些訊息的存亡,鉤子可以決定處理或不處理,也可以決定處理完之後再傳送給應用程式或者不處理直接傳送給應用程式,不過這樣做的話鉤子的意義就不大了!


1.2 區域性鉤子

區域性鉤子即只鉤程式下的訊息,當作業系統在將訊息傳送給各個程式時,作業系統不會把所有訊息都傳送給此鉤子,而是隻把當前程式下產生的訊息在傳送給程式下的訊息佇列時先傳送給鉤子子程式,而不是直接發給訊息佇列,由鉤子子程式決定這些訊息的處理方式!


  1.3 HOOK應用模式

觀察模式:

最常用的應用模式,即簡單建立一個Hook子程式,用於觀察某些程式下的訊息,並對其進行截獲處理!


        注入模式:

即通過Hook子程式將DLL動態庫注入到某個程式下,使其成為程式的一部分!


        替換模式(注入模式的一種)

利用Hook子程式將動態庫注入到某個程式下,並攔截某個程式呼叫函式過程,將呼叫函式替換成自己DLL動態庫函式!(黑客常用)


         外掛模式(注入模式的一種)

將動態庫函式注入到指定程式下,並協調呼叫動態庫函式,擴充套件程式業務!


 修復模式(替換模式的一種)

利用Hook技術將某個訊息對應的函式替換成新的執行函式!


其還有其他應用模式,這一般取決於使用者怎麼使用Hook,用它做些什麼!


1.4 Hook的執行機制-

上面說過Hook的工作機制,Hook的子程式的生存週期是由使用者而決定的,當我們安裝Hook子程式之後,就意味著作業系統要建立一張鉤子連結串列來維護它,並且每次傳遞訊息都要率先傳遞給鉤子子程式,如果鉤子子程式沒有做任何處理則再由鉤子子程式傳送給對應的程式,那麼這樣來來回回,就需要浪費了很多時間,耗費系統資源,所以當在使用完鉤子之後建議立馬解除安裝,避免影響系統執行效率,並且如果你安裝了鉤子,但是在程式結束時鉤子都沒有被解除安裝,那麼作業系統會幫你解除安裝這些鉤子!


其鉤子連結串列,這裡要說一下,倘若多個程式多個鉤子,其這些鉤子連結串列是由作業系統來維護的,作業系統會生成一張系統用的鉤子連結串列,這個連結串列負責儲存每個鉤子的相關資訊!


Hook的缺點非常明顯,那就是當某個程式沒有觸發任何窗體訊息時那麼Hook永遠不會被執行,其Windows下用於繪製視窗和處理事件驅動的模組是user32.dll,也就是說當一個視窗被建立和事件驅動等訊息機制被建立出來時就會載入user32動態庫,呼叫裡面的API來完成,倘若某個程式在進行一些複雜的計算公式,不去呼叫user32那麼Hook(僅限區域性,因為不是所有的程式都會這樣)將永遠不會被執行!


.實踐

到了這一章相比各位的理論知識已經很足了,那麼就可以開始進行實踐了,畢竟實踐來自於理論!

開始之前先介紹幾個所需AIP函式:

1.SetWindowsHookEx

函式原型:

WINUSERAPI HHOOK WINAPI SetWindowsHookExW(    _In_ int idHook,    _In_ HOOKPROC lpfn,    _In_opt_ HINSTANCE hmod,    _In_ DWORD dwThreadId);
引數介紹:
_In_ int idHook:	鉤子型別,可取以下值:
WH_MSGFILTER    = -1; 執行緒級; 截獲使用者與控制元件互動的訊息
WH_JOURNALRECORD  = 0; 系統級; 記錄所有訊息佇列從訊息佇列送出的輸入訊息, 在訊息從佇列中清除時發生; 可用於巨集記錄
WH_JOURNALPLAYBACK = 1; 系統級; 回放由 WH_JOURNALRECORD 記錄的訊息, 也就是將這些訊息重新送入訊息佇列
WH_KEYBOARD    = 2; 系統級或執行緒級; 截獲鍵盤訊息
WH_GETMESSAGE   = 3; 系統級或執行緒級; 截獲從訊息佇列送出的訊息
WH_CALLWNDPROC   = 4; 系統級或執行緒級; 截獲傳送到目標視窗的訊息, 在 SendMessage 呼叫時發生
WH_CBT       = 5; 系統級或執行緒級; 截獲系統基本訊息, 譬如: 視窗的建立、啟用、關閉、最大最小化、移動等等有用的窗體控制元件訊息
WH_SYSMSGFILTER  = 6; 系統級; 截獲系統範圍內使用者與控制元件互動的訊息
WH_MOUSE      = 7; 系統級或執行緒級; 截獲滑鼠訊息
WH_HARDWARE    = 8; 系統級或執行緒級; 截獲非標準硬體(非滑鼠、鍵盤)的訊息
WH_DEBUG      = 9; 系統級或執行緒級; 在其他鉤子呼叫前呼叫, 用於除錯鉤子
WH_SHELL      = 10; 系統級或執行緒級; 截獲發向外殼應用程式的訊息
WH_FOREGROUNDIDLE = 11; 系統級或執行緒級; 在程式前臺執行緒空閒時呼叫
WH_CALLWNDPROCRET = 12; 系統級或執行緒級; 截獲目標視窗處理完畢的訊息, 在 SendMessage 呼叫後發生

__in HOOKPROC lpfn:回撥函式地址
其回撥函式原型為:
LRESULT CALLBACK name(int nCode, WPARAM wParam, LPARAM lParam)
name可以隨便起,這裡來解釋一下回撥函式的引數:
int nCode:訊息程式碼
 WPARAM wParam:附加引數一
LPARAM lParam:附加引數二
返回值:LRESULT型別(便於傳遞hook,後面詳細介紹)
__in HINSTANCE hMod:DLL實列控制程式碼
__in DWORD dwThreadId:要掛鉤的執行緒ID
函式作用:設定鉤子

2.CallNextHookEx

函式原型:

LRESULT
WINAPI
CallNextHookEx(
    _In_opt_ HHOOK hhk,
    _In_ int nCode,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam);
引數介紹:
 _In_opt_ HHOOK hhk:下一個鉤子子程的控制程式碼
_In_ int nCode: 鉤子訊息程式碼,此訊息程式碼會被傳遞給下一個鉤子處理
 _In_ WPARAM wParam:訊息附加引數一
 _In_ LPARAM lParam:訊息附加引數二

函式作用:將訊息傳遞給下一個鉤子子程式,比如你有多個鉤子,當你回撥函式處理完之後,可以使用此函式將訊息傳遞給下一個鉤子子程處理,也可以選擇不傳遞,在windows裡最新鉤子子程式每次會第一個獲取到此訊息,上面說過鉤子連結串列這裡就不多說了,同時此函式也可以用來遮蔽訊息,後面詳細說明!

返回值:LRESULT型別,返回值是hook鉤子型別程式碼

3.UnhookWindowsHookEx

函式原型:


BOOL WINAPI UnhookWindowsHookEx( __in HHOOK hhk);
引數介紹:
__in HHOOK hhk:要刪除的hook鉤子控制程式碼
返回值:成功true否則false

函式作用:釋放鉤子

下面開始幾個列子講解此函式應該怎樣使用:


1.截獲當前執行緒鍵盤訊息(區域性鉤子)

這裡我們先新建一個win32程式


#include "stdafx.h"
#include <windows.h>

HWND hWnd;	//progman

//訊息函式
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	//判斷訊息ID
	switch (uMsg){
	
	case WM_DESTROY:    // 視窗銷燬訊息
		PostQuitMessage(0);   //  傳送退出訊息
		return 0;
	}
	// 其他的訊息呼叫預設的訊息處理程式
	return DefWindowProc(hwnd, uMsg, wParam, lParam);

}
// 3、註冊視窗型別
BOOL RegisterWindow(LPCSTR lpcWndName, HINSTANCE hInstance)
{
ATOM nAtom = 0;
	// 構造建立視窗引數
	WNDCLASS wndClass = { 0 };
	wndClass.style = CS_HREDRAW | CS_VREDRAW;
	wndClass.lpfnWndProc = WindowProc;      // 指向視窗過程函式
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hInstance = hInstance;
	wndClass.hIcon = NULL;
	wndClass.hCursor = NULL;
	wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wndClass.lpszMenuName = NULL;
	wndClass.lpszClassName = lpcWndName;    // 註冊的視窗名稱,並非標題,以後建立視窗根據此註冊的名稱建立
	nAtom = RegisterClass(&wndClass);
	return TRUE;
}
//建立視窗(lpClassName 一定是已經註冊過的視窗型別)
HWND CreateMyWindow(LPCTSTR lpClassName, HINSTANCE hInstance)
{
	HWND hWnd = NULL;
	// 建立視窗
	hWnd = CreateWindow(lpClassName, "test", WS_OVERLAPPEDWINDOW^WS_THICKFRAME, 0, 0, 1000, 800, NULL, NULL, hInstance, NULL);
	return hWnd;
}
//顯示視窗
void DisplayMyWnd(HWND hWnd)
{
	//獲得螢幕尺寸

	int scrWidth = GetSystemMetrics(SM_CXSCREEN);
	int scrHeight = GetSystemMetrics(SM_CYSCREEN);
	RECT rect;
	GetWindowRect(hWnd, &rect);
	ShowWindow(hWnd, SW_SHOW);
	//重新設定rect裡的值
	rect.left = (scrWidth - rect.right) / 2;
	rect.top = (scrHeight - rect.bottom) / 2;
	//移動視窗到指定的位置
	SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW);
	UpdateWindow(hWnd);
}

void doMessage()        // 訊息迴圈處理函式
{
	MSG msg = { 0 };
	// 獲取訊息
	while (GetMessage(&msg, NULL, 0, 0)) // 當接收到WM_QIUT訊息時,GetMessage函式返回0,結束迴圈
	{
		DispatchMessage(&msg); // 派發訊息,到WindowPro函式處理
	}
}

// 入口函式
int WINAPI WinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nShowCmd)
{
	HWND hWnd = NULL;
	LPCTSTR lpClassName = "MyWnd";  // 註冊視窗的名稱
	RegisterWindow(lpClassName, hInstance);
	hWnd = CreateMyWindow(lpClassName, hInstance);
	DisplayMyWnd(hWnd);
	doMessage();
	return 0;
}

win32視窗建立程式碼基本上寫完了,我們先定義一個全域性變數hook方便回撥函式傳遞hook


//hook
HHOOK hook;
在定義一個回撥函式:
//hook回撥函式
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	//攔截所有的鍵盤訊息
	MessageBox(NULL, "dd", "鍵盤訊息被觸發了", 0);
	return CallNextHookEx(hook, nCode, wParam, lParam);
}

這裡來說一下為什麼回撥函式的型別是LRESULT,通過上面的程式碼可以發現CallNextHookEx函式返回值是LRESULT型別的,所以想要相容此函式返回值也必須是LRESULT型別的,其返回值是鉤子型別的程式碼,這個我們無需關係!

註冊hook

hook = SetWindowsHookEx(WH_KEYBOARD, LowLevelMouseProc, NULL, GetCurrentThreadId());	//GetCurrentThreadId()API函式可以呼叫執行緒的執行緒ID

WH_KEYBOARD只攔截鍵盤訊息

執行效果:

這個時候我們隨便按下一個按鍵:



成功攔截到此訊息了,這裡我們在把回撥函式裡的程式碼稍微更改一下,只對空格鍵有效:


//攔截空格訊息
	if (wParam == VK_SPACE)
		MessageBox(NULL, "空格被按下了, "鍵盤訊息被觸發了", 0);
	return CallNextHookEx(hook, nCode, wParam, lParam);
//VK_SPACE是空格的鍵程式碼,這裡我們沒有使用鍵程式碼到字元訊息之間的對映轉換,所以我們要直接使用鍵程式碼來比對,鍵盤訊息的附加鍵程式碼存放在wParam引數裡

執行結果:



無論我們按下什麼按鍵都沒有反應,這裡我們按下空格試試:






完整程式碼:


#include "stdafx.h"
#include <windows.h>

HWND hWnd;	//progman

//訊息函式
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	//判斷訊息ID
	switch (uMsg){
	
	case WM_DESTROY:    // 視窗銷燬訊息
		PostQuitMessage(0);   //  傳送退出訊息
		return 0;
	}
	// 其他的訊息呼叫預設的訊息處理程式
	return DefWindowProc(hwnd, uMsg, wParam, lParam);

}
// 3、註冊視窗型別
BOOL RegisterWindow(LPCSTR lpcWndName, HINSTANCE hInstance)
{
	ATOM nAtom = 0;
	// 構造建立視窗引數
	WNDCLASS wndClass = { 0 };
	wndClass.style = CS_HREDRAW | CS_VREDRAW;
	wndClass.lpfnWndProc = WindowProc;      // 指向視窗過程函式
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hInstance = hInstance;
	wndClass.hIcon = NULL;
	wndClass.hCursor = NULL;
	wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wndClass.lpszMenuName = NULL;
	wndClass.lpszClassName = lpcWndName;    // 註冊的視窗名稱,並非標題,以後建立視窗根據此註冊的名稱建立
	nAtom = RegisterClass(&wndClass);
	return TRUE;
}
//建立視窗(lpClassName 一定是已經註冊過的視窗型別)
HWND CreateMyWindow(LPCTSTR lpClassName, HINSTANCE hInstance)
{
	HWND hWnd = NULL;
	// 建立視窗
	hWnd = CreateWindow(lpClassName, "test", WS_OVERLAPPEDWINDOW^WS_THICKFRAME, 0, 0, 1000, 800, NULL, NULL, hInstance, NULL);
	return hWnd;
}
//顯示視窗
void DisplayMyWnd(HWND hWnd)
{
	//獲得螢幕尺寸

	int scrWidth = GetSystemMetrics(SM_CXSCREEN);
	int scrHeight = GetSystemMetrics(SM_CYSCREEN);
	RECT rect;
	GetWindowRect(hWnd, &rect);
	ShowWindow(hWnd, SW_SHOW);
	//重新設定rect裡的值
	rect.left = (scrWidth - rect.right) / 2;
	rect.top = (scrHeight - rect.bottom) / 2;
	//移動視窗到指定的位置
	SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW);
	UpdateWindow(hWnd);
}

void doMessage()        // 訊息迴圈處理函式
{
	MSG msg = { 0 };
	// 獲取訊息
	while (GetMessage(&msg, NULL, 0, 0)) // 當接收到WM_QIUT訊息時,GetMessage函式返回0,結束迴圈
	{
		DispatchMessage(&msg); // 派發訊息,到WindowPro函式處理
	}
}
//hook
HHOOK hook;
//hook回撥函式
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	//攔截空格訊息
	if (wParam == VK_SPACE)
		MessageBox(NULL, "空格被按下了", "鍵盤訊息被觸發了", 0);
	return CallNextHookEx(hook, nCode, wParam, lParam);
}
// 入口函式
int WINAPI WinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nShowCmd)
{
	HWND hWnd = NULL;
	LPCTSTR lpClassName = "MyWnd";  // 註冊視窗的名稱
	RegisterWindow(lpClassName, hInstance);
	hWnd = CreateMyWindow(lpClassName, hInstance);
	//註冊hook
	hook = SetWindowsHookEx(WH_KEYBOARD, LowLevelMouseProc, NULL, GetCurrentThreadId());
	DisplayMyWnd(hWnd);
	doMessage();
	return 0;
}

上面說過CallNextHookEx函式可以遮蔽訊息,不讓傳送到目標視窗,這裡來仔細解釋一下:

當鉤子被觸發時調入到回撥函式時,上面也說過,鉤子回撥函式會在訊息到達目標視窗之前獲取到此訊息,當我們處理完之後,到return時,如果不呼叫CallNextHookEx函式,直接返回return 0;那麼此訊息將永遠不會被髮送給目標視窗,這裡來解釋一下:

CallNextHookEx的第一個引數是要傳遞的hook控制程式碼,倘若沒有hook了,那麼此控制程式碼一定是當前hook子程的控制程式碼,如果是當前的而非下一個,此函式就會認為hook控制程式碼無效,直接將訊息再次傳遞給目標視窗,其整個過程是由核心態來完成的,當前控制程式碼是否有效可以通過鉤子連結串列來確定當前是哪個hook在執行!

下面我們修改一下程式碼,遮蔽指定視窗所有訊息:


//遮蔽所有訊息:
	return 0;//無需關心型別,LRESULT其定義是long型別
完整程式碼:

#include "stdafx.h"
#include <windows.h>

HWND hWnd;	//progman

//訊息函式
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	//判斷訊息ID
	switch (uMsg){
	
	case WM_DESTROY:    // 視窗銷燬訊息
		PostQuitMessage(0);   //  傳送退出訊息
		return 0;
	}
	// 其他的訊息呼叫預設的訊息處理程式
	return DefWindowProc(hwnd, uMsg, wParam, lParam);

}
// 3、註冊視窗型別
BOOL RegisterWindow(LPCSTR lpcWndName, HINSTANCE hInstance)
{
	ATOM nAtom = 0;
	// 構造建立視窗引數
	WNDCLASS wndClass = { 0 };
	wndClass.style = CS_HREDRAW | CS_VREDRAW;
	wndClass.lpfnWndProc = WindowProc;      // 指向視窗過程函式
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hInstance = hInstance;
	wndClass.hIcon = NULL;
	wndClass.hCursor = NULL;
	wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wndClass.lpszMenuName = NULL;
	wndClass.lpszClassName = lpcWndName;    // 註冊的視窗名稱,並非標題,以後建立視窗根據此註冊的名稱建立
	nAtom = RegisterClass(&wndClass);
	return TRUE;
}
//建立視窗(lpClassName 一定是已經註冊過的視窗型別)
HWND CreateMyWindow(LPCTSTR lpClassName, HINSTANCE hInstance)
{
	HWND hWnd = NULL;
	// 建立視窗
	hWnd = CreateWindow(lpClassName, "test", WS_OVERLAPPEDWINDOW^WS_THICKFRAME, 0, 0, 1000, 800, NULL, NULL, hInstance, NULL);
	return hWnd;
}
//顯示視窗
void DisplayMyWnd(HWND hWnd)
{
	//獲得螢幕尺寸

	int scrWidth = GetSystemMetrics(SM_CXSCREEN);
	int scrHeight = GetSystemMetrics(SM_CYSCREEN);
	RECT rect;
	GetWindowRect(hWnd, &rect);
	ShowWindow(hWnd, SW_SHOW);
	//重新設定rect裡的值
	rect.left = (scrWidth - rect.right) / 2;
	rect.top = (scrHeight - rect.bottom) / 2;
	//移動視窗到指定的位置
	SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW);
	UpdateWindow(hWnd);
}

void doMessage()        // 訊息迴圈處理函式
{
	MSG msg = { 0 };
	// 獲取訊息
	while (GetMessage(&msg, NULL, 0, 0)) // 當接收到WM_QIUT訊息時,GetMessage函式返回0,結束迴圈
	{
		DispatchMessage(&msg); // 派發訊息,到WindowPro函式處理
	}
}
//hook
HHOOK hook;
//hook回撥函式
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	//遮蔽所有訊息:
	return 0;
}
// 入口函式
int WINAPI WinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nShowCmd)
{
	HWND hWnd = NULL;
	LPCTSTR lpClassName = "MyWnd";  // 註冊視窗的名稱
	RegisterWindow(lpClassName, hInstance);
	hWnd = CreateMyWindow(lpClassName, hInstance);
	//註冊hook
	hook = SetWindowsHookEx(WH_KEYBOARD, LowLevelMouseProc, NULL, GetCurrentThreadId());
	DisplayMyWnd(hWnd);
	doMessage();
	return 0;
}

最後別忘記在不用的時候使用UnhookWindowsHookEx將其釋放掉

區域性鉤子說完了,那麼該說說全域性的了,全域性即無論任何視窗按下此訊息都有反應,全域性鉤子必須的回撥函式必須是一個DLL,因為虛擬記憶體的原因,詳細可以參考博主關於對虛擬記憶體和作業系統核心下虛擬保護的相關文章!

使用dll可以將程式碼注入到指定程式下,這樣就可以自由呼叫了:

這裡我們寫一個dll動態庫:


標頭檔案:

#pragma once
#include <windows.h>
extern "C" _declspec(dllexport) LRESULT LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam);
extern "C" _declspec(dllexport) void setHook(HHOOK hook);


dll檔案:

#include "hookdll.h"
#pragma data_seg("MyData")
HHOOK hook_ = 0;//共享資料段
#pragma data_seg()
LRESULT  LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam){
	//攔截所有的鍵盤訊息
	MessageBox(NULL, L"dd", L"鍵盤訊息被觸發了", 0);
	return CallNextHookEx(hook_, nCode, wParam, lParam);
}
void setHook(HHOOK hook){
	hook_ = hook;
}

然後我們要將dll檔案拷貝到我們的工程目錄下,並編寫匯入程式碼:

//dll
	HMODULE dll = LoadLibrary("ConsoleApplication3.dll");
	if (dll == NULL) {
		DWORD err = GetLastError();
		return -1;
	}
	HOOKPROC addr = (HOOKPROC)GetProcAddress(dll, "LowLevelMouseProc");
	typedef void(*FunPtr)(HHOOK hook);//定義函式指標來指向Dll動態庫裡的函式  
	FunPtr funPtr = (FunPtr)GetProcAddress(dll, "setHook");	//set hook func
	//註冊hook
	hook = SetWindowsHookEx(WH_KEYBOARD, addr, dll,0);
	funPtr(hook);	//set hook

注意SetWindowsHookEx最後一個引數更改為0,也就是全域性的,無論任何視窗觸發鍵盤訊息,那麼此dll裡的回撥函式都會被裝載進指定視窗程式下,並被執行!
執行結果:
我們在vs編輯介面下按下一個按鍵試試:


成功讓此dll載入進系統佇列中

完整程式碼:

#include "stdafx.h"
#include <windows.h>

HWND hWnd;	//progman

//訊息函式
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	//判斷訊息ID
	switch (uMsg){
	
	case WM_DESTROY:    // 視窗銷燬訊息
		PostQuitMessage(0);   //  傳送退出訊息
		return 0;
	}
	// 其他的訊息呼叫預設的訊息處理程式
	return DefWindowProc(hwnd, uMsg, wParam, lParam);

}
// 3、註冊視窗型別
BOOL RegisterWindow(LPCSTR lpcWndName, HINSTANCE hInstance)
{
	ATOM nAtom = 0;
	// 構造建立視窗引數
	WNDCLASS wndClass = { 0 };
	wndClass.style = CS_HREDRAW | CS_VREDRAW;
	wndClass.lpfnWndProc = WindowProc;      // 指向視窗過程函式
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hInstance = hInstance;
	wndClass.hIcon = NULL;
	wndClass.hCursor = NULL;
	wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wndClass.lpszMenuName = NULL;
	wndClass.lpszClassName = lpcWndName;    // 註冊的視窗名稱,並非標題,以後建立視窗根據此註冊的名稱建立
	nAtom = RegisterClass(&wndClass);
	return TRUE;
}
//建立視窗(lpClassName 一定是已經註冊過的視窗型別)
HWND CreateMyWindow(LPCTSTR lpClassName, HINSTANCE hInstance)
{
	HWND hWnd = NULL;
	// 建立視窗
	hWnd = CreateWindow(lpClassName, "test", WS_OVERLAPPEDWINDOW^WS_THICKFRAME, 0, 0, 1000, 800, NULL, NULL, hInstance, NULL);
	return hWnd;
}
//顯示視窗
void DisplayMyWnd(HWND hWnd)
{
	//獲得螢幕尺寸

	int scrWidth = GetSystemMetrics(SM_CXSCREEN);
	int scrHeight = GetSystemMetrics(SM_CYSCREEN);
	RECT rect;
	GetWindowRect(hWnd, &rect);
	ShowWindow(hWnd, SW_SHOW);
	//重新設定rect裡的值
	rect.left = (scrWidth - rect.right) / 2;
	rect.top = (scrHeight - rect.bottom) / 2;
	//移動視窗到指定的位置
	SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW);
	UpdateWindow(hWnd);
}

void doMessage()        // 訊息迴圈處理函式
{
	MSG msg = { 0 };
	// 獲取訊息
	while (GetMessage(&msg, NULL, 0, 0)) // 當接收到WM_QIUT訊息時,GetMessage函式返回0,結束迴圈
	{
		DispatchMessage(&msg); // 派發訊息,到WindowPro函式處理
	}
}
//hook
HHOOK hook;

// 入口函式
int WINAPI WinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nShowCmd)
{
	HWND hWnd = NULL;
	LPCTSTR lpClassName = "MyWnd";  // 註冊視窗的名稱
	RegisterWindow(lpClassName, hInstance);
	hWnd = CreateMyWindow(lpClassName, hInstance);
	//dll
	HMODULE dll = LoadLibrary("ConsoleApplication3.dll");
	if (dll == NULL) {
		DWORD err = GetLastError();
		return -1;
	}
	HOOKPROC addr = (HOOKPROC)GetProcAddress(dll, "LowLevelMouseProc");
	typedef void(*FunPtr)(HHOOK hook);//定義函式指標來指向Dll動態庫裡的函式  
	FunPtr funPtr = (FunPtr)GetProcAddress(dll, "setHook");	//set hook func
	//註冊hook
	hook = SetWindowsHookEx(WH_KEYBOARD, addr, dll,0);
	funPtr(hook);	//set hook  共享hook控制程式碼
	DisplayMyWnd(hWnd);
	doMessage();
	return 0;
}

dll:
.h
#pragma once
#include <windows.h>
extern "C" _declspec(dllexport) LRESULT LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam);
extern "C" _declspec(dllexport) void setHook(HHOOK hook);
.cpp
#include "hookdll.h"
#pragma data_seg("MyData")
HHOOK hook_ = 0;//共享資料段
#pragma data_seg()
LRESULT  LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam){
	//攔截所有的鍵盤訊息
	MessageBox(NULL, L"dd", L"鍵盤訊息被觸發了", 0);
	return CallNextHookEx(hook_, nCode, wParam, lParam);
}
void setHook(HHOOK hook){
	hook_ = hook;
}

注意這裡回撥函式宣告沒有包含CALLBACK修飾符了,因為在.h檔案裡已經宣告此函式的入棧方式,所以重複宣告會被編譯器視為無效,那麼在載入時就會出現找不到指定dll函式,CALLBACK是入棧方式修飾符,詳細可以檢視博主對入棧方式方面介紹的部落格!

那麼你也可以通過此方法來入侵其他程式!把SetWindowsHookEx最後一個引數改成指定視窗程式下的執行緒id即可!

那麼下面來做一個個小示列:

這裡博主已經事先寫好一個簡單的Windows視窗了:


那麼我們通過FindWindow獲取視窗控制程式碼,在通過控制程式碼獲取視窗執行執行緒,在通過設定Hook來監視訊息!



HWND hwnd = FindWindow(NULL, "1");
DWORD ProcessID;	//程式ID
DWORD ThreadID;	//執行緒ID
ThreadID = GetWindowThreadProcessId(hwnd, &ProcessID);	//獲取視窗的程式ID和執行緒ID
hook = SetWindowsHookEx(WH_KEYBOARD, addr, dll, ThreadID);

執行效果:


可以看到成功觸發訊息!

注意如果你想要攔截某個控制元件的訊息,上面說過了,控制元件也是一個獨立的執行緒,有自己的訊息佇列,並且也是一個視窗,只是一個需要容器的視窗,所以可以通過FindWindowEx來獲取子視窗找到指定控制元件,並攔截!

這裡在補充一點,經過博主的測試:比如你向一個edit控制元件輸入字元,那麼容器視窗也會獲取到鍵盤訊息,edit也會獲取到此訊息,這兩個執行緒的訊息佇列都會獲取到此訊息,因為你在容器視窗下對任何控制元件進行操作,容器視窗都會獲取到此訊息,控制元件執行緒訊息佇列也會獲取到此訊息。並且雙方是互不干擾的,你攔截容器視窗的這個訊息遮蔽,控制元件還是一樣能收到,所以我們要遮蔽控制元件訊息才行!


下面我們使用此方法來監視一個視窗是否被啟用:

注意使用WH_CBT訊息來監視:

在開始之前先介紹一下幾個基於WH_CBT訊息的幾個訊息:

HCBT_CREATEWND:視窗建立訊息,當一個執行緒呼叫createwindow建立視窗完成之後,windows會向監視WM_CBT訊息的鉤子傳送此訊息,並且wParam裡面包含了視窗控制程式碼,要用(HWND)的形式強轉得到!

HC_ACTION:鍵盤訊息,即當視窗被觸發WM_CBT訊息時帶有鍵盤訊息!

HCBT_ACTIVATE:視窗被啟用訊息,與HCBT_CREATEWND一樣,在視窗獲取啟用訊息之前,此訊息會被轉發到鉤子訊息裡!

這些都是鉤子內部定義的WM_CBT訊息!

其餘的最大化,最小化均可以使用Windows內部定義的訊息!

這裡我們只使用HCBT_ACTIVATE來監視視窗是否被啟用!

HHOOK hook = SetWindowsHookEx(WH_CBT, addr, dll, ThreadID);
	BOOL bol = (hook != NULL);	//邏輯表示式
	if (bol == true){
		MessageBox(NULL, NULL, "hook ok!", 0);
	}
	funPtr();	//set hook  共享hook控制程式碼
dll:
LRESULT  LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam){
	     switch (nCode)
		     {
		         case HCBT_ACTIVATE:
			         {
									   HDC dc = GetDC(NULL);
									   TextOut(dc, 20, 20, L"JH", 2);
				         }
			       default:
				             break;
			     }

	return CallNextHookEx(hook_, nCode, wParam, lParam);
}

這裡肯定有人會問了,為什麼使用dc而不是massgebox

這裡要說一點:如果你使用一切帶有user32.dll動態庫的api,那麼就會造成訊息佇列堵塞,因為massgebox是一個對話方塊在建立時會觸發HCBT_CREATEWNDHCBT_ACTIVATE等訊息,並且massgebox api還會阻塞函式,從而導致訊息佇列堵塞,那麼出現這種情況windows會出現異常問題,但是當你在鉤一些鍵盤訊息時使用這個無所謂,因為這些訊息並不是特別重要的訊息,不會導致視窗出現異常,但是當你使用一些視窗重要訊息時,比如create或啟用,一旦阻塞則無法建立視窗或啟用顯示視窗,就會導致視窗異常,但是我們可以攔截此訊息,不讓它轉發給視窗,但是不能導致訊息佇列阻塞,否則windows會直接刪除此視窗!

比如:

當我在監視一個執行緒的建立訊息:HCBT_CREATEWND

那麼此執行緒開始建立一個視窗了,觸發了此訊息,但是我們內部想要在建立時在額外在此執行緒建立的視窗上附加一個按鈕,於是我們呼叫了:

CreateWindowEx函式來建立一個視窗,CreateWindowEx API MSDN給出的解釋是建立出來的視窗歸呼叫者執行緒所有,那麼此時,我們是在HCBT_CREATEWND裡建立的,於是CreateWindowEx函式在建立完成之後又觸發了HCBT_CREATEWND訊息,於是我們又重複的去建立了這個視窗,於是訊息佇列堵塞了,重複這個訊息,所以出現了訊息佇列堵塞,迴圈建立,導致記憶體爆滿,直接被Windows給卡嚓掉了!

那麼為什麼GetDc裡的dc不是視窗控制程式碼而是NULL

答:如果使用hWnd還是會被咔嚓掉,經過本人多次測試驗證得出:GetDc會呼叫視窗的user32.dll,來獲取視窗裡的繪圖DC,還是會導致產生一些讓訊息佇列堵塞的訊息!

最後得出的結論是使用WM_CBT訊息不能呼叫任何會呼叫user32.dllapi,否則就會直接被windows給殺掉,可能在觸發此關於視窗建立最小化或最大化的訊息時,視窗已經被刪除了,然後要重新繪製整個視窗區域,所以導致呼叫任何窗體訊息時會崩潰,並且本人在查了很多相關文件,大致說的都是不要在WM_CBT獲取到的訊息裡呼叫任何可能阻塞訊息佇列的API,也就是說我們只能用來做一些相關的初始化,或者攔截程式不被建立之類的!

攔截程式不被建立非常簡單:

SetWindowsHookEx(WM_CBT, addr, dll, 0);


0即為全域性系統鉤子

這樣我們鉤住全域性的系統鉤子產生任何訊息我們都會率先攔截,如果攔截到HCBT_CREATEWND訊息可以直接return,不讓其轉發,這樣這個視窗就不會被建立!

注意這裡要重點說一下WM_CREATE函式,此函式是在CreateWindowEx函式被請求呼叫時觸發的,注意是請求呼叫,也就是說還沒有被呼叫,如果我們攔截,那麼這個視窗永遠不會被建立!


最後在說一下,不同位的dll不能被附加到不同的位的程式上,因為有很多不可避免的因素在裡面:

不同位的dll使用的位元組佔用不同,如果執行在32位機器上那麼就無法正常執行,因為定址原因!

所以windows可以從pe標頭檔案裡找出是多少位的並加以區分!





相關文章