開發環境: Windows XP + VC6+Platform SDK 或者 VS.NET 2003+
測試環境: Windows XP
曾經以為獲取一個窗口的窗口過程很簡單,不就是GetWindowLong一下嗎,看spyxx獲取的多麼順利。後來才發現原來不是這麼簡單。獲取本進程內窗口的窗口過程確實很簡單,直接調用GetWindowLong(hWnd,GWL_WNDPROC)就可以了(注意,根據窗口是否是Unicode的,你要判斷是調用GetWindowLongA,還是GetWindowLongW,可以用IsWindowUnicode來判斷), 但是GetWindowLong這個函數內部會檢查調用進程和該窗口句柄是否屬於同一進程,如果不是,就簡單的返回0了。
既然這樣,我們又不能去修改GetWindowLong,那就只有想辦法讓它認為我們和那個窗口是在一個進程裡了。回想起Windows核心編程裡講過,通過創建遠程線程的方式,可以在其它進程內創建一個新的線程,並且可以指定這個線程的線程函數。因為Windows的進程之間的地址相互之間是不可見的,所以我們不能指定本地的線程函數,而要在遠程分配內存,把我們要做的事情以機器碼的形式寫進去。
(spyxx是怎麼做的呢?它啟動時安裝了一個全局的鉤子WH_GETMESSAGE,這樣,幾乎所有有消息循環的程序都會加載它的hook dll,從而可以在其它進程的地址空間裡調用GetWindowLong,如果我們僅僅為了獲取一下窗口過程裝一個全局鉤子,未免有點兒太浪費了:))
先來分析一下我們的線程函數都需要做什麼。
首先,需要調用GetWindowLong獲取這個窗口的窗口過程,然後要告訴我們。可以用PostMessage或者PostThreadMessage的方式通知我們的程序。具體如下所示:
//hWndTarget是我們要獲取其窗口過程的窗口句柄, 假設 hWndTarget = 0x12345678
//dwThreadId是我們的線程Id ,假設 dwThreadId = 0x5678
LONG wndProc = GetWindowLong(hWndTarget,GWL_WNDPROC);
PostThreadMessage(dwThreadId,WM_MYMESSAGE,(WPARAM)hWndTarget,(LPARAM)wndProc);
因為這個時候函數的參數我們都已經知道了,所以可以直接硬編碼到程序裡。先看一下GetWindowLongA的函數原型:
WINUSERAPI LONG WINAPI GetWindowLongA( HWND hWnd, int nIndex)
一共有兩個參數,我們的 GetWindowLongA(hWnd,GWL_WNDPROC)函數調用的匯編代碼大概就是這個樣子(右邊是響應的機器碼):
//參數入棧的順序是從右向左,所以先push nIndex,然後是push hWnd
push 0xFC //6A FC //GWL_WNDPROC的值是-4,寫成16進制就是0xFC
push hWndTarget //58 78 56 34 12 //假設 hWndTarget = 0x12345678
call GetWindowLongA //E8 (GetWindowLongA-下一條指令的地址)
GetWindowLongA函數的返回值在eax寄存器裡,我們的PostThreadMessageA函數裡用wParam參數發送該窗口句柄,用lParam參數發送該窗口的窗口過程 ,所以push lParam 就是 push eax
再來看看PostThreadMessageA的函數原型:
WINUSERAPI BOOL WINAPI PostThreadMessageA( DWORD idThread, UINT Msg, WPARAM wParam, LPARAM lParam)
PostThreadMessageA函數調用的匯編代碼如下:
//Msg是我們自定義的消息,假設 Msg = 0x1234
push lParam //push eax //50
push wParam //push hWndTarget //58 78 56 34 12 //假設 hWndTarget = 0x12345678
push Msg //58 34 12 00 00 //假設 Msg = 0x1234
push dwThreadId //58 78 56 00 00 //假設 dwThreadId = 0x5678
call PostThreadMessageA //E8 (PostThreadMessageA-下一條指令的地址)
解釋一下call PostThreadMessageA指令:
第一個機器碼是E8,後邊跟的不是PostThreadMessageA這個函數的地址,而是從當前指令的下一條指令的地址到那個函數的距離。例如,當前指令的地址是 0x2d0028,call指令長度為5個字節,那麼下一條指令的地址就是0x2d0028 + 5 = 0x2d0033,假如 PostThreadMessageA函數的起始地址是0x77d3ebb0,那麼我們call後邊的數就是 0x77d3ebb0 - 0x2d0033 = 77A6EB7D,那麼 call PostThreadMessageA的實際的機器碼就是 E8 7D EB A6 77
0x002d0012 push eax //50 1 byte
0x002d0013 push hWndTarget //58 78 56 34 12 5 bytes
0x002d0018 push Msg //58 34 12 00 00 5 bytes
0x002d0023 push dwThreadId //58 78 56 00 00 5 bytes
0x002d0028 call PostThreadMessageA //E8 7D EB A6 77 5 bytes
0x002d0033 ret //C3 1 bytes
0x002d0034 xxxx
有了以上的准備工作,下邊就可以正式進行工作了。假如給定了要獲取 hWndTarget 窗口的窗口過程
調用 GetWindowThreadProcessId ,得到該窗口所屬進程的Id,存放在 dwProcess中;
調用 OpenProcess,打開該進程(如果打開失敗,可能是權限不夠,需要調用AdjustTokenPrivileges提升一下當前進程的權限),得到一個該進程的句柄,存放在hProcess中 ;
調用 IsWindowUnicode,判斷下一步應該調用 GetWindowLongA 還是 GetWindowLongW函數;
調用 VirtualAllocEx,在目標進程中分配一些內存,供我們寫入線程函數使用。函數返回的就是分配的內存的起始地址,就是我們的線程函數的起始地址,假設叫fnStartAddr;根據我們上邊分析的結果,需要33個字節,另外,線程函數最後要有一個 ret指令,占用一個字節,共需34個字節;
把以上分析的結果寫入一個臨時的緩沖區裡;
調用 WriteProcessMemory,把剛才的結果寫入遠程進程 fnStartAddr的地址處 ;
調用 CreateRemoteThread,指定線程函數地址為 fnStartAddr;
進行一個小的消息循環,等待我們的返回結果;
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
if(msg.message == uMsgSendBack)
{
procRet = msg.lParam;
break;
}
}
進行一些善後工作,關閉打開的線程句柄、進程句柄,釋放分配的遠程內存;
具體細節請參考示例代碼。
本文配套源碼