實現DLL注入的方法已經不少了,也都比較成熟,用的最多的方法是經過遠程線程進行線程注入,而後導入Dll文件。遠程線程裏有兩個關鍵技術須要解決,一個是全局變量和字符串的存取問題,另外一個是地址重定位問題。若是用匯編來實現這兩個問題是很簡單的,但在高級語言中就顯得有點笨拙了。之前有一篇用C++來實現這個技術的文章,但那篇文章用了一個變通的方法,即用局部變量來代替全局變量,在創建遠程線程的時候把變量傳給遠程線程。這個方法能夠說達到了一舉兩得的目的,由於局部變量是存在於堆棧中的,不存在絕對地址的調用問題,對局部變量的訪問也就不存在地址重定位問題。但回頭想一想,那個方法仍是沒有可以突破C++的這些限制。筆者在參考了羅雲彬的《windows環境下32位彙編語言程序設計》中線程隱藏那一章節後,提出了在C++中實現遠程線程的方法。由於本文涉及到不少彙編方面的知識,因此我相信本文的意義不只僅是提出一種DLL注入的方法,更重要的是對編程能力的訓練和對加深對操做系統底層的理解。
高級語言的編譯過程
C++中的變量分爲如下幾個狀況:全局變量,字符串和局部變量。對於變量咱們在用C++編程中定義了直接調用就能夠,歷來不用考慮這些變量在生成的二進制文件裝入內存後是怎麼樣存儲的,但在進行遠程線程的操做時這些都是必須考慮的。實際上在編譯時,編譯器對變量和代碼有着不一樣的處理,全局變量和字符串被放在了數據段中,局部變量存放於堆棧中,代碼位於代碼段中,而數據段,堆棧區,代碼段在內存中是獨立存放的,它們的地址並不連續。在編譯時對全局變量和字符串的存取被修改成對絕對地址的調用,這個絕對地址是編譯時肯定的,在程序運行時由操做系統負責在該地址處分配內存並進行初始化。而編譯器對局部變量的處理是將其修改成對堆棧的操做,這樣局部變量的地址實際上依賴於棧寄存器,棧寄存器不一樣,其絕對地址也不一樣。
另外,高級語言中調用函數時,編譯器做的處理以下:
1.將須要傳遞的參數反向壓入堆棧;
2.用call語句調用函數地址,執行該函數;
3.函數執行完成後恢復堆棧的平衡;
在上面三步的第二步中又能夠分爲幾個小步:
1.把本句call指令的下一指令地址壓入堆棧;
2.轉到函數地址執行;
3.當遇到ret之類的返指令時從棧中彈出返回地址;
4. 轉到彈出的地址(也就是call語句的下句指令)繼續執行。
如今在拿LoadLibrary來舉個例子,該函數是windows中的一個API,位於Kernel32.dll中,其默認裝載地址爲7C801D7B。由於Kernel32.dll是常駐內存的,因此在通常狀況下內存中的這個地址就是LoadLibrary的入口地址,但這不是絕對的。在C++中要調用這個函數咱們能夠這樣寫: 編程
LoadLibrary(」Dll.dll」);
在彙編中就得這樣寫了:windows
Push 字符串」DLL.dll」在內存中的地址,一般這個地址位於數據區 Call LoadLibrary的入口地址,如7C801D7B
由於Windows中的API皆爲標準調用約定,因此恢復堆棧的工做就由被調用的函數完成了。固然在C++中自定義的函數通常爲C調用約定,若是調用本身寫的函數你就要考慮恢復堆棧的問題了。最後說明一點,在彙編中調用API的返回值通常會放在寄存器eax中,因此要檢測函數是否調用成功只要檢測eax的值就能夠了。
高級語言中遠程線程遇到的問題
首先咱們來回想一下遠程線程的實現過程:
1.在程序中的某個地方寫出遠程線程代碼;
2.用WriteProcessMemory將上步寫的遠程線程代碼複製到目標進程;
3.用CreateRemoteThread創建遠程線程,並使其運行。
如今仔細分析一下,不妨假設本進程爲進程A,要注入的宿主進程爲進程B。第一步中所寫的遠程線程代碼位於A中,此時用到的全局變量和字符串在編譯時生成的絕對地址是按進程A肯定的,這些地址位於進程A的數據區,第二步複製代碼時,咱們把代碼區的數據完整的複製到了進程B中,而數據區並無被複制。被複制過來的代碼要運行時一樣要訪問那些全局變量和字符串的絕對地址,在進程B中那個地址可能已經被其它進程佔用,也多是一些隨機的數據,這樣會形成訪問出錯。在彙編語言中能夠在代碼區中申請變量空間,在複製代碼時這些變量也相應被複制過去,這樣就不存在數據區絕對地址訪問的問題。但C++中是不容許在代碼區中申請變量空間的,因此如何讓變量隨代碼一塊兒被複制是第一個須要解決的問題。
其二,假如咱們己經在代碼區中成功的存放了變量。但編譯器在編譯時把對這個變量的存取修改成絕對地址的存取,這個地址一樣是位於進程A中的。在進程B中申請內存空間時,這個空間的地址是不肯定的,這樣代碼被複制到進程B中後對這個地址的存取仍是會出錯,其原理和上面是同樣的。因此這就須要咱們對這些變量進行地址重定位。
遠程線程代碼的實現
由於咱們的目的就是要把本身的DLL文件導入到目標進程中,因此遠程線程的代碼相對來講比較簡單,就是調用LoadLibrary,也就是上面舉的例子中的代碼。咱們要在代碼中保存的變量有兩個,一個是LoadLibrary的地址(雖然說這個地址基本上是固定的,但爲了保險起見咱們仍是動態獲取,並將其保存,不然就不須要這個變量了),另外一個是保存DLL文件名的字符串。爲了在寫遠程線程代碼時,在代碼區中爲這兩個變量分配內存,咱們能夠用空指令給這兩個變量佔位,複製到進程B中後再把它修改成真正的值。如:API的地址佔四個字節,彙編中一個空指令nop佔一個字節,因此咱們就用四個nop來爲其「申請」空間;咱們的DLL文件名爲」Dll.dll」,佔七個字節,考慮到字符要以0做爲結尾標記,共佔8個字節,因此咱們用8個nop來「申請」空間。
下一個問題是解決地址重定位問題,這個技術在病毒,木馬等諸多方面有着普遍的應用,固然這個技術並非由筆者實現,筆者也是經過學習獲得的,在這裏也簡要介紹一下實現的原理。
先看下面這段代碼:函數
1 call relocal 2 relocal: 3 pop ebx 4 sub ebx , offset relocal
如今細細分析一下。第一句話執行時首先會把第三句運行時的地址(注意是運行時的地址,不是絕對地址,這個地址在進程A中與在進程B中是不同的)壓入堆棧,而後執行第三句,而第三句又把該地址彈出到寄存器ebx。第四句的offset relocal,它在編譯時被編譯器修改成進程A中第三句的絕對地址,若是如今該段代碼運行於進程B中,第四句相減執行完後ebx並非0,而是這段代碼在進程A中的地址偏移與在進程B中地址偏移之差!獲得這個差值後,在進程B中每當訪問含有絕對地址的變量時只要加上這個差值就能夠獲得正確地址。
好了,關鍵技術實現後遠程線程的代碼以下:學習
REMOTE_THREAD_BEGIN: //遠程線程代碼開始標記 _asm { //*******給LoadLibrary函數地址佔位******* LoadLibraryAddr: nop nop nop nop //*******給FreeLibrary函數地址佔位******* FreeLibraryAddr: nop nop nop nop //*******給動態連接庫名佔位******* LibraryName: nop nop nop nop nop nop nop nop //*******代碼開始的真正位置******* REMOTE_THREAD_CODE: //*******實現地址重定位,ebx保存差值******* call relocal relocal: pop ebx sub ebx , offset relocal //*******1.調用LoadLibrary******* //*******1.1.壓入LoadLibrary參數(動態連接庫名)******* mov eax , ebx add eax , offset LibraryName //變量地址加上ebx,實現地址重定位 push eax //*******1.2.調用LoadLibrary******* mov eax , ebx add eax , offset LoadLibraryAddr //一樣實現地址重定位 mov eax , [eax] //從變量中取出LoadLibrary的地址 call eax //*******1.3.檢測是否成功,若是失敗了就直接返回,防止程序異常******* or eax , eax jnz NEXT1 //執行成功,跳轉到位NEXT1繼續執行 ret NEXT1: // *******2.釋放動態連接庫******* // *******2.1.壓入FreeLibrary參數******* push eax // *******2.2.調用FreeLibrary******* mov eax , ebx add eax , offset FreeLibraryAddr //地址重定位 mov eax , [eax] //從變量中取出FreeLibrary的地址 call eax ret } REMOTE_THREAD_END:
由於DLL文件在第一次被導入時會自動執行DllMain中的代碼,因此咱們把DLL中本身寫的代碼放在這個函數中,這樣只要DLL文件被導入就能夠執行代碼了;若是不這樣,咱們還必須去獲取DLL文件中的函數地址,那樣會加大工做量,固然若是你不怕麻煩也能夠去試試。
主程序的實現
其實遠程線程代碼實現後,本節的技術含量就相對低得多了。本段代碼主要實現如下幾個功能:
1.在宿主進程中申請代碼空間;
2.把遠程線程的代碼複製到宿主進程中;
3.修正遠程線程變量的值;
4.建立遠程線程,使遠程代碼執行。
關鍵代碼以下:測試
//*******1. 在宿主進程中申請代碼空間******* //*******1.1. 經過進程ID打開進程句柄,並得到進程句柄******* HANDLE hSelectedProcHandle; //保存宿主進程句柄 hSelectedProcHandle = OpenProcess(PROCESS_ALL_ACCESS , FALSE , nSelectedThreadId); //進程ID的獲取方法,完整的源代碼中有介紹,這裏就不介紹了 //*******1.2.獲得遠程線程代碼長度,目的是獲得要申請的空間的大小****** int nRemoteThreadCodeLength; //保存代碼長度 _asm { mov eax , offset REMOTE_THREAD_END mov ebx , offset REMOTE_THREAD_BEGIN sub eax , ebx //用代碼結尾偏移減去開始的偏移,獲得代碼長度 mov nRemoteThreadCodeLength , eax } //*******1.3.在宿主進程中申請空間******* LPVOID pRemoteThreadAddr; //保存申請空間的基址 pRemoteThreadAddr = VirtualAllocEx(hSelectedProcHandle , NULL , nRemoteThreadCodeLength , MEM_COMMIT,PAGE_EXECUTE_READWRITE); //*******2.把遠程線程的代碼複製到宿主進程******* //*******2.1.獲得本進程中遠程線程代碼的起始地址******* LPVOID pRemoteThreadCodeBuf; //指向本進程中遠程線程代碼的起始位置 DWORD nWritenNum , nSuccess; //臨時變量 _asm mov eax , offset REMOTE_THREAD_BEGIN _asm mov pRemoteThreadCodeBuf , eax //*******2.2.向宿主進程中複製代碼******* nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteThreadAddr , pRemoteThreadCodeBuf , nRemoteThreadCodeLength , &nWritenNum); // *******3.修正遠程線程中變量的值******* // *******3.1.首先獲取兩個關鍵函數的地址******* HMODULE hKernel32; hKernel32 = LoadLibrary("Kernel32.dll"); LPVOID pLoadLibrary , pFreeLibrary; pLoadLibrary = (LPVOID)GetProcAddress(hKernel32 , "LoadLibraryA"); pFreeLibrary = (LPVOID)GetProcAddress(hKernel32 , "FreeLibrary"); // *******3.2.修正代碼******* PBYTE pRemoteAddrMove; //在遠程線程地址上移動的指針 pRemoteAddrMove = (PBYTE)pRemoteThreadAddr; // *******3.2.1.修正LoadLibrary地址******* nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteAddrMove , &pLoadLibrary , 4 , &nWritenNum); //*******3.2.2.修正FreeLibrary地址******* pRemoteAddrMove +=4; //定位到保存FreeLibrary的變量 nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteAddrMove , &pFreeLibrary , 4 , &nWritenNum); //*******3.2.3.修正動態連接庫名******* char szDllName[8] = {"Dll.dll"}; //注意這裏必須是8個字符, //而且必須與你的DLL文件名相同 pRemoteAddrMove +=4; nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteAddrMove , szDllName , 8 , &nWritenNum); //*******4.建立遠程線程,使遠程代碼執行******* //*******4.1.把指針移動到遠程線程代碼開始處******* pRemoteAddrMove +=8; HANDLE hRemoteThreadHandle; //遠程線程句柄 // *******4.2.定義遠程線程函數類型******* typedef unsigned long (WINAPI *stRemoteThreadProc)(LPVOID); stRemoteThreadProc pRemoteThreadProc; // *******4.3.把入口地址賦給聲明的函數******* pRemoteThreadProc = (stRemoteThreadProc)pRemoteAddrMove; //*******4.4.建立遠程線程******* hRemoteThreadHandle = CreateRemoteThread(hSelectedProcHandle , NULL , 0 , pRemoteThreadProc , 0 , 0 , NULL);
由於本模塊主要是調用一些API,這些API去查下資料就能夠知道其用法,因此這裏就不作詳細介紹了。所附源代碼爲一個基於對話框的MFC程序,裏面還有一個獲取當前系統進程的模塊,這裏就不介紹其實現過程了。另外還附帶一個簡單的DLL工程做爲測試。在運行程序時必定要把DLL文件放到系統搜索路徑中,不然會因找不到DLL文件而失敗。
至此全部功能模塊已經介紹完畢,整體來講,這個方法實現了咱們的預期功能,它的不足之處是實現起來比較繁瑣,但從學習的角度來講不失爲一個好方法。若文中有說的不到之處,還請各位高手們批評指正。操作系統