向其他進程注入代碼的三種方法

 

作者:Robert Kuster
翻譯:袁曉輝
摘要:如何向其他線程的地址空間中注入代碼並在這個線程的上下文中執行之。

目錄:
●導言
●Windows 鉤子(Hooks)
●CreateRemoteThread 和LoadLibrary 技術
    ○進程間通訊
●CreateRemoteThread 和 WriteProcessmemory 技術
    ○如何使用該技術子類(SubClass)其他進程中的控件
    ○什麼情況下適合使用該技術
●寫在最後的話
●附錄
●參考
●文章歷史

導言:
     我們在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的大小,這個也許會更好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章