眾所周知,bo2k可以在一個指定的進程空間(比如explorer.exe進程)做為一個線程運行。本文試圖找出一種方法,使得任意exe都可以在其他進程中以線程運行(當然,這里說的"任意"是有條件的,下面會講到)。

  為行文簡單起見,我把先加載的exe稱為宿主,后加載的exe稱為客戶。對于上面的例子,explorer.exe為宿主,bo2k.exe為客戶。

基本知識

  每一個exe都有一個缺省加載基址,一般都是0x400000。如果實際加載基址和缺省基址相同,程序中的重定位表就不需要修正(fixup),否則,就必須修正重定位表;

  如果一個程序沒有重定位表,而且如果程序不能在缺省基址處加載,那么程序將不能運行。舉個例子,Windows95的最低加載基址是 0x400000,你在Windows NT上開發了一個exe,指定其加載基址為0x10000,如果連接時讓連接器剝離重定位表,那么他將無法在Windows95下運行。

  bo2k為了避免和普通程序沖突,選了一個極其特殊的基址:0x03140000,這個地址一般不會有程序用到。這樣bo2k啟動 后,用WriteProcessMemory將自身復制到宿主進程的0x03140000地址處,再用CreateRemoteThread遠程啟動一個 線程,從入口點開始執行。

  bo2k能夠在其他進程空間正常運行,關鍵有兩點:

  1)實際加載基址和缺省基址相同,這樣就無需修正重定位表。

  2)與bo2k隱性聯接(implicitly link)的動態聯接庫在目標進程中的加載基址和bo2k啟動時的加載基址一致,這樣就無需修改導入函數表。除非只用到ntdll.dll和kernel32.dll兩個dll,

  否則這點很難保證。bo2k的解決辦法是,遠程運行的代碼不用隱性調用,所有用到API都在遠程代碼運行后再動態確定(用LoadLibrary和GetProcAddress)

  我的目標是讓"所有"的程序都能在其他進程空間跑。在這里,"所有"的含義是所有那些"重定位表沒有被剝離"的32位pe格式的可執行程序。

  對于Visual

  對于一般的程序,上面兩點都很難滿足:

  1)絕大多數程序的加載基址都是0x400000,這樣,客戶exe就很難保證加載到其缺省基址。解決辦法只能是修正重定位表。如果,很不幸,這個exe的重定位表被剝離,這個exe就沒法在其他進程空間跑。

  對于Visual

  2)很多程序都用隱性聯接調用Windows API,而只用到kernel32.dll導出API的程序很少,因此這一點也很難保證。解決辦法是重填導入表(import table)。

  另外,對于有界面的程序,光修正重定位表和導入表還不夠。因為他們都會直接或間接用到GetModuleHandle和LoadResource這些函數

  GetModuleHandle有個特點,如果傳遞給他的ModuleName為NULL,則返回宿主exe的模塊句柄。LoadResource也類似,如果傳遞給他的模塊句柄為NULL,則認為是宿主exe模塊,類似的API還有一些,不一一列舉。

  客戶exe調用這些API顯然會得到錯誤的結果。因此必須截獲這些API做特殊處理。?

  綜合上面分析,要讓兩個程序共享一份進程空間,要做的工作有:

1)打開進程邊界:用WriteProcessMemory向宿主進程注入代碼,用CreateRemoteThread啟動遠程代碼;

2)在遠程代碼中,加載客戶exe,必要時修正重定位表和填充dll導入表。

3)截獲GetModuleHandle,LoadResource等API,在客戶exe以缺省參數調用時返回客戶exe的模塊句柄,而不是宿主句柄。?

  根據以上思路,我寫了remote.dll,導出三個函數:RemoteRunA,RemoteRunW,和RemoteCall。

原型分別為:

BOOL WINAPI RemoteRunA( DWORD processId, LPCSTR lpszAppPath, LPCSTR lpszCmdLine, int nCmdShow );

BOOL WINAPI RemoteRunW( DWORD processId, LPCWSTR lpszAppPath, LPCWSTR lpszCmdLine, int nCmdShow );

