2010年2月26日 星期五

使用 NetBeans CND 來做 Microsoft Windows Message Hook

最近,福克斯的同事需要設計一個 Windows 的 Message Hook。但是,福克斯及同事們都是採用 Linux + NetBeans 的開發環境,而且又不大熟悉 Microsoft Visual C++ 的開發方式。因此,福克斯只好研究如何使用 NetBeans 的 CND 來開發 Microsoft Windows Message Hook。

1. Message Hook

Windows 的 Message Hook 是用來攔截 Windows 所有事件的方式。這個功能,可以用來設計病毒,或是惡搞別的應用程式。所以,在設計的時候,必須非常小心,不然會需要重新開機很多次。

開發 Message Hook 的程式時,必須要注意到: Windows 的 program loader 會將 message hook 的程式(它會是 dynamic linking library)連結到正在執行的 process,並呼叫 message hook 的 callback。正因為 callback 必須連結到正在執行的 process,它一定要是 dynamic linking library。所以,這次的程式會包含:a. message hook callback library, b. 專門接收事件的應用程式。

福克斯知道,這件事與 Linux 的 shared library 運作方式不同。這是因為, Windows 是採用 message queue 的運作方式來傳輸事件(在 Windows 叫 message)。

2. Project 的設計

這次的開發,其實與傳統的 NetBeans 開發 Windows 應用程式非常相似,建立一個執行檔專案外加一個 dynamic linking library (DLL) 專案、及一個應用程式的專案。另外,為了方便起見,福克斯決定不使用 SendMessage 的方式將資料轉輸到另一個 process 之中。而是使用 shared memory 的方式來做資料交換。

3. NetBeans Project 的設定

Project 的設定大致上與傳統 NetBeans CND 的方式一樣,只是在 DLL 的部份,必須要選擇 Application,並於 linker 的 additional options 輸入 -mdll 用來產生 DLL。這個設定如下:


4. 程式內容

程式碼的解說,福克斯僅以程式為主,一般的宣告請參考原始碼。

4.a DLL - Library Entrypoint

Windows 的 library 可以有自己的 entrypoint,它就像 Linux 的 so 可以有 OnLoad 類似。但是,不同的地方是,這個 entrypoint 會在 DLL 被連結到 process 及 thread 時候被呼叫到。這邊要特別注意是: Message Hook 的 library 會被連結到多個 process 之中,所以這個 function 會被連結到很多個 process 之中。所以,這個 function 需要建立一個 cross process 的共享記憶體,並將不同 process 的 message 的資料放到這個共享記憶體之中。
/**
* the entry point of this dll
* */

BOOL WINAPI DllMain(HINSTANCE hInst, ULONG uReason, LPVOID lpReserved) {
switch (uReason) {
//create or link to the shared memory
case DLL_PROCESS_ATTACH:
{
//create the shared memory
g_hMappedHandle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE, 0, sizeof (GlobalSharedData),
"NetBeans_MessageHookSharedMemory");
//link the shared memory to g_pData which belongs to the current process.
g_pData = (GlobalSharedData*) MapViewOfFile(g_hMappedHandle,
FILE_MAP_WRITE, 0, 0, 0);
g_bReady = (NULL != g_hMappedHandle) && (NULL != g_pData) &&
(GetLastError() != ERROR_ALREADY_EXISTS);

if (g_bReady) {
//initialize the global shared data
g_pData->instance = hInst;
g_pData->hook = NULL;
g_pData->eventCount = 0;
}
//we don't care the thread switch, disable it.
DisableThreadLibraryCalls(hInst);
break;
}
case DLL_PROCESS_DETACH:
{
//remove the shared memory link.
CloseHandle(g_hMappedHandle);
break;
}
}

return TRUE;
}
4.b DLL - APIs

這個部份比較單純,就是提供一組 API 讓另一個專案呼叫。這邊較特別的是,HHOOK 必需被儲存到共享的記憶體之中,因為它之後會被呼叫到。

