研究如何攔截其他應用程式訊息,最直觀的練習目標就是常用的 MSN Messenger。雖無實際商業價值,但能完整體驗DLL 注入、全域鉤子、跨程序記憶體操作的核心流程。本文記錄完整實作過程。
兩個進程擁有獨立位址空間,無法直接存取彼此記憶體。SetWindowsHookEx 配合 DLL 實現注入:
SetWindowsHookEx(WH_MOUSE, GetMsgProc, hDll, dwThreadId)。GetMsgProc 回呼(在目標程序記憶體中執行)。#pragma data_seg("Shared")
HHOOK g_hhook = NULL;
DWORD g_dwThreadIdMsn = 0;
POINT g_MouseLoc = {0,0};
char g_text[256] = {0};
HWND g_Hwnd = NULL;
#pragma data_seg()
#pragma comment(linker, "/section:Shared,rws")
HINSTANCE g_hinstDll = NULL;
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason, LPVOID lpReserved)
{
if (ul_reason == DLL_PROCESS_ATTACH) g_hinstDll = (HINSTANCE)hModule;
return TRUE;
}
// 安裝鉤子
__declspec(dllexport) BOOL WINAPI SetMsnHook(DWORD dwThreadId)
{
if (dwThreadId == 0) return FALSE;
g_dwThreadIdMsn = GetCurrentThreadId();
g_hhook = SetWindowsHookEx(WH_MOUSE, GetMsgProc, g_hinstDll, dwThreadId);
if (g_hhook) PostThreadMessage(dwThreadId, WM_NULL, 0, 0); // 激活訊息泵
return (g_hhook != NULL);
}
// 移除鉤子
__declspec(dllexport) BOOL WINAPI UnsetMsnHook()
{
if (g_hhook) return UnhookWindowsHookEx(g_hhook);
return FALSE;
}
// 鉤子處理函數
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION) {
MOUSEHOOKSTRUCT* pMouse = (MOUSEHOOKSTRUCT*)lParam;
g_MouseLoc = pMouse->pt;
HWND hWnd = WindowFromPoint(pMouse->pt);
if (hWnd) {
SendMessage(hWnd, WM_GETTEXT, 256, (LPARAM)g_text);
g_Hwnd = hWnd;
// 示範:將文字改為固定內容
SendMessage(hWnd, WM_SETTEXT, 0, (LPARAM)"Hooked!");
}
}
return CallNextHookEx(g_hhook, nCode, wParam, lParam);
}
// 供 EXE 讀取共享資料
__declspec(dllexport) void WINAPI GetText(int& x, int& y, char** ptext)
{
x = g_MouseLoc.x; y = g_MouseLoc.y; *ptext = g_text;
}
__declspec(dllexport) HWND WINAPI GetMyHwnd() { return g_Hwnd; }
#include "MSNHook.h"
#pragma comment(lib, "MSNHook.lib")
// 對話框成員
CEdit m_Edit;
CButton m_btnStart, m_btnStop;
void CTestMSNHookDlg::OnBnClickedStart()
{
// 透過 Spy++ 取得 MSN 聊天視窗類別名
CWnd* pMsnWin = FindWindow(_T("IMWindowClass"), NULL);
if (!pMsnWin) { AfxMessageBox(_T("找不到 MSN 視窗")); return; }
DWORD dwThreadId = GetWindowThreadProcessId(pMsnWin->GetSafeHwnd(), NULL);
if (SetMsnHook(dwThreadId)) {
SetTimer(101, 100, NULL); // 定時輪詢共享資料
m_btnStart.EnableWindow(FALSE);
m_btnStop.EnableWindow(TRUE);
} else {
AfxMessageBox(_T("Hook 安裝失敗"));
}
}
void CTestMSNHookDlg::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == 101) {
char* pText = NULL;
int x = 0, y = 0;
GetText(x, y, &pText);
if (x || y) {
CString str;
str.Format(_T("滑鼠位置: %d, %d\n文字: %s"), x, y, pText);
m_Edit.SetWindowText(str);
}
HWND hWnd = GetMyHwnd();
if (hWnd) {
CWnd* pWnd = CWnd::FromHandle(hWnd);
CString wndText;
pWnd->GetWindowText(wndText);
// 可顯示視窗標題等資訊
}
}
CDialog::OnTimer(nIDEvent);
}
void CTestMSNHookDlg::OnBnClickedStop()
{
KillTimer(101);
UnsetMsnHook();
m_btnStart.EnableWindow(TRUE);
m_btnStop.EnableWindow(FALSE);
}
| 技術點 | 說明 |
|---|---|
| WH_MOUSE | 攔截滑鼠訊息,MOUSEHOOKSTRUCT 含座標與視窗句柄 |
| 共享資料段 | #pragma data_seg + /section:Shared,rws 實現跨程序變數共享 |
| 隱式連結 DLL | __declspec(dllimport) + #pragma comment(lib) 簡化呼叫 |
| PostThreadMessage(WM_NULL) | 確保目標執行緒有訊息泵,讓 Hook 生效 |
| WindowFromPoint | 從螢標座標取得視窗句柄,進而 WM_GETTEXT 取得文字 |
作者嘗試用 WM_GETTEXT 取得 MSN 聊天輸入框文字但失敗,推測 MSN 使用自繪控件或內部儲存機制不同。後續可嘗試:
WM_CHAR/WM_KEYDOWN 攔截鍵盤輸入。本範例展示了最經典的「DLL 注入 + 全域鉤子」技術棧,是理解 Windows 進程間攔截機制的最佳入門專案。實務應用中請遵守法律法規與用戶隱私,僅用於合法除錯、測試、輔助工具開發。