BOOL WINAPI RemoteCall( DWORD processId, PVOID pfnAddr, PVOID pParam, DWORD cbParamSize, BOOL fSyncronize );

RemoteRunA用于在宿主進程中加載執行客戶exe;

RemoteRunW是RemoteRunA的unicode版本;

RemoteCall實現遠程注入并運行代碼。

調用例子:

  假如宿主exe為Depends.exe(我經常使用的宿主進程),pid為136。客戶exe為"C:\WINNT\ system32\CALC.EXE",RemoteRunA( 136, "C:\\WINNT\\system32\\CALC.EXE", NULL, SW_SHOW );

或,

RemoteRunW( 136, L"C:\\WINNT\\system32\\CALC.EXE", NULL, SW_SHOW );

  RemoteCall是一個很cool的副產品,可以在任意宿主進程運行一系列你自己精心準備的代碼。

  遠程代碼無需特殊處理,就像在本地調用一樣。RemoteCall支持很多特性:

  可以對Windows API進行隱性調用(無需用LoadLibrary和GetProcAddress動態確定)

可以使用全局/靜態變量(除了不能動態初始化);

可以使用編譯時數據,特別是字符串常量;

支持異常處理;

支持源碼級調試;

支持同步、異步調用;

對于同步調用,可以取得返回結果和錯誤號;

對遠程代碼做了異常保護,代碼執行錯誤不會使宿主進程崩潰。

RemoteCall的唯一缺點是效率不高(當然,還有一個缺點,你的exe必須是可重定位的)。

調用例子:

  在Windows 2000中,對有密碼保護風格的Edit control調用SendMessage(hwnd, WM_GETTEXT,...)試圖得到密碼內容時,系統會檢查調用SendMessage的進程和Edit control所在的進程是否相同,不同則返回空字符串,調用失敗。

  解決辦法顯然應該是在目標進程中調用SendMessage。

利用RemoteCall,可以很容易地實現:

typedef struct _tagGETPASS {

?HWND hwndPassword; // in

?char szPassText[1024]; // out

}GETPASS;

static int *_p = NULL;

BOOL NullFunction() {

// 可以用靜態變量和異常保護。

?__try {

?*_p = 0;

?}__except(EXCEPTION_EXECUTE_HANDLER){}

?return TRUE;

}

// 準備在遠程運行的代碼

BOOL WINAPI RemoteGetPasswordText( GETPASS* pgp ) {

// 可以使用相對調用(near call),沒什么用,演示一下

?NullFunction();

// 隱性調用Windows API

?if( SendMessageA( pgp->hwndPassword, WM_GETTEXT, sizeof(pgp->szPassText)-1, (LPARAM)pgp->szPassText ) ) ) {

?MessageBoxA( NULL,

?pgp->szPassText,

?"Great!!", // 可以使用字符串常量

?MB_OK );

?return TRUE;

?}

?return FALSE;

}

void GetPasswordText( HWND hwnd ) {

?GETPASS gp;

?gp.hwndPassword = hwnd;

?DWORD processId;

?GetWindowThreadProcessId( hwnd, &processId );

?HMODULE hLib = ::LoadLibrary( "remote.dll" );

?if( hLib != NULL ) {

?typedef BOOL (WINAPI *PFN_RemoteCall)( DWORD processId, PVOID pfnAddr, PVOID pParam, DWORD cbParamSize, BOOL fSyncronize );

?PFN_RemoteCall fnRemoteCall = (PFN_RemoteCall)::GetProcAddress( hLib, "RemoteCall" );

?if( fnRemoteCall != NULL ) {

?if( fnRemoteCall( processId, RemoteGetPasswordText, &gp, sizeof(gp), TRUE ) )

?MessageBoxA( NULL, gp.szPassText, "we get the password!!", MB_OK );

?}

?::FreeLibrary( hLib );

?}

}

RemoteRun的調用例子:

void PrintUsage() {

?printf( "\tUsage: rmExe <target process id> <Exe file path>\n" );

}

