windows内的各個程序有各自的位址空間。它們互相獨立互不幹擾保證了系統的安全性。但是windows也為調試器或是其他工具設計了一些函數,這些函數可以讓一個程序對另一個程序進行操作。雖然他們是為調試器設計的,但是任何應用程式都可以調用它們 。接下來我們來談談使用遠端線程來注入DLL。
從根本上說,DLL注入就是将某一DLL注入到某一程序的位址空間。該程序中的一個線程調用LoadLibrary來載入想要注入的DLL。由于我們不能直接控制其他程序内的線程,是以我們必須在其他程序内建立一個我們自己的線程。我們可以對新建立的線程加以控制,讓他調用LoadLibrary來載入DLL。windows提供了一個函數,可以讓我們在其他程序内建立一個線程:
在其他程序内建立的線程被稱為:遠端線程,該程序被稱為遠端程序。
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> HANDLE WINAPI CreateRemoteThread(
- __in HANDLE hProcess,
- __in LPSECURITY_ATTRIBUTES lpThreadAttributes,
- __in SIZE_T dwStackSize,
- __in LPTHREAD_START_ROUTINE lpStartAddress,
- __in LPVOID lpParameter,
- __in DWORD dwCreationFlags,
- __out LPDWORD lpThreadId
- );
- </span>
<span style="font-size: 18px;"> HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId
);
</span>
很容易吧。該函數除了第一個參數hProcess,辨別要建立的線程所屬的程序外,其他參數與CreateThread的參數完全相同。
參數lpstartAddress是線程函數的位址。由于是在遠端程序建立的,是以該函數一定必須在遠端程序的位址空間内。
現在知道了如何在另一個程序建立一個線程,那麼我們如何讓該線程載入我們的DLL呢?
先别急着讓線程調用LoadLibrary載入DLL,現在要考慮的是如何讓線程運作起來,即為線程選擇線程函數。因為線程是在其他程序内運作的,是以該線程函數必須符合以下條件:
1:該函數符合線程函數的原型,
2:存在于遠端線程位址空間内。
仔細分析下,遠端線程的任務隻有一個。就是調用LoadLibray加載DLL。
既然如此可不可以讓LoadLibrary直接作為線程函數呢?
先看第一個條件:函數簽名是否相同。你還别說,除了參數類型有點不一樣外,其他一摸一樣的。由于參數類型可以通過強轉實作,是以第一個條件是滿足的。
再看第二個條件:該函數是否在遠端程序位址空間内。用屁股想一下我們都知道肯定在。另外他們都有相同的函數調用約定,也就是說他們的參數傳遞是從右到左壓棧的,有子程式平衡堆棧。OK,太棒了。使用LoadLibrary作為線程函數真的是太友善了 。
難道是微軟故意為我們這樣設計的?無從知曉。但在這裡要謝謝發現這一技巧的牛人。
檢視MSDN可以發現LoadLibrary并不是一個API,它其實是一個宏。
在WinBase.h可以發現這樣一句話:
#ifdef UNICODE
#define LoadLibrary LoadLibraryW
#else
#define LoadLibrary LoadLibraryA
#endif
明白了嗎?實際上有兩個Load*函數,他們的唯一差別就是參數類型不同。如果DLL檔案名是以ANSI形式儲存的,我們就必須調用LoadLibraryA,如果是UNICODE形式儲存的我們就必須調用LoadLibraryW。
接下來我們要做的事情就簡單了,隻需要調用CreateThread函數,傳給辨別線程函數的參數LoadLibraryA或是LoadLibraryW。然後将我們要遠端程序加載的DLL的路徑名的位址作為參數傳給它。哈哈,很興奮吧!一切都是那麼的順!
不要高興的太早。你就沒發現哪有不對的地方嗎?傳給線程函數的參數是DLL路徑名的位址。但是該位址是在我們進城内的。如果遠端程序引用此位址的資料,很可能會導緻通路違規,遠端程序被終止。怎麼樣很嚴重吧。但這也給我們一個破壞其他程序的思路。哈哈。自己發揮吧!
為了解決這個問題,我們應該将該字元串放到遠端位址的位址空間去。有沒有相應的函數呢?當然有!
首先應該在遠端程序的位址空間配置設定一塊兒記憶體。如何做呢!或許你很熟悉VirtualAlloc,但是他沒有這個功能。他兄弟VirtualAllocEx可以解決這個問題。看原型:
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> LPVOID WINAPI VirtualAllocEx(
- __in HANDLE hProcess,
- __in_opt LPVOID lpAddress,
- __in SIZE_T dwSize,
- __in DWORD flAllocationType,
- __in DWORD flProtect
- );
- </span>
<span style="font-size: 18px;"> LPVOID WINAPI VirtualAllocEx(
__in HANDLE hProcess,
__in_opt LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flAllocationType,
__in DWORD flProtect
);
</span>
hProcess應該知道是幹嘛的吧。他就是辨別你要想在那個程序的位址空間申請記憶體的程序句柄。其他參數跟VirtualAlloc完全相同。此處不再介紹。
當然知道如何申請還有知道如何釋放!看他搭檔:VirtualFreeEx
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> BOOL WINAPI VirtualFreeEx(
- __in HANDLE hProcess,
- __in LPVOID lpAddress,
- __in SIZE_T dwSize,
- __in DWORD dwFreeType
- );
- </span>
<span style="font-size: 18px;"> BOOL WINAPI VirtualFreeEx(
__in HANDLE hProcess,
__in LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD dwFreeType
);
</span>
與VirtualFree的差別這隻是多一個程序句柄。
現在申請空間的任務完成了,要怎麼樣将本程序的資料複制到另外一個程序呢?可以使用ReadProcessMemory和WriteProcessMemory
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> BOOL WINAPI ReadProcessMemory(
- __in HANDLE hProcess,
- __in LPCVOID lpBaseAddress,
- __out LPVOID lpBuffer,
- __in SIZE_T nSize,
- __out SIZE_T *lpNumberOfBytesRead
- );
- </span>
<span style="font-size: 18px;"> BOOL WINAPI ReadProcessMemory(
__in HANDLE hProcess,
__in LPCVOID lpBaseAddress,
__out LPVOID lpBuffer,
__in SIZE_T nSize,
__out SIZE_T *lpNumberOfBytesRead
);
</span>
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> BOOL WINAPI WriteProcessMemory(
- __in HANDLE hProcess,
- __in LPVOID lpBaseAddress,
- __in LPCVOID lpBuffer,
- __in SIZE_T nSize,
- __out SIZE_T *lpNumberOfBytesWritten
- );
- </span>
<span style="font-size: 18px;"> BOOL WINAPI WriteProcessMemory(
__in HANDLE hProcess,
__in LPVOID lpBaseAddress,
__in LPCVOID lpBuffer,
__in SIZE_T nSize,
__out SIZE_T *lpNumberOfBytesWritten
);
</span>
由于他們簽名類似,此處放在一塊介紹。
hProcess是用來辨別遠端程序的。
lpBaseAddress是在遠端程序位址空間的位址,是VirtualAllocEx的傳回值。
lpBuffer是在本程序的記憶體位址。此處也就是DLL路徑名的位址。
nSize為要傳輸的字元串。
lpNumberOfByteRead和lpNumberOfByteWrite為實際傳輸的位元組數。
注意:當調用WriteProcessMemory時有時會導緻失敗。此時可以嘗試調用VirtualProtect來修改寫入頁面的屬性,寫入之後再改回來。
到此為止,看起來沒啥東西了,但是還有一個比較隐晦的問題,如果不對PE檔案格式和DLL加載的方式有所了解的話是很難發現的。
我們知道導入函數的真實位址是在DLL加載的時候獲得的。加載程式從導入表取得每一個導入函數的函數名(字元串),然後在被加載到程序位址空間的DLL中查詢之後,填到導入表的相應位置(IAT)的。也就是說在運作之前我們并不知道導入函數的位址(當然子產品綁定過得除外)。那麼程式代碼中是如何表示對導入函數的調用呢?有沒有想過這個問題呢。
你或許覺得應該是:CALL DWORD PTR[004020108] ( [ ]内僅表示導入函數位址,無實際意義)。
由于程式的代碼在經過編譯連接配接之後就已經确定,而導入表的位址如00402010是在程式運作的時候獲得的。是以程式在調用導入函數的時候并不能這樣實作。那到底是如何實作的呢?
[ ]内有一個确定的位址這是毋庸置疑的,但是他的值并不是導入函數的位址,而是一個子程式的位址。該子程式被稱為轉換函數(thunk)。這些轉換函數用來跳轉到導入函數。當程式調用導入函數時,先會調用轉換函數,轉換函數從導入表的IAT獲得導入函數的真實位址時在調用相應位址。
是以對導入函數的調用形如如下的形式:
[cpp] view plain copy print ?
- CALL 00401164 ;轉換函數的位址。
- 。。。。。。
- :00401164
- 。。。。。
- CALL DWORD PTR [00402010] ;調用導入函數。
CALL 00401164 ;轉換函數的位址。
。。。。。。
:00401164
。。。。。
CALL DWORD PTR [00402010] ;調用導入函數。
分析到這兒,我們也可以明白為什麼在聲明一個導出函數的時候要加上_decllpec(dllimport)字首。
原因是:編譯器無法區分應用程式是對一般函數的調用還是對導入函數的調用。當我們在一個函數前加上此字首就是告訴編譯器此函數來自導入函數,編譯器就會産生如上的指令。而不是CALL XXXXXXXX的形式。
是以在寫一個輸出函數的時候一定要在函數聲明前加上修飾符:_decllpec(dllimport)。
言歸正傳.之是以說這麼多,就是因為我們傳給CreateRemoteThread的線程函數LoadLibrary*,會被解析成我們程序内的轉換函數的位址。如果把這個轉換函數的位址作為線程函數的起始位址很可能導緻通路違規。解決方法是:強制代碼略過轉換函數而直接調用LoadLibrary*.
這可以通過GetProAddress來實作。
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> FARPROC WINAPI GetProcAddress(
- __in HMODULE hModule,
- __in LPCSTR lpProcName
- );
- </span>
<span style="font-size: 18px;"> FARPROC WINAPI GetProcAddress(
__in HMODULE hModule,
__in LPCSTR lpProcName
);
</span>
hModule是子產品句柄。标志某一子產品。
lpProcName是該子產品内某一函數的函數名。
它傳回該函數在子產品所屬程序位址空間的位址。
如GetProcAddress(GetModuleHandle("Kernel.dll","LoadLibraryW"));
此語句取得LoadLibrary在Kernel.dll所在程序空間的真實位址。注意此時僅僅是取得在本程序Kernel.dll的位址和LoadLibraryW的位址。難道在遠端程序内也是一樣嗎?
《windows核心程式設計》第五版 589頁第三段中說,”從作者的經驗來看,Kernel.dll映射到每個程序的位址都是相同的。“基于此,我們可以認為,我們調用此語句是取得了Kernel.dll和LoadLibraryW在遠端位址空間的位址。
下面來介紹一個例子。通過遠端線程向explorer.exe程序注入DLL。
explorer.exe:資料總管程序。随系統啟動而啟動,且一直運作。是以它經常被用來被當做遠端線程的寄主。
步驟:
1:獲得explorer程序的句柄。
這可以通過調用CreatehlpSnapshot,獲得此時系統的一個快照。然後周遊該快照。找到程序名稱為explorer.exe的程序。并得到起程序對象句柄。
看代碼:
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> PROCESSENTRY32 pe32;
- pe32.dwSize=sizeof(pe32);
- HANDLE hSnapshot=CreateToolhelp32Snapshot(TH32CS_SNAPALL,0);
- int ret=Process32First(hSnapshot,&pe32);
- CString a;
- UpdateData();
- if(-1==m_processToFind.Find(".exe",0))
- m_processToFind+=".exe";
- while(ret)
- {
- if(pe32.szExeFile==m_processToFind)
- {
- a.Format("程序:%s找到,它的程序ID為:%d",m_processToFind,pe32.th32ProcessID);
- MessageBox(a);
- break ;
- }
- ret=Process32Next(hSnapshot,&pe32);
- }</span>
<span style="font-size: 18px;"> PROCESSENTRY32 pe32;
pe32.dwSize=sizeof(pe32);
HANDLE hSnapshot=CreateToolhelp32Snapshot(TH32CS_SNAPALL,0);
int ret=Process32First(hSnapshot,&pe32);
CString a;
UpdateData();
if(-1==m_processToFind.Find(".exe",0))
m_processToFind+=".exe";
while(ret)
{
if(pe32.szExeFile==m_processToFind)
{
a.Format("程序:%s找到,它的程序ID為:%d",m_processToFind,pe32.th32ProcessID);
MessageBox(a);
break ;
}
ret=Process32Next(hSnapshot,&pe32);
}</span>
[cpp] view plain copy print ?
- <span style="font-size: 18px;">HANDLE WINAPI CreateToolhelp32Snapshot(
- __in DWORD dwFlags,
- __in DWORD th32ProcessID
- );
- </span>
<span style="font-size: 18px;">HANDLE WINAPI CreateToolhelp32Snapshot( __in DWORD dwFlags, __in DWORD th32ProcessID ); </span>
該函數用于擷取指定程序的快照,以及該程序使用的堆,線程等。
dwFlags用來表示此快照中包含的項目。具體參考MSDN。
此處傳入TH32CS_SNAPALL,表示此快照包括系統中所有的程序和線程,以及在th32ProcessID中指定的程序的各子產品和線程的資訊。
th32ProessID指定要包括到此快照的程序ID,當傳入0時表示目前程序。
執行成功傳回快照句柄。否則傳回INVALID_HANDLE_VALUE。可以調用GetLastError檢視更多錯誤資訊。
[cpp] view plain copy print ?
- <span style="font-size: 18px;">BOOL WINAPI Process32First(
- __in HANDLE hSnapshot,
- __inout LPPROCESSENTRY32 lppe
- );
- </span>
<span style="font-size: 18px;">BOOL WINAPI Process32First( __in HANDLE hSnapshot, __inout LPPROCESSENTRY32 lppe ); </span>
BOOL WINAPI Process32Next( __in HANDLE hSnapshot, __out LPPROCESSENTRY32 lppe );
以上兩個函數,用于在CreateHlpSnapshot中周遊各項。用法請參考上例,其他資訊請參考MSDN。
2:獲得explorer的程序ID之後,還要調用OpenProcess來獲得該程序的句柄。函數執行成功傳回程序句柄。
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,0,pe32.th32ProcessID);</span>
<span style="font-size: 18px;"> HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,0,pe32.th32ProcessID);</span>
3:在explorer的位址空間中申請存儲要注入的DLL的路徑名的空間。
[cpp] view plain copy print ?
- <span style="font-size: 18px;"> PVOID addr=VirtualAllocEx(hProcess,NULL,50,MEM_COMMIT, PAGE_READWRITE);
- if(addr==NULL)
- {
- CString a;
- int ret=GetLastError();
- a.Format("在遠端程序申請空間失敗!錯誤碼為:%d",ret);
- MessageBox(a);
- }
- else
- {
- MessageBox("遠端程序位址空間中申請空間成功!");
- } </span>
<span style="font-size: 18px;"> PVOID addr=VirtualAllocEx(hProcess,NULL,50,MEM_COMMIT, PAGE_READWRITE);
if(addr==NULL)
{
CString a;
int ret=GetLastError();
a.Format("在遠端程序申請空間失敗!錯誤碼為:%d",ret);
MessageBox(a);
}
else
{
MessageBox("遠端程序位址空間中申請空間成功!");
} </span>
4:将路徑名寫入在explorer程序申請的空間。
[cpp] view plain copy print ?
- <span style="font-size: 18px;">char path[50]="F:\\injectDll.dll";
- int retval=WriteProcessMemory(hProcess,addr,(LPVOID)path,sizeof(path),NULL);
- if(retval)
- {
- MessageBox("寫入成功!");
- }
- else
- MessageBox("寫入失敗!");
- </span>
<span style="font-size: 18px;">char path[50]="F:\\injectDll.dll";
int retval=WriteProcessMemory(hProcess,addr,(LPVOID)path,sizeof(path),NULL);
if(retval)
{
MessageBox("寫入成功!");
}
else
MessageBox("寫入失敗!");
</span>
5:建立遠端線程。
//獲得LoadLibraryA在遠端程序中的位址。(與本程序的位址相同。)
[cpp] view plain copy print ?
- <span style="font-size: 18px;">PTHREAD_START_ROUTINE pfnThread=(PTHREAD_START_ROUTINE)
- GetProcAddress(GetModuleHandle("kernel32.DLL"),"LoadLibraryA");</span>
<span style="font-size: 18px;">PTHREAD_START_ROUTINE pfnThread=(PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle("kernel32.DLL"),"LoadLibraryA");</span>
[cpp] view plain copy print ?
- <span style="font-size: 18px;">HANDLE hRemoteThread=CreateRemoteThread(
- hProcess,//in HANDLE hProcess,
- NULL,
- 0,//__in SIZE_T dwStackSize,
- pfnThread,
- addr,
- 0,
- NULL); </span>
[cpp] view plain copy print ?<span style="font-size: 18px;">HANDLE hRemoteThread=CreateRemoteThread( hProcess,//in HANDLE hProcess, NULL, 0,//__in SIZE_T dwStackSize, pfnThread, addr, 0, NULL); </span>
- <span style="font-size: 18px;">if(hRemoteThread==INVALID_HANDLE_VALUE)
- {
- MessageBox("遠端線程穿件失敗!");
- }
- else
- {
- MessageBox("遠端線程建立成功!");
- }
- </span>
<span style="font-size: 18px;">if(hRemoteThread==INVALID_HANDLE_VALUE) { MessageBox("遠端線程穿件失敗!"); } else { MessageBox("遠端線程建立成功!"); } </span>
6:建立要注入到遠端程序的DLL。此處不再介紹。可以參考《windows核心程式設計系列》談談DLL基礎。
如果在注入的DLL建立一個線程,就可以執行我們想讓它做的工作。
比如監控某程式的運作,一旦程式運作,就将另一個DLL加載到此程序。此DLL會挂在全局鈎子,獲得使用者鍵盤的動作。這也是鍵盤盜取qq的原理。可以自己發揮啊。
到此為止,關于遠端線程就介紹完畢。
參考自《windows核心程式設計》第五版 第二十二章 ,《加密與解密》第二版 段鋼著,第十章
以上僅僅在參考各書籍的基礎之上加以總結。如有錯誤,請不吝賜教。