這是我發表在《黑客防線》2008年12期上的一篇文章,這里是網絡上的首發。
附件是我寫的一個工具“超級間諜(SuperSpy)”,主要包括一個窗口探測功能,甚至能夠探測到VC自帶的SPY++所不能探測到的窗口;當然,還包括這里提到的“托管注入”功能。
程序需運行在.Net2.0以上,如果需要使用注入功能,則需要.Net3.5以上。
超級間諜下載地址:http://download.csdn.net/source/850509
網上關于dll注入的文章實在太多,但基本上都是針對Win32 dll的,而很少涉及到托管dll。
首先讓我們來看看Win32 dll是如何注入的,通常有兩種方法:鉤子和遠程線程。而遠程線程更靈活,所以本文主要討論遠程線程的方法,為了便于交流,先明確以下概念:
1. 主程序:用于將dll注入到其它進程的exe
2. dll:被注入其它進程的dll
3. 宿主:dll將被注入的其它程序
前兩個程序都是我們自己寫的,而第3個是原來就有的。
遠程注入的基本步驟為:主程序通過CreateRemoteThread函數迫使宿主調用LoadLibrary函數加載dll,從而執行dll的入口函數DllMain,只要我們把代碼放到DllMain里,就可以被調用了。
現在我們來看托管dll的注入:
用C#或VB.NET寫的dll沒有DllMain函數,我們自然想到了功能強大的C++。通常,我們把用C#或VB.NET寫的dll,或者用C++寫的,但編譯為/clr:pure的dll稱為托管dll
而把用C++編寫的,但編譯為/clr的dll稱為混合dll。
混合dll也可以調用托管代碼,所以也可以將其稱為托管dll,本文所說的托管dll注入,實際上是混合dll的注入。
我們首先想到的是用常規方法來注入混合dll,結果會發現:只要在DllMain函數里調用了托管代碼,程序就會崩潰。
也許你還會想到下面的方法:
定義一個類,在其構造函數里調用托管代碼,然后在全局域里定義這個類的一個變量,當我們這樣做了后會發現,注入后,什么也沒有執行。
查閱MSDN,我們找到了答案:
DllMain不能直接或間接地調用托管代碼,并且全局變量不會進行初始化。
這樣的結果讓人非常沮喪!
難道真的就沒有辦法了嗎?
網友CiCi給出了一個解決方案:
寫一個混合dll,在其中定義一個導出函數,在這個導出函數里可以調用托管代碼。
然后寫一個非托管dll(也就是Win32 dll),在其DllMain函數里調用前面那個混合dll的導出函數。
這個方案能夠解決問題,并且我在一段時間里,也一直使用這個方法。但總覺得不完美:必須使用兩個dll。
有沒有辦法用一個dll就解決問題呢?答案是肯定的。
我在一次偶然的機會中,發現了一個現象:
如里用鉤子來實現注入,則全局變量會得到初始化。這個發現讓我非常困惑:為什么遠程線程注入就不會初始化呢?
我考慮這兩種方法的區別:鉤子注入時,宿主會調用dll中的鉤子回調函數。
于是我大膽設想,只要宿主調用了dll中的任意一個函數,全局變量就會得到初始化。
于是我在dll中定義了一個空實現的函數(因為我的目地是迫使全局變量初始化,而不是去執行這個函數)
結果正如我所料,全局變量被初始化了,其構造函數中的托管代碼被調用了!
這里其實已經實現了托管代碼的注入,當然,還遠遠不夠完善。
這時候,我又覺得全局變量是多余的了:既然我能夠使宿主調用dll中的一個特定函數,為什么不直接把托管代碼放到這個函數中呢?
我馬上進行了測試,也成功了。
之前我是這樣實現讓宿主調用dll中的一個特定函數的:
在dll被注入之后,我在DllMain函數里將這個特定函數的地址寫到共享內存中(這時DllMain里沒有調用托管代碼,所以可以執行),然后主程序讀取共享內存中的值,再通過遠程線程迫使宿主調用這個特定函數。
于是我又想,既然可以在主程序中用遠程線程迫使宿主調用dll中的特定函數,為什么不直接在dll中用相同的方法去調用呢?
我還沒有來得及驗證,馬上又想到:dll跟宿主是一個進程,即使用遠程線程,傳給CreateRomoteThread函數的第一個參數也是-1(GetCurrentProcess()返回-1),為什么不直接用“近程”線程CreateThread呢?
需要著重強調地是:我當時用CreateThread的目的只是讓它調用一個特定函數,而不是要去創建一個線程,雖然最終的確創建了一個線程,不過對于此目的,貌似只是一個副作用。
我非常興奮,馬上進行了驗證,成功了!
現在我來總結一下托管代碼注入的過程:
首先定義一個線程回調函數,可以在其中調用托管代碼:
DWORD CALLBACK ThreadProc(LPVOID lp)
{
//可以在此調用托管代碼
return 0;
}
然后在dll的入口函數DllMain里調用CreateThread函數:
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateThread(0,0,ThreadProc,0,0,0);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
這樣就實現了托管代碼的注入。
深入話題:
通常我們會有這樣的要求:在主程序中單擊某個按扭,使宿主執行dll中的一段代碼,多次單擊就多次執行。
上面的代碼無法實現這個目地,因為只有dll剛被注入到宿主時,DllMain中的DLL_PROCESS_ATTACH才會收到通知,我們自然想到:每次執行后,卸載掉dll,下次單擊后,DLL_PROCESS_ATTACH又會收到通知;遺憾的是,目前我還沒有辦法卸載掉這個dll。
對于非托管dll,我們可以通過FreeLibrary來卸載(如果是在dll內部卸載,還必須借助FreeLibraryAndExitThread函數,關于dll的自卸載,由于沒有在這個程序中使用,這里就不做介紹了)
而對于托管dll,查閱MSDN,我們得到的結論是:.Net不支持dll的卸掉,dll只能隨著應用程序域的卸載而卸載。
對于混合dll,應該如何卸載呢?
我嘗試用Win32 dll的方式卸載,結果也“真的”卸載了,這是因為:
1. DllMain中的DLL_PROCESS_DETACH收到了通知
2. 用模塊查看工具去看,宿主中確實不存在注入的dll了
但事實上是失敗了,至少有兩個問題:
1. 關閉宿主時,會提示出錯
2. 再次注入時,雖然DLL_PROCESS_ATTACH會收到通知,但用CreateThread創建的新線程并不被執行
既然MSDN上都說無法卸載托管dll,那這個問題就擱淺了吧。
雖然不能卸載這個dll,但并不是說就不能實現前面提到的問題。
實際上,每次主程序通過CreateRemoteThread函數在宿主創建線程時,DllMain中的DLL_THREAD_ATTACH和DLL_THREAD_DETACH也會收到通知,當然最好不要在這里直接調用CreateThread,這是因為:
1. 如果不做處理,直接在DLL_THREAD_ATTACH中調中CreateThread,顯然會造成死循環:每產生一個線程,DLL_THREAD_ATTACH就會收到通知,然后又去創建線程,它又會得到通知……
2. 雖然多次單擊按扭注入時,DLL_THREAD_ATTACH都會得到通知;但反過來,得到了通知,不意味著就是遠程注入:一個最明顯的情況是,對CreateThread的調用就會導致DLL_THREAD_ATTACH收到通知,但這并非遠程注入
我的解決辦法是:每次主程序調用CreateRemoteThread迫使宿主調用LoadLibrary加載dll的時候,都會使模塊計數器加1,根據模塊計數器的值的變化,就可以確定是否是遠程注入了。
示例程序說明:
示例程序包括3個文件:
1. SuperSpy.exe 用C#寫的主程序
2. Invoke.dll 用C++寫的dll,也只能用C++來寫
3. PropertyControl.dll 用VB.NET寫的插件,它不是必需的
除了Invoke.dll必須用C++外,其余兩個可以用任意語言寫。
你也許會感到奇怪,為什么有兩個dll?
其中PropertyControl.dll是一個插件,你完全可以把所有的代碼都放到Invoke.dll中,之所以用插件的形式,只是為了方便大家編寫自已的插件,插件的規范是,在任意一個類中實現以下成員(你可以用你習慣的語言來編寫這個插件):
public static void Inject();
public static string Description
{
get;
}
兩個成員都必須是公共、靜態的,其中Inject方法是注入后要調用的方法;Description屬性是用于描述插件作用的。
我自已實現的插件的功能是:查詢和編輯一個托管程序中所有窗口(包括控件)的屬性,這是一個很強大的功能,我舉兩個例子:
1. 星號密碼查看。由于已經注入,本來顯示為星號的密碼,可以直接通過Text屬性獲得
2. 灰色按扭突破。只需將窗口的Enabled屬性設置成true即可
以往要實現這兩個功能,都需要分別編寫相關的程序,而現在僅僅是這個插件的一個簡單應用。而且,你可以編寫自己的插件,來實現自己的要求。