int main(int argc, char* argv[]) {

?if( argc <= 2) {

?PrintUsage();

?return -1;

?}

?int pid = atoi( argv[1] );

?if( pid != 0 ) {

?HMODULE hRemote = ::LoadLibrary( "remote.dll" );

?if( hRemote != NULL ) {

?typedef DWORD (WINAPI *PFN_RemoteRun)( DWORD processId, LPCSTR lpszAppPath, LPSTR lpszCmdLine, int nCmdShow);

?PFN_RemoteRun fnRemoteRun = (PFN_RemoteRun)::GetProcAddress( hRemote, "RemoteRunA" );

?if( fnRemoteRun != NULL )

?fnRemoteRun( pid, argv[2], NULL, SW_SHOW );

?FreeLibrary( hRemote );

?}

?}

?return 0;

}

應該注意的問題

1)最困難的部分是加載客戶exe,簡單的調用LoadLibrary根本不能解決問題,他不會替你修改重定位表和導入表。

另外對于.tls section(用于支持線程本地存儲)和.bss section(用于為初始化數據),我目前還不是很清楚如何處理;希望有人和我一起探討;

2)目前remote.dll還不能支持在一個進程空間運行三個或更多程序。問題出在我在remote.dll中維護著一個客戶exe的thread列表,

用于判斷誰調用了GetModuleHandle等API,目前只能處理一個客戶exe。這個問題不難解決;

3)有一些工具可以查看進程中加載的模塊列表,如果想做進程徹底隱藏,不想讓這些工具檢測到我們的模塊,在我看來,至少有兩種解決辦法:

  一,不用LoadLibrary,自己寫LoadDLL,這看起來似乎很困難,幸運的是,在bo2k的源代碼中提供了一套這樣的工具(在dll_load.cpp中實現)。

  remote.dll中修改重定位表和導入表基本上用的都是dll_load.cpp里的代碼。值得注意的是, dll_load.cpp原來的實現中有一點bug,他不能正確處理有Borland的tlink32生成的exe。具體原因請仔細閱讀Matt pietrek的"Windows 95 system programming secrets",或msdn文章:"Peering Inside the PE: A Tour of the Win32 Portable Executable File Format",里面講到了ms linker和borland linker的區別。

  二,我自己實現了一種模塊剝離技術,可以讓進程脫離.exe文件和.dll文件運行。其思想是先對要剝離的exe或dll模塊的所有數據做好備份,然后用FreeLibrary或者UnmapViewOfFile卸掉模塊,

  再把備份的模塊數據恢復回來。我以前在csdn上貼過代碼的,自己找吧。

  4)截獲API用的是MS Detours Package 1.3。我不打算附上它的源代碼,自己去下載吧:http://research.microsoft.com/sn/detours

5)在截獲API時必須掛起其他線程。我用了兩個未公開的接口:NtQuerySystemInformation用于枚舉線程;NtOpenThread用于得到線程句柄。

  推薦一本工具書:"Windows NT Native API reference"(中文譯名為"Windows NT 本機API參考"),書名大致如此,不必深究。氣人的是居然把Navtive翻為本機,I 服了you。書中列出了很多Native API的原型及其用到的

運行成功的例子:

  在Depends.exe進程中運行Calc.exe;

  在Depends.exe進程中運行Acrobat 5.0;

  在Depends.exe進程中運行Microsoft Visio 2000;

  在Depends.exe進程中運行Process Hacker(我自己寫的一個進程查看工具),用了很多低層接口

  在Process Hacker進程中運行Acrobat 5.0。

  唯一失敗的例子是以客戶身份運行matlab 5.1。這個可執行文件很特殊,有多個code section和data section,還有.tls section和.bss section。

  失敗原因不是很清楚(主要是沒有足夠的時間研究),可能是.tls和.bss section在加載時沒有處理好;也可能是某個應該做特殊處理的API沒有攔截處理。

  寫這個東西是我一時的興趣。希望有感興趣的人和我一同研究。