有使用過外掛的朋友應該知道,我們在玩游戲的時候,有很多輔助功能給你使用,比如吃葯,使用物品等功能,這個時候我們就是使用注入代碼的技術,簡單的來將就是我們讓另外一個進程去執行我們想讓它執行的代碼,這中間的關鍵函數是CreateRemoteThread
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
);
CreateRemoteThread的參數跟CreateThread的參數差不多,多出來的hProcess是我們要對其操作的進程HANDLE,在早期的WINDOWS版本CreateThread確實是使用CreateRemoteThread實現的,就是把hProcess傳入我們自己的進程HANDLE
CreateRemoteThread的功能就是在指定的進程創建一個線程,這個線程運行我們指定的函數,看起來很簡單,但是有一個問題,就是虛擬內存導致的問題
大家都知道,在WINDOWS下是使用虛擬內存來進行數據管理的,每個進程都有自己獨立的地址空間,假設進程A准備向進程B注入一段代碼,他要讓進程B執行他進程空間的函數InjectionCode(),這個函數在進程A的地址空間地址為0X3000
現在我們開始進行代碼注入,利用CreateRemoteThread我們告訴B進程,請執行虛擬內存地址為0X3000的代碼,這個時候B進程該干什么呢??B進程收到這個命令后,他很聽話地創建了線程,然后乖乖得CALL了0X3000的內容,請注意,現在B進程CALL的是它自己內存空間內0X3000的代碼而不是A進程的,那么現在B進程的0X3000是什么內容??沒人知道,運氣好的話說不定真的有段代碼給你執行,運氣不好你自己也不知道會發生什么事情,這就跟你進錯了學生公寓一樣,同樣號碼的房間,運氣好是校花的房間,運氣不好就是如花的房間
那么我們怎么才能讓進程去執行我們對應的代碼呢??我們只要在B進程內開辟一塊內存,然后把我們的代碼或者數據復制進去,再執行對應的代碼就可以了,我們需要用到這幾個函數:
LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect ); BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten );
這兩個函數跟我們平常用的函數都差不多,只是多了個進程的選項,大概步驟如下圖:
現在我們將實際操作一下:
下面是我們要注入的程序,在這之前,我們最好把基地址固定掉,這樣我們不會每次重新運行程序的時候函數的地址都會改變,在VS2008中,項目屬性->鏈接器->高級,把隨機基址和固定基址選擇默認值
void PrintMsg(const char *msg) { printf("ThreadI D:%d Msg:%s\n",GetCurrentThreadId(),msg); } int main(void) { printf("%d\n",GetCurrentThreadId()); printf("Print Msg Function Address:%X\n",PrintMsg); system("pause"); }
假設我們的PrintMsg的地址是0x401000,現在我們需要往這個進程里面注入一段代碼,讓她可以自動調用PrintMsg這個函數
1 static const char *msg = "INJECTION CODE SUCESS\n"; 2 static const unsigned int PARAM_SIZE = 100; 3 static const unsigned int EXE_SIZE = 500; 4 5 6 void InjectionCode(const char *msg) 7 { 8 __asm 9 { 10 push eax 11 push msg 12 mov eax,0x401000 13 call eax //因為在我們要注入的進程中,PrintMsg位於0x401000這個位置 14 pop eax 15 pop eax 16 } 17 } 18 int main(void) 19 { 20 HANDLE hProcess = OpenProcessByProcessNmae("main.exe"); //這個函數在上一章 21 22 if (hProcess == INVALID_HANDLE_VALUE) 23 { 24 printf("error open process %d\n",GetLastError()); 25 return 1; 26 } 27 //一定要把函數的代碼和msg寫入要注入的進程,否則會發生位置錯誤(一般是崩潰) 28 LPVOID RemoteExe = VirtualAllocEx(hProcess,NULL,EXE_SIZE,MEM_COMMIT,PAGE_EXECUTE); 29 LPVOID RemoteParam = VirtualAllocEx(hProcess,NULL,PARAM_SIZE,MEM_COMMIT,PAGE_READWRITE); 30 31 SIZE_T WriteCount = 0; 32 int ret = 0; 33 ret = WriteProcessMemory(hProcess,RemoteParam,msg,PARAM_SIZE,&WriteCount); 34 ret = WriteProcessMemory(hProcess,RemoteExe,InjectionCode,0x13,&WriteCount); 35 36 HANDLE hThread = CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)RemoteExe,RemoteParam,0,NULL); 37 WaitForSingleObject(hThread,INFINITE); 38 }
運行上面的程序,我們就可以在另外一個進程中創建一個線程,並且這個線程將會輸出該線程的ID以及我們要輸出的消息
上面的程序還有幾個要注意的:
1.資源競爭
由於是創建線程執行相應代碼所以肯定會有資源競爭的問題,以后要寫代碼一定要注意,在本例中我忽略了這個問題
2. 關於代碼的長度問題
在本例中,我們的代碼長度是0X13,但是要知道,匯編代碼的長度隨便懂一下就可能更改,可能因為一個指令,也可能因為一個參數,所以我們需要時刻注意這點,關於代碼長度怎么測量,我是看了反匯編的代碼后計算的,這個方法比較准確,也可以大概估計下,只要能把代碼復制完整就可以,超出也沒關系,只要不超出申請的內存大小就可以
3.記得備份我們使用的寄存器
這個十分重要,一旦你更改了寄存器,如果沒有后面沒有恢復,可能會導致一系列錯誤,特別是ESP,EBP等重要的寄存器
3.注入代碼多次調用系統DLL中的函數
<<WINDOWS核心編程>>里面說,系統的DLL都會加載到一個固定的地址,比如VirtualAllocEx,一般我們在A進程和B進程的時候,call或者jmp的地址都是一樣的,所以一般我們如果調用的是系統函數,一般我們不需要擔心,但是,昨天我想到了一個問題,比如我們進程A要命令進程B調用CreateToolhelp32Snapshot這個系統API,現在我們假設CreateToolhelp32Snapshot這個API在單獨的TLHELP32.DLL里面(實際上這個在KERNEL32.DLL里面,所有進程都會加載這個DLL,所以不需要擔心下面的問題,這個只是舉例),操作系統在加載DLL的時候,會統一把這個API的地址映射到虛擬內存的0XFF40100的地址,按照我們原來的想法進程B會自己跑去call 0XFF40100這個地址。但問題在於,如果我們的進程根本就沒有加載TLHELP32.DLL這個DLL,那么進程call 0XFF40100會怎么樣??這個就要看你這個地方是什么代碼了,有人說操作系統會幫你加載這個DLL,但我覺得是錯的,因為操作系統要幫你加載的DLL都在PE頭里面的導入表里面,要嘛就是我們要顯示地去加載,否則操作系統不會知道我們的API在哪個DLL里面
4.注入代碼多次調用我們自己編寫的函數
比如我們有IntejectionCode,里面調用了IntejectionCode1,這個時候我們需要把IntejectionCode1也寫入對方進程里面,不能只寫入IntejectionCode,並且,我們需要更改IntejectionCode里面call IntejectionCode1跳轉指令,讓其跳轉到正確的位置。總之,別人的地盤別人做主,對方進程想把代碼放哪里就放哪里,我們無法管理(實際上virtualAllocEx是可以指定位置的,但是一般我們都盡量讓操作系統去指定),我們只能入鄉隨俗,人家讓我們去哪里調用我們就要去哪里調用,不然很容易導致進程崩潰
5.關於代碼的基地址
在本例中PrintMsg的基地址是固定的,是我們人為去固定基地址的,我們在開發的時候很少人會去把基地址固定掉,所以在進程運行的時候,PrintMsg這個函數的地址是會改變的,當然我們也可以算出來這段代碼在運行的時候會放在哪里,因為PrintMsg這段代碼以二進制放在EXE文件的時候,也有一個文件偏移量,當操作系統把EXE文件加載進內存后,會根據基地址和文件偏移量,來算出PrintMsg在虛擬內存中的位置,所以我們只要能拿到進程運行時候的基地址,並且把EXE反匯編查看這段代碼的文件偏移量,也能算出每次運行的時候PrintMsg的地址,雖然很麻煩,特別是反匯編找代碼的那部分,但也沒辦法,這個在后面我會講