本文將討論如何把代碼注入不同的進程地址空間,然后在該進程的上下文中執(zhí)行注入的代碼,。 我們在網(wǎng)上可以查到一些窗口/密碼偵測的應(yīng)用例子,,網(wǎng)上的這些程序大多都依賴 Windows 鉤子技術(shù)來實現(xiàn)。本文將討論除了使用 Windows 鉤子技術(shù)以外的其它技術(shù)來實現(xiàn)這個功能,。如圖一所示: 圖一 WinSpy 密碼偵測程序 為了找到解決問題的方法,。首先讓我們簡單回顧一下問題背景。 ::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer ); 通常有三種可能性來解決這個問題。
第一部分: Windows 鉤子 范例程序——參見HookSpy 和HookInjEx Windows 鉤子主要作用是監(jiān)控某些線程的消息流,。通常我們將鉤子分為本地鉤子和遠(yuǎn)程鉤子以及系統(tǒng)級鉤子,,本地鉤子一般監(jiān)控屬于本進程的線程的消息流,遠(yuǎn)程鉤子是線程專用的,,用于監(jiān)控屬于另外進程的線程消息流,。系統(tǒng)級鉤子監(jiān)控運行在當(dāng)前系統(tǒng)中的所有線程的消息流。
HookSpy 和 HookInjEx 的源代碼都可以從本文的下載源代碼中獲得,。 第二部分:CreateRemoteThread 和 LoadLibrary 技術(shù) 范例程序——LibSpy 通常,,任何進程都可以通過 LoadLibrary API 動態(tài)加載DLL。但是,,如何強制一個外部進程調(diào)用這個函數(shù)呢,?答案是:CreateRemoteThread。 HINSTANCE LoadLibrary( LPCTSTR lpLibFileName // 庫模塊文件名的地址 ); BOOL FreeLibrary( HMODULE hLibModule // 要加載的庫模塊的句柄 ); 現(xiàn)在將它們與傳遞到 CreateRemoteThread 的線程例程——ThreadProc 的聲明進行比較,。 DWORD WINAPI ThreadProc( LPVOID lpParameter // 線程數(shù)據(jù) ); 你可以看到,所有函數(shù)都使用相同的調(diào)用規(guī)范并都接受 32位參數(shù),,返回值的大小都相同,。也就是說,,我們可以傳遞一個指針到LoadLibrary/FreeLibrary 作為到 CreateRemoteThread 的線程例程。但這里有兩個問題,,請看下面對CreateRemoteThread 的描述:
第一個問題實際上是由它自己解決的,。LoadLibrary 和 FreeLibray 兩個函數(shù)都在 kernel32.dll 中。因為必須保證kernel32存在并且在每個“常規(guī)”進程中的加載地址要相同,,LoadLibrary/FreeLibray 的地址在每個進程中的地址要相同,,這就保證了有效的指針被傳遞到遠(yuǎn)程進程。 所以,為了使用CreateRemoteThread 和 LoadLibrary 技術(shù),,需要按照下列步驟來做:
此外,,處理完成后不要忘了關(guān)閉所有句柄,,包括在第四步和第八步創(chuàng)建的兩個線程以及在第一步獲取的遠(yuǎn)程線程句柄。現(xiàn)在讓我們看一下 LibSpy 的部分代碼,,為了簡單起見,,上述步驟的實現(xiàn)細(xì)節(jié)中的錯誤處理以及 UNICODE 支持部分被略掉。 HANDLE hThread; char szLibPath[_MAX_PATH]; // “LibSpy.dll”模塊的名稱 (包括全路徑); void* pLibRemote; // 遠(yuǎn)程進程中的地址,,szLibPath 將被拷貝到此處; DWORD hLibModule; // 要加載的模塊的基地址(HMODULE) HMODULE hKernel32 = ::GetModuleHandle("Kernel32"); // 初始化szLibPath //... // 1. 在遠(yuǎn)程進程中為szLibPath 分配內(nèi)存 // 2. 將szLibPath 寫入分配的內(nèi)存 pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath), MEM_COMMIT, PAGE_READWRITE ); ::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath, sizeof(szLibPath), NULL ); // 將"LibSpy.dll" 加載到遠(yuǎn)程進程(使用CreateRemoteThread 和 LoadLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32, "LoadLibraryA" ), pLibRemote, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // 獲取所加載的模塊的句柄 ::GetExitCodeThread( hThread, &hLibModule ); // 清除 ::CloseHandle( hThread ); ::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );假設(shè)我們實際想要注入的代碼——SendMessage ——被放在DllMain (DLL_PROCESS_ATTACH)中,,現(xiàn)在它已經(jīng)被執(zhí)行。那么現(xiàn)在應(yīng)該從目標(biāo)進程中將DLL 卸載: // 從目標(biāo)進程中卸載"LibSpy.dll" (使用 CreateRemoteThread 和 FreeLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32, "FreeLibrary" ), (void*)hLibModule, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // 清除 ::CloseHandle( hThread );進程間通信 到目前為止,,我們只討論了關(guān)于如何將DLL 注入到遠(yuǎn)程進程的內(nèi)容,,但是,在大多數(shù)情況下,,注入的 DLL 都需要與原應(yīng)用程序進行某種方式的通信(回想一下,,我們的DLL是被映射到某個遠(yuǎn)程進程的地址空間里了,不是在本地應(yīng)用程序的地址空間中),。比如秘密偵測程序,,DLL必須要知道實際包含密碼的控件句柄,顯然,,編譯時無法將這個值進行硬編碼,。同樣,一旦DLL獲得了秘密,,它必須將它發(fā)送回原應(yīng)用程序,,以便能正確顯示出來。 幸運的是,,有許多方法處理這個問題,,文件映射,WM_COPYDATA,,剪貼板以及很簡單的 #pragma data_seg 共享數(shù)據(jù)段等,,本文我不打算使用這些技術(shù),因為MSDN(“進程間通信”部分)以及其它渠道可以找到很多文檔參考,。不過我在 LibSpy例子中還是使用了 #pragma data_seg,。細(xì)節(jié)請參考 LibSpy 源代碼。 第三部分:CreateRemoteThread 和 WriteProcessMemory 技術(shù) 范例程序——WinSpy 另外一個將代碼拷貝到另一個進程地址空間并在該進程上下文中執(zhí)行的方法是使用遠(yuǎn)程線程和 WriteProcessMemory API,。這種方法不用編寫單獨的DLL,,而是用 WriteProcessMemory 直接將代碼拷貝到遠(yuǎn)程進程——然后用 CreateRemoteThread 啟動它執(zhí)行。先來看看 CreateRemoteThread 的聲明: HANDLE CreateRemoteThread( HANDLE hProcess, // 傳入創(chuàng)建新線程的進程句柄 LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全屬性指針 DWORD dwStackSize, // 字節(jié)為單位的初始線程堆棧 LPTHREAD_START_ROUTINE lpStartAddress, // 指向線程函數(shù)的指針 LPVOID lpParameter, // 新線程使用的參數(shù) DWORD dwCreationFlags, // 創(chuàng)建標(biāo)志 LPDWORD lpThreadId // 指向返回的線程ID );如果你比較它與 CreateThread(MSDN)的聲明,,你會注意到如下的差別:
綜上所述,,我們得按照如下的步驟來做:
ThreadFunc 必須要遵循的原則:
如果你沒有按照這些規(guī)則來做,目標(biāo)進程很可能會崩潰,。所以務(wù)必牢記,。在目標(biāo)進程中不要假設(shè)任何事情都會像在本地進程中那樣 (參見附錄F)。 GetWindowTextRemote(A/W) 要想從“遠(yuǎn)程”編輯框獲得密碼,,你需要做的就是將所有功能都封裝在GetWindowTextRemot(A/W):中,。 int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString ); int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString ); 參數(shù)說明: hProcess:編輯框控件所屬的進程句柄; hWnd:包含密碼的編輯框控件句柄,; lpString:接收文本的緩沖指針,; 返回值:返回值是拷貝的字符數(shù); 下面讓我們看看它的部分代碼——尤其是注入數(shù)據(jù)的代碼——以便明白 GetWindowTextRemote 的工作原理,。此處為簡單起見,,略掉了 UNICODE 支持部分。 INJDATA typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM); typedef struct { HWND hwnd; // 編輯框句柄 SENDMESSAGE fnSendMessage; // 指向user32.dll 中 SendMessageA 的指針 char psText[128]; // 接收密碼的緩沖 } INJDATA; INJDATA 是一個被注入到遠(yuǎn)程進程的數(shù)據(jù)結(jié)構(gòu),。但在注入之前,,結(jié)構(gòu)中指向 SendMessageA 的指針是在本地應(yīng)用程序中初始化的。因為對于每個使用user32.dll的進程來說,,user32.dll總是被映射到相同的地址,,因此,SendMessageA 的地址也肯定是相同的,。這就保證了被傳遞到遠(yuǎn)程進程的是一個有效的指針,。 static DWORD WINAPI ThreadFunc (INJDATA *pData) { pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password sizeof(pData->psText), (LPARAM)pData->psText ); return 0; } // 該函數(shù)在ThreadFunc之后標(biāo)記內(nèi)存地址 // int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc. static void AfterThreadFunc (void) { } ThradFunc 是被遠(yuǎn)程線程執(zhí)行的代碼。
范例程序——InjectEx
這里主要的問題是如何將數(shù)據(jù)傳到遠(yuǎn)程窗口過程 NewProc,,因為 NewProc 是一個回調(diào)函數(shù),它必須遵循特定的規(guī)范和原則,,我們不能簡單地在參數(shù)中傳遞 INJDATA指針,。幸運的是我找到了有兩個方法來解決這個問題,只不過要借助匯編語言,,所以不要忽略了匯編,,關(guān)鍵時候它是很有用的! 如下圖所示: 在遠(yuǎn)程進程中,,INJDATA 被放在NewProc 之前,,這樣 NewProc 在編譯時便知道 INJDATA 在遠(yuǎn)程進程地址空間中的內(nèi)存位置。更確切地說,,它知道相對于其自身位置的 INJDATA 的地址,,我們需要所有這些信息。下面是 NewProc 的代碼: static LRESULT CALLBACK NewProc( HWND hwnd, // 窗口句柄 UINT uMsg, // 消息標(biāo)示符 WPARAM wParam, // 第一個消息參數(shù) LPARAM lParam ) // 第二個消息參數(shù) { INJDATA* pData = (INJDATA*) NewProc; // pData 指向 NewProc pData--; // 現(xiàn)在pData 指向INJDATA; // 回想一下INJDATA 被置于遠(yuǎn)程進程NewProc之前; //----------------------------- // 此處是子類化代碼 // ........ //----------------------------- // 調(diào)用原窗口過程; // fnOldProc (由SetWindowLong 返回) 被(遠(yuǎn)程)ThreadFunc初始化 // 并被保存在(遠(yuǎn)程)INJDATA;中 return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); }但這里還有一個問題,,見第一行代碼: INJDATA* pData = (INJDATA*) NewProc; 這種方式 pData得到的是硬編碼值(在我們的進程中是原 NewProc 的內(nèi)存地址),。這不是我們十分想要的。在遠(yuǎn)程進程中,,NewProc “當(dāng)前”拷貝的內(nèi)存地址與它被移到的實際位置是無關(guān)的,,換句話說,我們會需要某種類型的“this 指針”,。 static LRESULT CALLBACK NewProc( HWND hwnd, // 窗口句柄 UINT uMsg, // 消息標(biāo)示符 WPARAM wParam, // 第一個消息參數(shù) LPARAM lParam ) // 第二個消息參數(shù) { // 計算INJDATA 結(jié)構(gòu)的位置 // 在遠(yuǎn)程進程中記住這個INJDATA // 被放在NewProc之前 INJDATA* pData; _asm { call dummy dummy: pop ecx // <- ECX 包含當(dāng)前的EIP sub ecx, 9 // <- ECX 包含NewProc的地址 mov pData, ecx } pData--; //----------------------------- // 此處是子類化代碼 // ........ //----------------------------- // 調(diào)用原來的窗口過程 return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); }那么,,接下來該怎么辦呢,?事實上,每個進程都有一個特殊的寄存器,,它指向下一條要執(zhí)行的指令的內(nèi)存位置,。即所謂的指令指針,在32位 Intel 和 AMD 處理器上被表示為 EIP,。因為 EIP是一個專用寄存器,,你無法象操作一般常規(guī)存儲器(如:EAX,EBX等)那樣通過編程存取它,。也就是說沒有操作代碼來尋址 EIP,,以便直接讀取或修改其內(nèi)容,。但是,EIP 仍然還是可以通過間接方法修改的(并且隨時可以修改),,通過JMP,,CALL和RET這些指令實現(xiàn)。下面我們就通過例子來解釋通過 CALL/RET 子例程調(diào)用機制在32位 Intel 和 AMD 處理器上是如何工作的,。 當(dāng)你調(diào)用(通過 CALL)某個子例程時,,子例程的地址被加載到 EIP,但即便是在 EIP杯修改之前,,其舊的那個值被自動PUSH到堆棧(被用于后面作為指令指針返回)。在子例程執(zhí)行完時,,RET 指令自動將堆棧頂POP到 EIP,。 現(xiàn)在你知道了如何通過 CALL 和 RET 實現(xiàn) EIP 的修改,但如何獲取其當(dāng)前的值呢,?下面就來解決這個問題,,前面講過,CALL PUSH EIP 到堆棧,,所以,,為了獲取其當(dāng)前值,調(diào)用“啞函數(shù)”,,然后再POP堆棧頂,。讓我們用編譯后的 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
這樣一來,不管 NewProc 被移到什么地方,,它總能計算出其自己的地址,。但是,NewProc 的入口點和 “POP ECX”之間的距離可能會隨著你對編譯/鏈接選項的改變而變化,,由此造成 RELEASE和DEBUG版本之間也會有差別,。但關(guān)鍵是你仍然確切地知道編譯時的值。
此即為 InjecEx 中使用的解決方案,,類似于 HookInjEx,交換鼠標(biāo)點擊“開始”左右鍵時的功能,。 對于我們的問題,,在遠(yuǎn)程進程地址空間中將 INJDATA 放在 NewProc 前面不是唯一的解決辦法??聪旅?NewProc的變異版本: static LRESULT CALLBACK NewProc( HWND hwnd, // 窗口句柄 UINT uMsg, // 消息標(biāo)示符 WPARAM wParam, // 第一個消息參數(shù) LPARAM lParam ) // 第二個消息參數(shù) { INJDATA* pData = 0xA0B0C0D0; // 虛構(gòu)值 //----------------------------- // 子類化代碼 // ........ //----------------------------- // 調(diào)用原來的窗口過程 return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); }此處 0xA0B0C0D0 只是遠(yuǎn)程進程地址空間中真實(絕對)INJDATA地址的占位符,。前面講過,你無法在編譯時知道該地址,。但你可以在調(diào)用 VirtualAllocEx (為INJDATA)之后得到 INJDATA 在遠(yuǎn)程進程中的位置,。編譯我們的 NewProc 后,可以得到如下結(jié)果: 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.現(xiàn)在你可以象下面這樣繼續(xù):
何時使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)
到目前為止,,有幾個問題是我們未提及的,,現(xiàn)總結(jié)如下:
最后,有幾件事情一定要了然于心:你的注入代碼很容易摧毀目標(biāo)進程,,尤其是注入代碼本身出錯的時候,,所以要記住:權(quán)力帶來責(zé)任,! 附錄A: 為什么 kernel32.dll 和user32.dll 總是被映射到相同的地址,。
附錄B: /GZ 編譯器開關(guān) 在生成 Debug 版本時,,/GZ 編譯器特性是默認(rèn)打開的。你可以用它來捕獲某些錯誤(具體細(xì)節(jié)請參考相關(guān)文檔),。但對我們的可執(zhí)行程序意味著什么呢,?
靜態(tài)函數(shù)和增量鏈接 增量鏈接主要作用是在生成應(yīng)用程序時縮短鏈接時間。常規(guī)鏈接和增量鏈接的可執(zhí)行程序之間的差別是——增量鏈接時,,每個函數(shù)調(diào)用經(jīng)由一個額外的JMP指令,,該指令由鏈接器發(fā)出(該規(guī)則的一個例外是函數(shù)聲明為靜態(tài)),。這些 JMP 指令允許鏈接器在內(nèi)存中移動函數(shù),這種移動無需修改引用函數(shù)的 CALL指令,。但這些JMP指令也確實導(dǎo)致了一些問題:如 ThreadFunc 和 AfterThreadFunc 將指向JMP指令而不是實際的代碼,。所以當(dāng)計算ThreadFunc 的大小時: const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)你實際上計算的是指向 ThreadFunc 的JMPs 和AfterThreadFunc之間的“距離” (通常它們會緊挨著,不用考慮距離問題)?,F(xiàn)在假設(shè) ThreadFunc 的地址位于004014C0 而伴隨的 JMP指令位于 00401020,。 :00401020 jmp 004014C0 ... :004014C0 push EBP ; ThreadFunc 的實際地址 :004014C1 mov EBP, ESP ...那么 WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);將拷貝“JMP 004014C0”指令(以及隨后cbCodeSize范圍內(nèi)的所有指令)到遠(yuǎn)程進程——不是實際的 ThreadFunc。遠(yuǎn)程進程要執(zhí)行的第一件事情將是“JMP 004014C0” ,。它將會在其最后幾條指令當(dāng)中——遠(yuǎn)程進程和所有進程均如此,。但 JMP指令的這個“規(guī)則”也有例外。如果某個函數(shù)被聲明為靜態(tài)的,,它將會被直接調(diào)用,,即使增量鏈接也是如此。這就是為什么規(guī)則#4要將 ThreadFunc 和 AfterThreadFunc 聲明為靜態(tài)或禁用增量鏈接的緣故,。(有關(guān)增量鏈接的其它信息參見 Matt Pietrek的文章“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” ) 附錄D: 為什么 ThreadFunc的局部變量只有 4k,? 局部變量總是存儲在堆棧中,如果某個函數(shù)有256個字節(jié)的局部變量,,當(dāng)進入該函數(shù)時,,堆棧指針就減少256個字節(jié)(更精確地說,在函數(shù)開始處),。例如,,下面這個函數(shù): void Dummy(void) { BYTE var[256]; var[0] = 0; var[1] = 1; var[255] = 255; }編譯后的匯編如下: :00401000 push ebp :00401001 mov ebp, esp :00401003 sub esp, 00000100 ; change ESP as storage for ; local variables is needed :00401006 mov byte ptr [esp], 00 ; var[0] = 0; :0040100A mov byte ptr [esp+01], 01 ; var[1] = 1; :0040100F mov byte ptr [esp+FF], FF ; var[255] = 255; :00401017 mov esp, ebp ; restore stack pointer :00401019 pop ebp :0040101A ret注意上述例子中,堆棧指針是如何被修改的,?而如果某個函數(shù)需要4KB以上局部變量內(nèi)存空間又會怎么樣呢,?其實,堆棧指針并不是被直接修改,,而是通過另一個函數(shù)調(diào)用來修改的,。就是這個額外的函數(shù)調(diào)用使得我們的 ThreadFunc “被破壞”了,因為其遠(yuǎn)程拷貝會調(diào)用一個不存在的東西,。 我們看看文檔中對堆棧探測和 /Gs編譯器選項是怎么說的: ——“/GS是一個控制堆棧探測的高級特性,,堆棧探測是一系列編譯器插入到每個函數(shù)調(diào)用的代碼。當(dāng)函數(shù)被激活時,,堆棧探測需要的內(nèi)存空間來存儲相關(guān)函數(shù)的局部變量,。 如果函數(shù)需要的空間大于為局部變量分配的堆棧空間,,其堆棧探測被激活,。默認(rèn)的大小是一個頁面(在80x86處理器上4kb)。這個值允許在Win32 應(yīng)用程序和Windows NT虛擬內(nèi)存管理器之間進行謹(jǐn)慎調(diào)整以便增加運行時承諾給程序堆棧的內(nèi)存。” 我確信有人會問:文檔中的“……堆棧探測到一塊需要的內(nèi)存空間來存儲相關(guān)函數(shù)的局部變量……”那些編譯器選項(它們的描述)在你完全弄明白之前有時真的讓人氣憤,。例如,,如果某個函數(shù)需要12KB的局部變量存儲空間,堆棧內(nèi)存將進行如下方式的分配(更精確地說是“承諾” ),。 sub esp, 0x1000 ; "分配" 第一次 4 Kb test [esp], eax ; 承諾一個新頁內(nèi)存(如果還沒有承諾) sub esp, 0x1000 ; "分配" 第二次4 Kb test [esp], eax ; ... sub esp, 0x1000 test [esp], eax注意4KB堆棧指針是如何被修改的,,更重要的是,每一步之后堆棧底是如何被“觸及”(要經(jīng)過檢查),。這樣保證在“分配”(承諾)另一頁面之前,,當(dāng)前頁面承諾的范圍也包含堆棧底。 注意事項
為什么要將開關(guān)語句拆分成三個以上? 用下面這個例子很容易解釋這個問題,,假設(shè)有如下這么一個函數(shù): int Dummy( int arg1 ) { int ret =0; switch( arg1 ) { case 1: ret = 1; break; case 2: ret = 2; break; case 3: ret = 3; break; case 4: ret = 0xA0B0; break; } return ret; }編譯后變成下面這個樣子: 地址 操作碼/參數(shù) 解釋后的指令 -------------------------------------------------- ; arg1 -> ECX :00401000 8B4C2404 mov ecx, dword ptr [esp+04] :00401004 33C0 xor eax, eax ; EAX = 0 :00401006 49 dec ecx ; ECX -- :00401007 83F903 cmp ecx, 00000003 :0040100A 771E ja 0040102A ; JMP 到表***中的地址之一 ; 注意 ECX 包含的偏移 :0040100C FF248D2C104000 jmp dword ptr [4*ecx+0040102C] :00401013 B801000000 mov eax, 00000001 ; case 1: eax = 1; :00401018 C3 ret :00401019 B802000000 mov eax, 00000002 ; case 2: eax = 2; :0040101E C3 ret :0040101F B803000000 mov eax, 00000003 ; case 3: eax = 3; :00401024 C3 ret :00401025 B8B0A00000 mov eax, 0000A0B0 ; case 4: eax = 0xA0B0; :0040102A C3 ret :0040102B 90 nop ; 地址表*** :0040102C 13104000 DWORD 00401013 ; jump to case 1 :00401030 19104000 DWORD 00401019 ; jump to case 2 :00401034 1F104000 DWORD 0040101F ; jump to case 3 :00401038 25104000 DWORD 00401025 ; jump to case 4注意如何實現(xiàn)這個開關(guān)語句,? 與其單獨檢查每個CASE語句,不如創(chuàng)建一個地址表,,然后通過簡單地計算地址表的偏移量而跳轉(zhuǎn)到正確的CASE語句,。這實際上是一種改進。假設(shè)你有50個CASE語句,。如果不使用上述的技巧,,你得執(zhí)行50次 CMP和JMP指令來達(dá)到最后一個CASE。相反,有了地址表后,,你可以通過表查詢跳轉(zhuǎn)到任何CASE語句,,從計算機算法角度和時間復(fù)雜度看,我們用O(5)代替了O(2n)算法,。其中:
現(xiàn)在,,你也許認(rèn)為出現(xiàn)上述情況只是因為CASE常量被有意選擇為連續(xù)的(1,2,,3,,4)。幸運的是,,它的這個方案可以應(yīng)用于大多數(shù)現(xiàn)實例子中,,只有偏移量的計算稍微有些復(fù)雜。但有兩個例外:
顯然,單獨判斷每個的CASE常量的話,,結(jié)果代碼繁瑣耗時,,但使用CMP和JMP指令則使得結(jié)果代碼的執(zhí)行就像普通的if-else 語句。 現(xiàn)在回到問題,! 操作碼 指令 描述 FF /4 JMP r/m32 Jump near, absolute indirect, address given in r/m32 原來JMP 使用了一種絕對尋址方式,,也就是說,,它的操作數(shù)(CASE語句中的 0040102C)表示一個絕對地址。還用我說什么嗎,?遠(yuǎn)程 ThreadFunc 會盲目地認(rèn)為地址表中開關(guān)地址是 0040102C,,JMP到一個錯誤的地方,造成遠(yuǎn)程進程崩潰,。 附錄F:
為什么遠(yuǎn)程進程會崩潰呢,? 當(dāng)遠(yuǎn)程進程崩潰時,,它總是會因為下面這些原因:
不管哪種情況,,你都要小心翼翼地使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)。尤其要注意你的編譯器/鏈接器選項,,一不小心它們就會在 ThreadFunc 添加內(nèi)容,。
參考資料: |
|