程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 如何獲取其它進程中窗口的窗口過程

如何獲取其它進程中窗口的窗口過程

編輯:關於VC++

開發環境: 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;
  }
}

進行一些善後工作,關閉打開的線程句柄、進程句柄,釋放分配的遠程內存;

具體細節請參考示例代碼。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved