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沒有攔截處理。
寫這個東西是我一時的興趣。希望有感興趣的人和我一同研究。