/**
* to remove the message hook
* */

BOOL StopHook() {
//remove the windows hook
BOOL bResult = UnhookWindowsHookEx(g_pData->hook);
//clear the data
g_pData->hook = NULL;
g_pData->instance = NULL;
return bResult;
}

/**
* to get the record message
* */

void GetMessageString(char* buf) {
//copy the strBuffer to cb
strcpy(buf, g_pData->strBuffer);
}

/**
* to hook the message hook
* */

BOOL StartHook() {
if (g_bReady) {
//hook the windows hook
g_pData->hook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC) HookProc,
g_pData->instance, 0);
//if null is returned, it fails.
return NULL != g_pData->hook;
} else {
//no shared memory linked, we don't need to do anything.
return false;
}
}

/**
* the total event count
* */

int GetEventCount() {
return g_pData->eventCount;
}
4.c DLL - Message Hook Callback

因為這邊福克斯是以 Keyboard callback 當成範例,所以下面是將 keyboard message 中的資料轉換成字串,然後放到暫存的字串空間之中,並將 count 加 1 ,以等待另一個專案的程式來將暫存的字串拿走。這邊比較特別的地方是,這個 function 會被不同 process 呼叫到,所以GetModulebaseName() 不一定是 HookMain,而是正在接收 message 的 process name。

/**
* the hook proc for receiving all keyboard events
* */
LRESULT HookProc(int nCode, WPARAM wParam, LPARAM lParam) {

//get process name
char strProcName[_MAX_FNAME];
GetModuleBaseName(GetCurrentProcess(), NULL, strProcName, sizeof (strProcName));

//put data to buffer
sprintf(g_pData->strBuffer, "data #%d, %s, lParam: %u, wParam: %u, code: %d\n",
(unsigned int) g_pData->eventCount, strProcName, lParam, wParam, nCode);
g_pData->eventCount++;
//we always call next hook to process this message
return CallNextHookEx(g_pData->hook, nCode, wParam, lParam);
}
嚴格來說,這樣的設計是一個錯誤的設計。因為,兩個 process 在執行的時候,它們執行的先後順序是無法控制。所以,正確的作法會是透過 send message 的方式讓其它的 process 收到資料。

福克斯會設計這個範例的最主要原因是,先前福克斯的同事們大多僅以設計 single process 的程式,而較少 inter-process communication 的部份,以共享記憶體的方式,較容易被人所接受。當這部份的原理了解後,再去使用 send message 將會較為容易了(呵,其實正確的說法是,會寫 Windows 程式的人一定會用 send message,但不一定會用 inter-process 的共享記憶體)。

4.d HookMain - WinMain

WinMain 是 Windows 主程式的進入點,它跟 main function 是一樣的。所以它的寫法與傳統的方式完全相同。

這邊的程式如 4.c 所提到,它只是個錯誤範例。它會每隔 1ms 去檢查 event count 是否有變化,如果有變化就把文字列到 console 之中。

/**
* the main function in windows
* */

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {

//the buffer for message text
char tempBuffer[256] = "Empty";
//create the message hook
StartHook();

int nCurrentMessageCount = 0;
int nLastMessageCount = 0;

DWORD nStartTickCount = GetTickCount();
DWORD nDiff = 0;
//5 seconds
while (nDiff < 5000) {
//get the message count
nCurrentMessageCount = GetEventCount();
if (nCurrentMessageCount != nLastMessageCount) {
//copy and print the message from the dll
GetMessageString(tempBuffer);
printf("%s", tempBuffer);
nLastMessageCount = nCurrentMessageCount;
} else {
Sleep(1);
}
nDiff = GetTickCount() - nStartTickCount;
}
//print the total message count
printf("Event Count: %d\n", nLastMessageCount);
//remove the message hook.
StopHook();
return 0;
}
5. 執行畫面與程式

完整的執行畫面如下:

程式的原始碼可於此處下載