vc++ 向其他程序注入程式碼的三種方法
阿新 • • 發佈:2019-02-04
導言:
我們在Code project(www.codeproject.com)上可以找到許多密碼間諜程式(譯者注:那些可以看到別的程式中密碼框內容的軟體),他們都依賴於Windows鉤子技術。要實現這個還有其他的方法嗎?有!但是,首先,讓我們簡單回顧一下我們要實現的目標,以便你能弄清楚我在說什麼。
要讀取一個控制元件的內容,不管它是否屬於你自己的程式,一般來說需要傳送 WM_GETTEXT 訊息到那個控制元件。這對edit控制元件也有效,但是有一種情況例外。如果這個edit控制元件屬於其他程序並且具有 ES_PASSWORD 風格的話,這種方法就不會成功。只有“擁有(OWNS)”這個密碼控制元件的程序才可以用 WM_GETTEXT 取得它的內容。所以,我們的問題就是:如何讓下面這句程式碼在其他程序的地址空間中執行起來:
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
一般來說,這個問題有三種可能的解決方案:
1. 把你的程式碼放到一個DLL中;然後用 windows 鉤子把它對映到遠端程序。
2. 把你的程式碼放到一個DLL中;然後用 CreateRemoteThread 和 LoadLibrary 把它對映到遠端程序。
3. 不用DLL,直接複製你的程式碼到遠端程序(使用WriteProcessMemory)並且用CreateRemoteThread執行之。在這裡有詳細的說明:
Ⅰ. Windows 鉤子
示例程式:HookSpy 和 HookInjEx
Windows鉤子的主要作用就是監視某個執行緒的訊息流動。一般可分為:
1. 區域性鉤子,只監視你自己程序中某個執行緒的訊息流動。
2. 遠端鉤子,又可以分為:
a. 特定執行緒的,監視別的程序中某個執行緒的訊息;
b. 系統級的,監視整個系統中正在執行的所有執行緒的訊息。
如果被掛鉤(監視)的執行緒屬於別的程序(情況2a和2b),你的鉤子過程(hook procedure)必須放在一個動態連線庫(DLL)中。系統把這包含了鉤子過程的DLL對映到被掛鉤的執行緒的地址空間。Windows會對映整個DLL而不僅僅是你的鉤子過程。這就是為什麼windows鉤子可以用來向其他執行緒的地址空間注入程式碼的原因了。
在這裡我不想深入討論鉤子的問題(請看MSDN中對SetWindowsHookEx的說明),讓我再告訴你兩個文件中找不到的訣竅,可能會有用:
1. 當SetWindowHookEx呼叫成功後,系統會自動對映這個DLL到被掛鉤的執行緒,但並不是立即對映。因為所有的Windows鉤子都是基於訊息的,直到一個適當的事件發生後這個DLL才被對映。比如:
如果你安裝了一個監視所有未排隊的(nonqueued)的訊息的鉤子(WH_CALLWNDPROC),只有一個訊息傳送到被掛鉤執行緒(的某個視窗)後這個DLL才被對映。也就是說,如果在訊息傳送到被掛鉤執行緒之前呼叫了UnhookWindowsHookEx那麼這個DLL就永遠不會被對映到該執行緒(雖然SetWindowsHookEx呼叫成功了)。為了強制對映,可以在呼叫SetWindowsHookEx後立即傳送一個適當的訊息到那個執行緒。
同理,呼叫UnhookWindowsHookEx之後,只有特定的事件發生後DLL才真正地從被掛鉤執行緒解除安裝。
2. 當你安裝了鉤子後,系統的效能會受到影響(特別是系統級的鉤子)。然而如果你只是使用的特定執行緒的鉤子來對映DLL而且不截獲如何訊息的話,這個缺陷也可以輕易地避免。看一下下面的程式碼片段:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( ul_reason_for_call == DLL_PROCESS_ATTACH )
{
//用 LoadLibrary增加引用次數
char lib_name[MAX_PATH];
::GetModuleFileName( hModule, lib_name, MAX_PATH );
::LoadLibrary( lib_name );
// 安全解除安裝鉤子
::UnhookWindowsHookEx( g_hHook );
}
return TRUE;
}
我們來看一下。首先,我們用鉤子對映這個DLL到遠端執行緒,然後,在DLL被真正對映進去後,我們立即解除安裝掛鉤(unhook)。一般來說當第一個訊息到達被掛鉤執行緒後,這DLL會被解除安裝,然而我們通過LoadLibrary來增加這個DLL的引用次數,避免了DLL被解除安裝。
剩下的問題是:使用完畢後如何解除安裝這個DLL?UnhookWindowsHookEx不行了,因為我們已經對那個執行緒取消掛鉤(unhook)了。你可以這麼做:
○在你想要解除安裝這個DLL之前再安裝一個鉤子;
○傳送一個“特殊”的訊息到遠端執行緒;
○在你的新鉤子的鉤子過程(hook procedure)中截獲該訊息,呼叫FreeLibrary 和 (譯者注:對新鉤子呼叫)UnhookwindowsHookEx。
現在,鉤子只在對映DLL到遠端程序和從遠端程序解除安裝DLL時使用,對被掛鉤執行緒的效能沒有影響。也就是說,我們找到了一種(相比第二部分討論的LoadLibrary技術)WinNT和Win9x下都可以使用的,不影響目的程序效能的DLL對映機制。
但是,我們應該在何種情況下使用該技巧呢?通常是在DLL需要在遠端程序中駐留較長時間(比如你要子類[subclass]另一個程序中的控制元件)並且你不想過於干涉目的程序時比較適合使用這種技巧。我在HookSpy中並沒有使用它,因為那個DLL只是短暫地注入一段時間――只要能取得密碼就足夠了。我在另一個例子HookInjEx中演示了這種方法。HookInjEx把一個DLL對映進“explorer.exe”(當然,最後又從其中解除安裝),子類了其中的開始按鈕,更確切地說我是把開始按鈕的滑鼠左右鍵點選事件顛倒了一下。
你可以在本文章的開頭部分找到HookSpy和HookInjEx及其原始碼的下載包連結。
Ⅱ. CreateRemoteThread 和 LoadLibrary 技術
示例程式:LibSpy
通常,任何程序都可以通過LoadLibrary動態地載入DLL,但是我們如何強制一個外部程序呼叫該函式呢?答案是CreateRemoteThread。
讓我們先來看看LoadLibrary和FreeLibrary的函式宣告:
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName // address of filename of library module
);
BOOL FreeLibrary(
HMODULE hLibModule // handle to loaded library module
);
再和CreateRemoteThread的執行緒過程(thread procedure)ThreadProc比較一下:
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
你會發現所有的函式都有同樣的呼叫約定(calling convention)、都接受一個32位的引數並且返回值型別的大小也一樣。也就是說,我們可以把LoadLibrary/FreeLibrary的指標作為引數傳遞給CrateRemoteThread。
然而,還有兩個問題(參考下面對CreateRemoteThread的說明)
1. 傳遞給ThreadProc的lpStartAddress 引數必須為遠端程序中的執行緒過程的起始地址。
2. 如果把ThreadProc的lpParameter引數當做一個普通的32位整數(FreeLibrary把它當做HMODULE)那麼沒有如何問題,但是如果把它當做一個指標(LoadLibrary把它當做一個char*),它就必須指向遠端程序中的記憶體資料。
第一個問題其實已經迎刃而解了,因為LoadLibrary和FreeLibrary都是存在於kernel32.dll中的函式,而kernel32可以保證任何“正常”程序中都存在,且其載入地址都是一樣的。(參看附錄A)於是LoadLibrary/FreeLibrary在任何程序中的地址都是一樣的,這就保證了傳遞給遠端程序的指標是個有效的指標。
第二個問題也很簡單:把DLL的檔名(LodLibrary的引數)用WriteProcessMemory複製到遠端程序。
所以,使用CreateRemoteThread和LoadLibrary技術的步驟如下:
1. 得到遠端程序的HANDLE(使用OpenProcess)。
2. 在遠端程序中為DLL檔名分配記憶體(VirtualAllocEx)。
3. 把DLL的檔名(全路徑)寫到分配的記憶體中(WriteProcessMemory)
4. 使用CreateRemoteThread和LoadLibrary把你的DLL對映近遠端程序。
5. 等待遠端執行緒結束(WaitForSingleObject),即等待LoadLibrary返回。也就是說當我們的DllMain(是以DLL_PROCESS_ATTACH為引數呼叫的)返回時遠端執行緒也就立即結束了。
6. 取回遠端執行緒的結束碼(GetExitCodeThtread),即LoadLibrary的返回值――我們DLL載入後的基地址(HMODULE)。
7. 釋放第2步分配的記憶體(VirtualFreeEx)。
8. 用CreateRemoteThread和FreeLibrary把DLL從遠端程序中解除安裝。呼叫時傳遞第6步取得的HMODULE給FreeLibrary(通過CreateRemoteThread的lpParameter引數)。
9. 等待執行緒的結束(WaitSingleObject)。
同時,別忘了在最後關閉所有的控制代碼:第4、8步得到的執行緒控制代碼,第1步得到的遠端程序控制代碼。
現在我們看看LibSpy的部分程式碼,分析一下以上的步驟是任何實現的。為了簡單起見,沒有包含錯誤處理和支援Unicode的程式碼。
HANDLE hThread;
char szLibPath[_MAX_PATH]; // "LibSpy.dll"的檔名
// (包含全路徑!);
void* pLibRemote; // szLibPath 將要複製到地址
DWORD hLibModule; //已載入的DLL的基地址(HMODULE);
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
//初始化 szLibPath
//...
// 1. 在遠端程序中為szLibPath 分配記憶體
// 2. 寫szLibPath到分配的記憶體
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );
// 載入 "LibSpy.dll" 到遠端程序
// (通過 CreateRemoteThread & LoadLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"LoadLibraryA" ),
pLibRemote, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
//取得DLL的基地址
::GetExitCodeThread( hThread, &hLibModule );
//掃尾工作
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );
我們放在DllMain中的真正要注入的程式碼(比如為SendMessage)現在已經被執行了(由於DLL_PROCESS_ATTACH),所以現在可以把DLL從目的程序中解除安裝了。
// 從目標程序解除安裝LibSpu.dll
// (通過 CreateRemoteThread & FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"FreeLibrary" ),
(void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 掃尾工作
::CloseHandle( hThread );
程序間通訊
到目前為止,我們僅僅討論了任何向遠端程序注入DLL,然而,在多數情況下被注入的DLL需要和你的程式以某種方式通訊(記住,那個DLL是被對映到遠端程序中的,而不是在你的本地程式中!)。以密碼間諜為例:那個DLL需要知道包含了密碼的的控制元件的控制代碼。很明顯,這個控制代碼是不能在編譯期間硬編碼(hardcoded)進去的。同樣,當DLL得到密碼後,它也需要把密碼發回我們的程式。
幸運的是,這個問題有很多種解決方案:檔案對映(Mapping),WM_COPYDATA,剪貼簿等。還有一種非常便利的方法#pragma data_seg。這裡我不想深入討論因為它們在MSDN(看一下Interprocess Communications部分)或其他資料中都有很好的說明。我在LibSpy中使用的是#pragma data_seg。
你可以在本文章的開頭找到LibSpy及原始碼的下載連結。
Ⅲ.CreateRemoteThread和WriteProcessMemory技術
示例程式:WinSpy
另一種注入程式碼到其他程序地址空間的方法是使用WriteProcessMemory API。這次你不用編寫一個獨立的DLL而是直接複製你的程式碼到遠端程序(WriteProcessMemory)並用CreateRemoteThread執行之。
讓我們看一下CreateRemoteThread的宣告:
HANDLE CreateRemoteThread(
HANDLE hProcess, // handle to process to create thread in
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security
// attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread
// function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
);
和CreateThread相比,有一下不同:
●增加了hProcess引數。這是要在其中建立執行緒的程序的控制代碼。
●CreateRemoteThread的lpStartAddress引數必須指向遠端程序的地址空間中的函式。這個函式必須存在於遠端程序中,所以我們不能簡單地傳遞一個本地ThreadFucn的地址,我們必須把程式碼複製到遠端程序。
●同樣,lpParameter引數指向的資料也必須存在於遠端程序中,我們也必須複製它。
現在,我們總結一下使用該技術的步驟:
1. 得到遠端程序的HANDLE(OpenProcess)。
2. 在遠端程序中為要注入的資料分配記憶體(VirtualAllocEx)、
3. 把初始化後的INJDATA結構複製到分配的記憶體中(WriteProcessMemory)。
4. 在遠端程序中為要注入的資料分配記憶體(VirtualAllocEx)。
5. 把ThreadFunc複製到分配的記憶體中(WriteProcessMemory)。
6. 用CreateRemoteThread啟動遠端的ThreadFunc。
7. 等待遠端執行緒的結束(WaitForSingleObject)。
8. 從遠端程序取回指執行結果(ReadProcessMemory 或 GetExitCodeThread)。
9. 釋放第2、4步分配的記憶體(VirtualFreeEx)。
10. 關閉第6、1步開啟開啟的控制代碼。
另外,編寫ThreadFunc時必須遵守以下規則:
1. ThreadFunc不能呼叫除kernel32.dll和user32.dll之外動態庫中的API函式。只有kernel32.dll和user32.dll(如果被載入)可以保證在本地和目的程序中的載入地址是一樣的。(注意:user32並不一定被所有的Win32程序載入!)參考附錄A。如果你需要呼叫其他庫中的函式,在注入的程式碼中使用LoadLibrary和GetProcessAddress強制載入。如果由於某種原因,你需要的動態庫已經被對映進了目的程序,你也可以使用GetMoudleHandle代替LoadLibrary。同樣,如果你想在ThreadFunc中呼叫你自己的函式,那麼就分別複製這些函式到遠端程序並通過INJDATA把地址提供給ThreadFunc。
2. 不要使用static字串。把所有的字串提供INJDATA傳遞。為什麼?編譯器會把所有的靜態字串放在可執行檔案的“.data”段,而僅僅在程式碼中保留它們的引用(即指標)。這樣,遠端程序中的ThreadFunc就會執行不存在的記憶體資料(至少沒有在它自己的記憶體空間中)。
3. 去掉編譯器的/GZ編譯選項。這個選項是預設的(看附錄B)。
4. 要麼把ThreadFunc和AfterThreadFunc宣告為static,要麼關閉編譯器的“增量連線(incremental linking)”(看附錄C)。
5. ThreadFunc中的區域性變數總大小必須小於4k位元組(看附錄D)。注意,當degug編譯時,這4k中大約有10個位元組會被事先佔用。
6. 如果有多於3個switch分支的case語句,必須像下面這樣分割開,或用if-else if代替:
switch( expression ) {
case constant1: statement1; goto END;
case constant2: statement2; goto END;
case constant3: statement2; goto END;
}
switch( expression ) {
case constant4: statement4; goto END;
case constant5: statement5; goto END;
case constant6: statement6; goto END;
}
END:
(參考附錄E)
如果你不按照這些遊戲規則玩的話,你註定會使目的程序掛掉!記住,不要妄想遠端程序中的任何資料會和你本地程序中的資料存放在相同記憶體地址!(參看附錄F)
(原話如此:You will almost certainly crash the target process if you don't play by those rules. Just remember: Don't assume anything in the target process is at the same address as it is in your process.)
GetWindowTextRemote(A/W)
所有取得遠端edit中文字的工作都被封裝進這個函式:GetWindowTextRemote(A/W):
int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
引數:
hProcess
目的edit所在的程序控制代碼
hWnd
目的edit的控制代碼
lpString
接收字串的緩衝
返回值:
成功複製的字元數。
讓我們看以下它的部分程式碼,特別是注入的資料和程式碼。為了簡單起見,沒有包含支援Unicode的程式碼。
INJDATA
typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
typedef struct {
HWND hwnd; // handle to edit control
SENDMESSAGE fnSendMessage; // pointer to user32!SendMessageA
char psText[128]; // buffer that is to receive the password
} INJDATA;
INJDATA是要注入遠端程序的資料。在把它的地址傳遞給SendMessageA之前,我們要先對它進行初始化。幸運的是unse32.dll在所有的程序中(如果被對映)總是被對映到相同的地址,所以SendMessageA的地址也總是相同的,這也保證了傳遞給遠端程序的地址是有效的。
ThreadFunc
static DWORD WINAPI ThreadFunc (INJDATA *pData)
{
pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // 得到密碼
sizeof(pData->psText),
(LPARAM)pData->psText );
return 0;
}
// This function marks the memory address after ThreadFunc.
// int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.
static void AfterThreadFunc (void)
{
}
ThreadFunc是遠端執行緒實際執行的程式碼。
●注意AfterThreadFunc是如何計算ThreadFunc的程式碼大小的。一般地,這不是最好的辦法,因為編譯器會改變你的函式中程式碼的順序(比如它會把ThreadFunc放在AfterThreadFunc之後)。然而,你至少可以確定在同一個工程中,比如在我們的WinSpy工程中,你函式的順序是固定的。如果有必要,你可以使用/ORDER連線選項,或者,用反彙編工具確定ThreadFunc的大小,這個也許會更好。
如何用該技術子類(subclass)一個遠端控制元件
示例程式:InjectEx
讓我們來討論一個更復雜的問題:如何子類屬於其他程序的一個控制元件?
首先,要完成這個任務,你必須複製兩個函式到遠端程序:
1. ThreadFunc,這個函式通過呼叫SetWindowLong API來子類遠端程序中的控制元件,
2. NewProc, 那個控制元件的新視窗過程(Window Procedure)。
然而,最主要的問題是如何傳遞資料到遠端的NewProc。因為NewProc是一個回撥(callback)函式,它必須符合特定的要求(譯者注:這裡指的主要是引數個數和型別),我們不能再簡單地傳遞一個INJDATA的指標作為它的引數。幸運的我已經找到解決這個問題的方法,而且是兩個,但是都要藉助於組合語言。我一直都努力避免使用匯編,但是這一次,我們逃不掉了,沒有彙編不行的。
解決方案1
看下面的圖片:
不知道你是否注意到了,INJDATA緊挨著NewProc放在NewProc的前面?這樣的話在編譯期間NewProc就可以知道INJDATA的記憶體地址。更精確地說,它知道INJDATA相對於它自身地址的相對偏移,但是這並不是我們真正想要的。現在,NewProc看起來是這個樣子:
static LRESULT CALLBACK NewProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam ) // second message parameter
{
INJDATA* pData = (INJDATA*) NewProc; // pData 指向
// NewProc;
pData--; // 現在pData指向INJDATA;
// 記住,INJDATA 在遠端程序中剛好位於
// NewProc的緊前面;
//-----------------------------
// 子類程式碼
// ........
//-----------------------------
//呼叫用來的的視窗過程;
// fnOldProc (由SetWindowLong返回) 是被ThreadFunc(遠端程序中的)初始化
// 並且儲存在遠端程序中的INJDATA裡的;
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
然而,還有一個問題,看第一行:
INJDATA* pData = (INJDATA*) NewProc;
pData被硬編碼為我們程序中NewProc的地址,但這是不對的。因為NewProc會被複制到遠端程序,那樣的話,這個地址就錯了。
用C/C++沒有辦法解決這個問題,可以用內聯的彙編來解決。看修改後的NewProc:
static LRESULT CALLBACK NewProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam ) // second message parameter
{
// 計算INJDATA 的地址;
// 在遠端程序中,INJDATA剛好在
//NewProc的前面;
INJDATA* pData;
_asm {
call dummy
dummy:
pop ecx // <- ECX 中存放當前的EIP
sub ecx, 9 // <- ECX 中存放NewProc的地址
mov pData, ecx
}
pData--;
//-----------------------------
// 子類程式碼
// ........
//-----------------------------
// 呼叫原來的視窗過程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
這是什麼意思?每個程序都有一個特殊的暫存器,這個暫存器指向下一條要執行的指令的記憶體地址,即32位Intel和AMD處理器上所謂的EIP暫存器。因為EIP是個特殊的暫存器,所以你不能像訪問通用暫存器(EAX,EBX等)那樣來訪問它。換句話說,你找不到一個可以用來定址EIP並且對它進行讀寫的操作碼(OpCode)。然而,EIP同樣可以被JMP,CALL,RET等指令隱含地改變(事實上它一直都在改變)。讓我們舉例說明32位的Intel和AMD處理器上CALL/RET是如何工作的吧:
當我們用CALL呼叫一個子程式時,這個子程式的地址被載入進EIP。同時,在EIP被改變之前,它以前的值會被自動壓棧(在後來被用作返回指令指標[return instruction-pointer])。在子程式的最後RET指令自動把這個值從棧中彈出到EIP。
現在我們知道了如何通過CALL和RET來修改EIP的值了,但是如何得到他的當前值?
還記得CALL把EIP的值壓棧了嗎?所以為了得到EIP的值我們呼叫了一個“假(dummy)函式”然後彈出棧頂值。看一下編譯過的NewProc:
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp ; entry point of
; NewProc
:00401001 8BEC mov ebp, esp
:00401003 51 push ecx
:00401004 E800000000 call 00401009 ; *a* call dummy
:00401009 59 pop ecx ; *b*
:0040100A 83E909 sub ecx, 00000009 ; *c*
:0040100D 894DFC mov [ebp-04], ecx ; mov pData, ECX
:00401010 8B45FC mov eax, [ebp-04]
:00401013 83E814 sub eax, 00000014 ; pData--;
.....
.....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
a. 一個假的函式呼叫;僅僅跳到下一條指令並且(譯者注:更重要的是)把EIP壓棧。
b. 彈出棧頂值到ECX。ECX就儲存的EIP的值;這也就是那條“pop ECX”指令的地址。
c. 注意從NewProc的入口點到“pop ECX”指令的“距離”為9位元組;因此把ECX減去9就得到的NewProc的地址了。
這樣一來,不管被複制到什麼地方,NewProc總能正確計算自身的地址了!然而,要注意從NewProc的入口點到“pop ECX”的距離可能會因為你的編譯器/連結選項的不同而不同,而且在Release和Degub版本中也是不一樣的。但是,不管怎樣,你仍然可以在編譯期知道這個距離的具體值。
1. 首先,編譯你的函式。
2. 在反彙編器(disassembler)中查出正確的距離值。
3. 最後,使用正確的距離值重新編譯你的程式。
這也是InjectEx中使用的解決方案。InjectEx和HookInjEx類似,交換開始按鈕上的滑鼠左右鍵點選事件。
解決方案2
在遠端程序中把INJDATA放在NewProc的前面並不是唯一的解決方案。看一下下面的NewProc:
static LRESULT CALLBACK NewProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam ) // second message parameter
{
INJDATA* pData = 0xA0B0C0D0; // 一個假值
//-----------------------------
// 子類程式碼
// ........
//-----------------------------
// 呼叫以前的視窗過程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
這裡,0XA0B0C0D0僅僅是INJDATA在遠端程序中的地址的佔位符(placeholder)。你無法在編譯期得到這個值,然而你在呼叫VirtualAllocEx(為INJDATA分配記憶體時)後確實知道INJDATA的地址!(譯者注:就是VirtualAllocEx的返回值)
我們的NewProc編譯後大概是這個樣子:
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:00401003 C745FCD0C0B0A0 mov [ebp-04], A0B0C0D0
:0040100A ...
....
....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
編譯後的機器碼應該為:558BECC745FCD0C0B0A0......8BE55DC21000。
現在,你這麼做:
1. 把INJDATA,ThreadFunc和NewFunc複製到目的程序。
2. 改變NewPoc的機器碼,讓pData指向INJDATA的真實地址。
比如,假設INJDATA的的真實地址(VirtualAllocEx的返回值)為0x008a0000,你把NewProc的機器碼改為:
558BECC745FCD0C0B0A0......8BE55DC21000 <- 修改前的 NewProc 1
558BECC745FC00008A00......8BE55DC21000 <- 修改後的 NewProc
也就是說,你把假值 A0B0C0D0改為INJDATA的真實地址2
3. 開始指向遠端的ThreadFunc,它子類了遠端程序中的控制元件。
我們在Code project(www.codeproject.com)上可以找到許多密碼間諜程式(譯者注:那些可以看到別的程式中密碼框內容的軟體),他們都依賴於Windows鉤子技術。要實現這個還有其他的方法嗎?有!但是,首先,讓我們簡單回顧一下我們要實現的目標,以便你能弄清楚我在說什麼。
要讀取一個控制元件的內容,不管它是否屬於你自己的程式,一般來說需要傳送 WM_GETTEXT 訊息到那個控制元件。這對edit控制元件也有效,但是有一種情況例外。如果這個edit控制元件屬於其他程序並且具有 ES_PASSWORD 風格的話,這種方法就不會成功。只有“擁有(OWNS)”這個密碼控制元件的程序才可以用 WM_GETTEXT 取得它的內容。所以,我們的問題就是:如何讓下面這句程式碼在其他程序的地址空間中執行起來:
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
一般來說,這個問題有三種可能的解決方案:
1. 把你的程式碼放到一個DLL中;然後用 windows 鉤子把它對映到遠端程序。
2. 把你的程式碼放到一個DLL中;然後用 CreateRemoteThread 和 LoadLibrary 把它對映到遠端程序。
3. 不用DLL,直接複製你的程式碼到遠端程序(使用WriteProcessMemory)並且用CreateRemoteThread執行之。在這裡有詳細的說明:
Ⅰ. Windows 鉤子
示例程式:HookSpy 和 HookInjEx
Windows鉤子的主要作用就是監視某個執行緒的訊息流動。一般可分為:
1. 區域性鉤子,只監視你自己程序中某個執行緒的訊息流動。
2. 遠端鉤子,又可以分為:
a. 特定執行緒的,監視別的程序中某個執行緒的訊息;
b. 系統級的,監視整個系統中正在執行的所有執行緒的訊息。
如果被掛鉤(監視)的執行緒屬於別的程序(情況2a和2b),你的鉤子過程(hook procedure)必須放在一個動態連線庫(DLL)中。系統把這包含了鉤子過程的DLL對映到被掛鉤的執行緒的地址空間。Windows會對映整個DLL而不僅僅是你的鉤子過程。這就是為什麼windows鉤子可以用來向其他執行緒的地址空間注入程式碼的原因了。
在這裡我不想深入討論鉤子的問題(請看MSDN中對SetWindowsHookEx的說明),讓我再告訴你兩個文件中找不到的訣竅,可能會有用:
1. 當SetWindowHookEx呼叫成功後,系統會自動對映這個DLL到被掛鉤的執行緒,但並不是立即對映。因為所有的Windows鉤子都是基於訊息的,直到一個適當的事件發生後這個DLL才被對映。比如:
如果你安裝了一個監視所有未排隊的(nonqueued)的訊息的鉤子(WH_CALLWNDPROC),只有一個訊息傳送到被掛鉤執行緒(的某個視窗)後這個DLL才被對映。也就是說,如果在訊息傳送到被掛鉤執行緒之前呼叫了UnhookWindowsHookEx那麼這個DLL就永遠不會被對映到該執行緒(雖然SetWindowsHookEx呼叫成功了)。為了強制對映,可以在呼叫SetWindowsHookEx後立即傳送一個適當的訊息到那個執行緒。
同理,呼叫UnhookWindowsHookEx之後,只有特定的事件發生後DLL才真正地從被掛鉤執行緒解除安裝。
2. 當你安裝了鉤子後,系統的效能會受到影響(特別是系統級的鉤子)。然而如果你只是使用的特定執行緒的鉤子來對映DLL而且不截獲如何訊息的話,這個缺陷也可以輕易地避免。看一下下面的程式碼片段:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( ul_reason_for_call == DLL_PROCESS_ATTACH )
{
//用 LoadLibrary增加引用次數
char lib_name[MAX_PATH];
::GetModuleFileName( hModule, lib_name, MAX_PATH );
::LoadLibrary( lib_name );
// 安全解除安裝鉤子
::UnhookWindowsHookEx( g_hHook );
}
return TRUE;
}
我們來看一下。首先,我們用鉤子對映這個DLL到遠端執行緒,然後,在DLL被真正對映進去後,我們立即解除安裝掛鉤(unhook)。一般來說當第一個訊息到達被掛鉤執行緒後,這DLL會被解除安裝,然而我們通過LoadLibrary來增加這個DLL的引用次數,避免了DLL被解除安裝。
剩下的問題是:使用完畢後如何解除安裝這個DLL?UnhookWindowsHookEx不行了,因為我們已經對那個執行緒取消掛鉤(unhook)了。你可以這麼做:
○在你想要解除安裝這個DLL之前再安裝一個鉤子;
○傳送一個“特殊”的訊息到遠端執行緒;
○在你的新鉤子的鉤子過程(hook procedure)中截獲該訊息,呼叫FreeLibrary 和 (譯者注:對新鉤子呼叫)UnhookwindowsHookEx。
現在,鉤子只在對映DLL到遠端程序和從遠端程序解除安裝DLL時使用,對被掛鉤執行緒的效能沒有影響。也就是說,我們找到了一種(相比第二部分討論的LoadLibrary技術)WinNT和Win9x下都可以使用的,不影響目的程序效能的DLL對映機制。
但是,我們應該在何種情況下使用該技巧呢?通常是在DLL需要在遠端程序中駐留較長時間(比如你要子類[subclass]另一個程序中的控制元件)並且你不想過於干涉目的程序時比較適合使用這種技巧。我在HookSpy中並沒有使用它,因為那個DLL只是短暫地注入一段時間――只要能取得密碼就足夠了。我在另一個例子HookInjEx中演示了這種方法。HookInjEx把一個DLL對映進“explorer.exe”(當然,最後又從其中解除安裝),子類了其中的開始按鈕,更確切地說我是把開始按鈕的滑鼠左右鍵點選事件顛倒了一下。
你可以在本文章的開頭部分找到HookSpy和HookInjEx及其原始碼的下載包連結。
Ⅱ. CreateRemoteThread 和 LoadLibrary 技術
示例程式:LibSpy
通常,任何程序都可以通過LoadLibrary動態地載入DLL,但是我們如何強制一個外部程序呼叫該函式呢?答案是CreateRemoteThread。
讓我們先來看看LoadLibrary和FreeLibrary的函式宣告:
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName // address of filename of library module
);
BOOL FreeLibrary(
HMODULE hLibModule // handle to loaded library module
);
再和CreateRemoteThread的執行緒過程(thread procedure)ThreadProc比較一下:
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
你會發現所有的函式都有同樣的呼叫約定(calling convention)、都接受一個32位的引數並且返回值型別的大小也一樣。也就是說,我們可以把LoadLibrary/FreeLibrary的指標作為引數傳遞給CrateRemoteThread。
然而,還有兩個問題(參考下面對CreateRemoteThread的說明)
1. 傳遞給ThreadProc的lpStartAddress 引數必須為遠端程序中的執行緒過程的起始地址。
2. 如果把ThreadProc的lpParameter引數當做一個普通的32位整數(FreeLibrary把它當做HMODULE)那麼沒有如何問題,但是如果把它當做一個指標(LoadLibrary把它當做一個char*),它就必須指向遠端程序中的記憶體資料。
第一個問題其實已經迎刃而解了,因為LoadLibrary和FreeLibrary都是存在於kernel32.dll中的函式,而kernel32可以保證任何“正常”程序中都存在,且其載入地址都是一樣的。(參看附錄A)於是LoadLibrary/FreeLibrary在任何程序中的地址都是一樣的,這就保證了傳遞給遠端程序的指標是個有效的指標。
第二個問題也很簡單:把DLL的檔名(LodLibrary的引數)用WriteProcessMemory複製到遠端程序。
所以,使用CreateRemoteThread和LoadLibrary技術的步驟如下:
1. 得到遠端程序的HANDLE(使用OpenProcess)。
2. 在遠端程序中為DLL檔名分配記憶體(VirtualAllocEx)。
3. 把DLL的檔名(全路徑)寫到分配的記憶體中(WriteProcessMemory)
4. 使用CreateRemoteThread和LoadLibrary把你的DLL對映近遠端程序。
5. 等待遠端執行緒結束(WaitForSingleObject),即等待LoadLibrary返回。也就是說當我們的DllMain(是以DLL_PROCESS_ATTACH為引數呼叫的)返回時遠端執行緒也就立即結束了。
6. 取回遠端執行緒的結束碼(GetExitCodeThtread),即LoadLibrary的返回值――我們DLL載入後的基地址(HMODULE)。
7. 釋放第2步分配的記憶體(VirtualFreeEx)。
8. 用CreateRemoteThread和FreeLibrary把DLL從遠端程序中解除安裝。呼叫時傳遞第6步取得的HMODULE給FreeLibrary(通過CreateRemoteThread的lpParameter引數)。
9. 等待執行緒的結束(WaitSingleObject)。
同時,別忘了在最後關閉所有的控制代碼:第4、8步得到的執行緒控制代碼,第1步得到的遠端程序控制代碼。
現在我們看看LibSpy的部分程式碼,分析一下以上的步驟是任何實現的。為了簡單起見,沒有包含錯誤處理和支援Unicode的程式碼。
HANDLE hThread;
char szLibPath[_MAX_PATH]; // "LibSpy.dll"的檔名
// (包含全路徑!);
void* pLibRemote; // szLibPath 將要複製到地址
DWORD hLibModule; //已載入的DLL的基地址(HMODULE);
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
//初始化 szLibPath
//...
// 1. 在遠端程序中為szLibPath 分配記憶體
// 2. 寫szLibPath到分配的記憶體
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );
// 載入 "LibSpy.dll" 到遠端程序
// (通過 CreateRemoteThread & LoadLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"LoadLibraryA" ),
pLibRemote, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
//取得DLL的基地址
::GetExitCodeThread( hThread, &hLibModule );
//掃尾工作
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );
我們放在DllMain中的真正要注入的程式碼(比如為SendMessage)現在已經被執行了(由於DLL_PROCESS_ATTACH),所以現在可以把DLL從目的程序中解除安裝了。
// 從目標程序解除安裝LibSpu.dll
// (通過 CreateRemoteThread & FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"FreeLibrary" ),
(void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 掃尾工作
::CloseHandle( hThread );
程序間通訊
到目前為止,我們僅僅討論了任何向遠端程序注入DLL,然而,在多數情況下被注入的DLL需要和你的程式以某種方式通訊(記住,那個DLL是被對映到遠端程序中的,而不是在你的本地程式中!)。以密碼間諜為例:那個DLL需要知道包含了密碼的的控制元件的控制代碼。很明顯,這個控制代碼是不能在編譯期間硬編碼(hardcoded)進去的。同樣,當DLL得到密碼後,它也需要把密碼發回我們的程式。
幸運的是,這個問題有很多種解決方案:檔案對映(Mapping),WM_COPYDATA,剪貼簿等。還有一種非常便利的方法#pragma data_seg。這裡我不想深入討論因為它們在MSDN(看一下Interprocess Communications部分)或其他資料中都有很好的說明。我在LibSpy中使用的是#pragma data_seg。
你可以在本文章的開頭找到LibSpy及原始碼的下載連結。
Ⅲ.CreateRemoteThread和WriteProcessMemory技術
示例程式:WinSpy
另一種注入程式碼到其他程序地址空間的方法是使用WriteProcessMemory API。這次你不用編寫一個獨立的DLL而是直接複製你的程式碼到遠端程序(WriteProcessMemory)並用CreateRemoteThread執行之。
讓我們看一下CreateRemoteThread的宣告:
HANDLE CreateRemoteThread(
HANDLE hProcess, // handle to process to create thread in
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security
// attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread
// function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
);
和CreateThread相比,有一下不同:
●增加了hProcess引數。這是要在其中建立執行緒的程序的控制代碼。
●CreateRemoteThread的lpStartAddress引數必須指向遠端程序的地址空間中的函式。這個函式必須存在於遠端程序中,所以我們不能簡單地傳遞一個本地ThreadFucn的地址,我們必須把程式碼複製到遠端程序。
●同樣,lpParameter引數指向的資料也必須存在於遠端程序中,我們也必須複製它。
現在,我們總結一下使用該技術的步驟:
1. 得到遠端程序的HANDLE(OpenProcess)。
2. 在遠端程序中為要注入的資料分配記憶體(VirtualAllocEx)、
3. 把初始化後的INJDATA結構複製到分配的記憶體中(WriteProcessMemory)。
4. 在遠端程序中為要注入的資料分配記憶體(VirtualAllocEx)。
5. 把ThreadFunc複製到分配的記憶體中(WriteProcessMemory)。
6. 用CreateRemoteThread啟動遠端的ThreadFunc。
7. 等待遠端執行緒的結束(WaitForSingleObject)。
8. 從遠端程序取回指執行結果(ReadProcessMemory 或 GetExitCodeThread)。
9. 釋放第2、4步分配的記憶體(VirtualFreeEx)。
10. 關閉第6、1步開啟開啟的控制代碼。
另外,編寫ThreadFunc時必須遵守以下規則:
1. ThreadFunc不能呼叫除kernel32.dll和user32.dll之外動態庫中的API函式。只有kernel32.dll和user32.dll(如果被載入)可以保證在本地和目的程序中的載入地址是一樣的。(注意:user32並不一定被所有的Win32程序載入!)參考附錄A。如果你需要呼叫其他庫中的函式,在注入的程式碼中使用LoadLibrary和GetProcessAddress強制載入。如果由於某種原因,你需要的動態庫已經被對映進了目的程序,你也可以使用GetMoudleHandle代替LoadLibrary。同樣,如果你想在ThreadFunc中呼叫你自己的函式,那麼就分別複製這些函式到遠端程序並通過INJDATA把地址提供給ThreadFunc。
2. 不要使用static字串。把所有的字串提供INJDATA傳遞。為什麼?編譯器會把所有的靜態字串放在可執行檔案的“.data”段,而僅僅在程式碼中保留它們的引用(即指標)。這樣,遠端程序中的ThreadFunc就會執行不存在的記憶體資料(至少沒有在它自己的記憶體空間中)。
3. 去掉編譯器的/GZ編譯選項。這個選項是預設的(看附錄B)。
4. 要麼把ThreadFunc和AfterThreadFunc宣告為static,要麼關閉編譯器的“增量連線(incremental linking)”(看附錄C)。
5. ThreadFunc中的區域性變數總大小必須小於4k位元組(看附錄D)。注意,當degug編譯時,這4k中大約有10個位元組會被事先佔用。
6. 如果有多於3個switch分支的case語句,必須像下面這樣分割開,或用if-else if代替:
switch( expression ) {
case constant1: statement1; goto END;
case constant2: statement2; goto END;
case constant3: statement2; goto END;
}
switch( expression ) {
case constant4: statement4; goto END;
case constant5: statement5; goto END;
case constant6: statement6; goto END;
}
END:
(參考附錄E)
如果你不按照這些遊戲規則玩的話,你註定會使目的程序掛掉!記住,不要妄想遠端程序中的任何資料會和你本地程序中的資料存放在相同記憶體地址!(參看附錄F)
(原話如此:You will almost certainly crash the target process if you don't play by those rules. Just remember: Don't assume anything in the target process is at the same address as it is in your process.)
GetWindowTextRemote(A/W)
所有取得遠端edit中文字的工作都被封裝進這個函式:GetWindowTextRemote(A/W):
int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
引數:
hProcess
目的edit所在的程序控制代碼
hWnd
目的edit的控制代碼
lpString
接收字串的緩衝
返回值:
成功複製的字元數。
讓我們看以下它的部分程式碼,特別是注入的資料和程式碼。為了簡單起見,沒有包含支援Unicode的程式碼。
INJDATA
typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
typedef struct {
HWND hwnd; // handle to edit control
SENDMESSAGE fnSendMessage; // pointer to user32!SendMessageA
char psText[128]; // buffer that is to receive the password
} INJDATA;
INJDATA是要注入遠端程序的資料。在把它的地址傳遞給SendMessageA之前,我們要先對它進行初始化。幸運的是unse32.dll在所有的程序中(如果被對映)總是被對映到相同的地址,所以SendMessageA的地址也總是相同的,這也保證了傳遞給遠端程序的地址是有效的。
ThreadFunc
static DWORD WINAPI ThreadFunc (INJDATA *pData)
{
pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // 得到密碼
sizeof(pData->psText),
(LPARAM)pData->psText );
return 0;
}
// This function marks the memory address after ThreadFunc.
// int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.
static void AfterThreadFunc (void)
{
}
ThreadFunc是遠端執行緒實際執行的程式碼。
●注意AfterThreadFunc是如何計算ThreadFunc的程式碼大小的。一般地,這不是最好的辦法,因為編譯器會改變你的函式中程式碼的順序(比如它會把ThreadFunc放在AfterThreadFunc之後)。然而,你至少可以確定在同一個工程中,比如在我們的WinSpy工程中,你函式的順序是固定的。如果有必要,你可以使用/ORDER連線選項,或者,用反彙編工具確定ThreadFunc的大小,這個也許會更好。
如何用該技術子類(subclass)一個遠端控制元件
示例程式:InjectEx
讓我們來討論一個更復雜的問題:如何子類屬於其他程序的一個控制元件?
首先,要完成這個任務,你必須複製兩個函式到遠端程序:
1. ThreadFunc,這個函式通過呼叫SetWindowLong API來子類遠端程序中的控制元件,
2. NewProc, 那個控制元件的新視窗過程(Window Procedure)。
然而,最主要的問題是如何傳遞資料到遠端的NewProc。因為NewProc是一個回撥(callback)函式,它必須符合特定的要求(譯者注:這裡指的主要是引數個數和型別),我們不能再簡單地傳遞一個INJDATA的指標作為它的引數。幸運的我已經找到解決這個問題的方法,而且是兩個,但是都要藉助於組合語言。我一直都努力避免使用匯編,但是這一次,我們逃不掉了,沒有彙編不行的。
解決方案1
看下面的圖片:
不知道你是否注意到了,INJDATA緊挨著NewProc放在NewProc的前面?這樣的話在編譯期間NewProc就可以知道INJDATA的記憶體地址。更精確地說,它知道INJDATA相對於它自身地址的相對偏移,但是這並不是我們真正想要的。現在,NewProc看起來是這個樣子:
static LRESULT CALLBACK NewProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam ) // second message parameter
{
INJDATA* pData = (INJDATA*) NewProc; // pData 指向
// NewProc;
pData--; // 現在pData指向INJDATA;
// 記住,INJDATA 在遠端程序中剛好位於
// NewProc的緊前面;
//-----------------------------
// 子類程式碼
// ........
//-----------------------------
//呼叫用來的的視窗過程;
// fnOldProc (由SetWindowLong返回) 是被ThreadFunc(遠端程序中的)初始化
// 並且儲存在遠端程序中的INJDATA裡的;
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
然而,還有一個問題,看第一行:
INJDATA* pData = (INJDATA*) NewProc;
pData被硬編碼為我們程序中NewProc的地址,但這是不對的。因為NewProc會被複制到遠端程序,那樣的話,這個地址就錯了。
用C/C++沒有辦法解決這個問題,可以用內聯的彙編來解決。看修改後的NewProc:
static LRESULT CALLBACK NewProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam ) // second message parameter
{
// 計算INJDATA 的地址;
// 在遠端程序中,INJDATA剛好在
//NewProc的前面;
INJDATA* pData;
_asm {
call dummy
dummy:
pop ecx // <- ECX 中存放當前的EIP
sub ecx, 9 // <- ECX 中存放NewProc的地址
mov pData, ecx
}
pData--;
//-----------------------------
// 子類程式碼
// ........
//-----------------------------
// 呼叫原來的視窗過程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
這是什麼意思?每個程序都有一個特殊的暫存器,這個暫存器指向下一條要執行的指令的記憶體地址,即32位Intel和AMD處理器上所謂的EIP暫存器。因為EIP是個特殊的暫存器,所以你不能像訪問通用暫存器(EAX,EBX等)那樣來訪問它。換句話說,你找不到一個可以用來定址EIP並且對它進行讀寫的操作碼(OpCode)。然而,EIP同樣可以被JMP,CALL,RET等指令隱含地改變(事實上它一直都在改變)。讓我們舉例說明32位的Intel和AMD處理器上CALL/RET是如何工作的吧:
當我們用CALL呼叫一個子程式時,這個子程式的地址被載入進EIP。同時,在EIP被改變之前,它以前的值會被自動壓棧(在後來被用作返回指令指標[return instruction-pointer])。在子程式的最後RET指令自動把這個值從棧中彈出到EIP。
現在我們知道了如何通過CALL和RET來修改EIP的值了,但是如何得到他的當前值?
還記得CALL把EIP的值壓棧了嗎?所以為了得到EIP的值我們呼叫了一個“假(dummy)函式”然後彈出棧頂值。看一下編譯過的NewProc:
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp ; entry point of
; NewProc
:00401001 8BEC mov ebp, esp
:00401003 51 push ecx
:00401004 E800000000 call 00401009 ; *a* call dummy
:00401009 59 pop ecx ; *b*
:0040100A 83E909 sub ecx, 00000009 ; *c*
:0040100D 894DFC mov [ebp-04], ecx ; mov pData, ECX
:00401010 8B45FC mov eax, [ebp-04]
:00401013 83E814 sub eax, 00000014 ; pData--;
.....
.....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
a. 一個假的函式呼叫;僅僅跳到下一條指令並且(譯者注:更重要的是)把EIP壓棧。
b. 彈出棧頂值到ECX。ECX就儲存的EIP的值;這也就是那條“pop ECX”指令的地址。
c. 注意從NewProc的入口點到“pop ECX”指令的“距離”為9位元組;因此把ECX減去9就得到的NewProc的地址了。
這樣一來,不管被複制到什麼地方,NewProc總能正確計算自身的地址了!然而,要注意從NewProc的入口點到“pop ECX”的距離可能會因為你的編譯器/連結選項的不同而不同,而且在Release和Degub版本中也是不一樣的。但是,不管怎樣,你仍然可以在編譯期知道這個距離的具體值。
1. 首先,編譯你的函式。
2. 在反彙編器(disassembler)中查出正確的距離值。
3. 最後,使用正確的距離值重新編譯你的程式。
這也是InjectEx中使用的解決方案。InjectEx和HookInjEx類似,交換開始按鈕上的滑鼠左右鍵點選事件。
解決方案2
在遠端程序中把INJDATA放在NewProc的前面並不是唯一的解決方案。看一下下面的NewProc:
static LRESULT CALLBACK NewProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam ) // second message parameter
{
INJDATA* pData = 0xA0B0C0D0; // 一個假值
//-----------------------------
// 子類程式碼
// ........
//-----------------------------
// 呼叫以前的視窗過程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
這裡,0XA0B0C0D0僅僅是INJDATA在遠端程序中的地址的佔位符(placeholder)。你無法在編譯期得到這個值,然而你在呼叫VirtualAllocEx(為INJDATA分配記憶體時)後確實知道INJDATA的地址!(譯者注:就是VirtualAllocEx的返回值)
我們的NewProc編譯後大概是這個樣子:
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:00401003 C745FCD0C0B0A0 mov [ebp-04], A0B0C0D0
:0040100A ...
....
....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
編譯後的機器碼應該為:558BECC745FCD0C0B0A0......8BE55DC21000。
現在,你這麼做:
1. 把INJDATA,ThreadFunc和NewFunc複製到目的程序。
2. 改變NewPoc的機器碼,讓pData指向INJDATA的真實地址。
比如,假設INJDATA的的真實地址(VirtualAllocEx的返回值)為0x008a0000,你把NewProc的機器碼改為:
558BECC745FCD0C0B0A0......8BE55DC21000 <- 修改前的 NewProc 1
558BECC745FC00008A00......8BE55DC21000 <- 修改後的 NewProc
也就是說,你把假值 A0B0C0D0改為INJDATA的真實地址2
3. 開始指向遠端的ThreadFunc,它子類了遠端程序中的控制元件。