最近工作中遇到一個問題,需要通過程序界面進行判斷程序的運行狀態,剛開始認為很簡單,不就是一個窗體控件獲取,獲取Button的狀態和Text。剛好去年干過該事情,就沒太在意,就把優先級排到后面了,隨着項目交付時間的臨近,就准備開始解決問題,一下懵逼了,這次軟件作者也聰明了,居然換了花樣。
從圖中可以發現,此處的按鈕上的文字等信息不是通過Button信息進行操作。靜想猜測是通過Windows API直接將信息進行寫入,那么哪個API可以進行寫入呢,想想不難發現,其實就是窗口程序中在窗口中寫入文字的方法DrawText,同時隱約記得SetWindowsText也具備該功能。既然有猜想了那就實踐看看,驗證下猜想。
開干之前,還需要理理思路:
- 既然程序會調用DrawText方法,那我要獲取文本,就必須截獲到該方法
- 截獲方法不就是使用Windows上大名鼎鼎的鈎子(Hook)函數。
- 說到鈎子函數,這又分鈎窗口消息和鈎API函數。對於本文來說當然就是后者了。實現API HOOK主要有兩個重要環節:
- 如何把代碼注入到目標地址空間
- 如何讓自己的代碼被調用
- 稍稍查詢下資料,發現鈎窗口函數貌似就復雜了,如果要研究細節請參考該文
- 進一步資料查詢,我發現以牛逼函數庫(居然還是微軟自己開發的):Detours(當然我會提供下載鏈接)Detours它用於實現攔截Win32二進制代碼中的API函數。它使用一個JMP指令替換了目標函數的前面幾個字節,使得控制直接調用實現的Detours函數。並通過一個trampoline函數保留了原來函數的功能調用。
- 到目前為止就是對該庫的使用了。從文檔上來看應該沒什么問題了。但是文檔上是通過創建進程進行注入DetourCreateProcessWithDll,對目前我的應用場景就不太匹配了。客戶程序一直處於運行狀態,我需要獲取動態注入,那么問題來了,如何解決了?
BOOL WINAPI DetourCreateProcessWithDll(LPCSTR lpApplicationName,
LPSTR lpCommandLine,
...);
- 能提出問題,基本上問題就解決了一般。果不然,通過百度和Google的不懈努力,終於發現原來Detours1.5版本中有個方法DetourContinueProcessWithDll該方法就是進行動態注入的,這下就可以開干了(至於Detours的原理本文就不再贅述,請大家自行查詢資料,本文以解決實際問題問題主)
BOOL WINAPI DetourContinueProcessWithDll(HANDLE hProcess, LPCSTR lpDllName);
經過上面幾個步驟下來,已經有了完整的思路,下文主要結合實踐,進行代碼實踐。本文主要從如下幾個方面進行時間:
- 首先,居然是要截取函數DrawText和SetWindowText那么首先的先實現自己的函數(通過DLL封裝)
- 然后,就是動態注入我們的DLL文件指定進程
- 最后,拿出來溜溜(本文為了簡便,僅將相關信息打印到DebugView中)
實現HookWindowTextDll
首先,按照Detours的編程規范,需要在加載HookWindowTextDll時通過方法DetourFunctionWithTrampoline進行注冊。在卸載的時候通過DetourRemove卸載。
該處主要分以下幾步:
- 需要Hook的方法聲明和實現
- 安裝和卸載注入方法
Hook方法聲明和實現
首先需要聲明我們的方法
BOOL WINAPI MySetWindowTextA( HWND hWnd, LPCTSTR lpString );
BOOL WINAPI MySetWindowTextW( HWND hWnd, LPCWSTR lpString );
int WINAPI MyDrawTextA( HDC hDC, LPCTSTR lpString, int nCount, LPRECT lpRect, UINT uFormat );
int WINAPI MyDrawTextW( HDC hDC, LPCWSTR lpString, int nCount, LPRECT lpRect, UINT uFormat );
//該方法主要是用Real_SetWindowTextA保存原來函數SetWindowTextA地址,方便后面調用
DETOUR_TRAMPOLINE( BOOL WINAPI Real_SetWindowTextA( HWND a0, LPCTSTR a1 ), SetWindowTextA );
DETOUR_TRAMPOLINE( BOOL WINAPI Real_SetWindowTextW( HWND a0, LPCWSTR a1 ), SetWindowTextW );
DETOUR_TRAMPOLINE( int WINAPI Real_DrawTextA( HDC a0, LPCTSTR a1, int a2, LPRECT a3, UINT a4 ), DrawTextA );
DETOUR_TRAMPOLINE( int WINAPI Real_DrawTextW( HDC a0, LPCWSTR a1, int a2, LPRECT a3, UINT a4 ), DrawTextW );
//代碼實現,限於篇幅,僅列出MySetWindowTextA
BOOL WINAPI MySetWindowTextA( HWND hWnd, LPCTSTR lpString )
{
#ifdef _DEBUG
char strMsg[ 1024 ]={0};
wsprintf( strMsg, "SetWindowTextA : %s. size = %ld\n", lpString, strlen(lpString) );
OutputDebugString( strMsg );
#endif
return Real_SetWindowTextA( hWnd, lpString );
}
Hook方法的安裝和卸載
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( DLL_PROCESS_ATTACH == ul_reason_for_call )//dll加載
{
InstallProbes();
}
else if( DLL_PROCESS_DETACH == ul_reason_for_call )//dll卸載
{
UninstallProbes();
}
else;
return TRUE;
}
攔截注入通過方法DetourFunctionWithTrampoline進行,該方法原型如下:
BOOL WINAPI DetourFunctionWithTrampoline(PBYTE pbTrampoline,
PBYTE pbDetour);
這個函數有兩個參數,pbTrampoline和一個指向pbDetour函數的指針。目標函數Target之所以沒有作
為一個參數,是因為它已經編碼到pbTrampoline函數之中(上文中進行編碼DETOUR_TRAMPOLINE)。
BOOL InstallProbes()
{
DetourFunctionWithTrampoline( (PBYTE)Real_SetWindowTextA, (PBYTE)MySetWindowTextA );
DetourFunctionWithTrampoline( (PBYTE)Real_SetWindowTextW, (PBYTE)MySetWindowTextW );
DetourFunctionWithTrampoline( (PBYTE)Real_DrawTextA, (PBYTE)MyDrawTextA );
DetourFunctionWithTrampoline( (PBYTE)Real_DrawTextW, (PBYTE)MyDrawTextW );
OutputDebugString("InstallProbesA ok.\n");
return TRUE;
}
BOOL UninstallProbes()
{
DetourRemove( (PBYTE)Real_SetWindowTextA, (PBYTE)MySetWindowTextA );
DetourRemove( (PBYTE)Real_SetWindowTextW, (PBYTE)MySetWindowTextW );
OutputDebugString("UNInstallProbesB ok.\n");
return TRUE;
}
至此攔截注入的方法就完成。下一小節就是如何進行動態注入了。
動態注入HookWindowTextDll
既然要動態注入,那么就先要看看動態注入方法DetourContinueProcessWithDll方法的使用方法
//把一個動態鏈接庫注入到一個新的進程中
BOOL WINAPI DetourContinueProcessWithDllA(HANDLE hProcess, LPCSTR lpDllName)
該方法有兩個參數,一看看就明白了
- hProcess:需要注入的原進程句柄
- lpDllName:需要注入的Dll路徑,本文即HookWindowTextDll.dll
那么此時應該先獲取進程句柄,獲取進程句柄通過如下方法即可:
通過進程名稱獲取進程ID
DWORD GetProcessIdFromProcessName(std::string processname)
{
DWORD dwRet = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap != INVALID_HANDLE_VALUE)
{
BOOL bMore = ::Process32First(hProcessSnap, &pe32);
while (bMore)
{
if (boost::iequals(pe32.szExeFile, processname))
{
dwRet = pe32.th32ProcessID;
break;
}
bMore = ::Process32Next(hProcessSnap, &pe32);
}
::CloseHandle(hProcessSnap);
}
return dwRet;
}
調用測試:
std::string str1 = "WireCut.EXE";
DWORD dwProcessId = GetProcessIdFromProcessName(str1);
std::cout << dwProcessId << std::endl; //
獲取到了進程,那就進入下一節,獲取句柄。
通過進程ID獲取進程句柄
OpenProcess 函數用來打開一個已存在的進程對象,並返回進程的句柄
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
參數:
- dwDesiredAccess:想擁有的該進程訪問權限
- PROCESS_ALL_ACCESS:所有能獲得的權限
- PROCESS_CREATE_PROCESS:需要創建一個進程
- PROCESS_CREATE_THREAD:需要創建一個線程
- PROCESS_DUP_HANDLE:重復使用DuplicateHandle句柄
- PROCESS_QUERY_INFORMATION:獲得進程信息的權限,如它的退出代碼、優先級
- PROCESS_SET_INFORMATION :設置某些信息的權限,如進程優先級
- PROCESS_SET_QUOTA :設置內存限制的權限,使用SetProcessWorkingSetSize
- PROCESS_SUSPEND_RESUME :暫停或恢復進程的權限
- PROCES_TERMINATE :終止一個進程的權限,使用TerminateProcess
- PROCESS_VM_OPERATION :操作進程內存空間的權限(可用VirtualProtectEx和WriteProcessMemory)
- PROCESS_VM_READ :讀取進程內存空間的權限,可使用ReadProcessMemory
- PROCESS_VM_WRITE :讀取進程內存空間的權限,可使用WriteProcessMemory
- SYNCHRONIZE :等待進程終止
- bInheritHandle:表示所得到的進程句柄是否可以被繼承
- dwProcessId:被打開進程的PID
返回類型:
- 如成功,返回值為指定進程的句柄。
- 如失敗,返回值為NULL,可調用GetLastError()獲得錯誤代碼。
HANDLE GetProcessHandle(DWORD nID)
{
//PROCESS_ALL_ACCESS 所有能獲得的權限
return OpenProcess(PROCESS_ALL_ACCESS, FALSE, nID);
}
DWORD dwProcessId = GetProcessIdFromProcessName("WireCut.EXE");
if (dwProcessId != 0)
{
bRet = DetourContinueProcessWithDllW(GetProcessHandle(dwProcessId), szDllFilePath);
}
到此就完成了所有工作,后面提供項目代碼和庫文件
鏈接:https://pan.baidu.com/s/1c09LWg9zo5NIVwR2htJYZA
提取碼:f0kt
歡迎關注交流共同進步
博客地址:wqliceman.top