2020-11-23(Windows系统的dll注入 )
一、什么是dll注入
在Windows操作系統中,運行的每一個進程都生活在自己的程序空間中(保護模式),每一個進程都認為自己擁有整個機器的控制權,每個進程都認為自己擁有計算機的整個內存空間,這些假象都是操作系統創造的(操作系統控制CPU使得CPU啟用保護模式)。理論上而言,運行在操作系統上的每一個進程之間都是互不干擾的,即每個進程都會擁有獨立的地址空間。比如說進程B修改了地址為0x4000000的數據,那么進程C的地址為0x4000000處的數據并未隨著B的修改而發生改變,并且進程C可能并不擁有地址為0x4000000的內存(操作系統可能沒有為進程C映射這塊內存)。因此,如果某進程有一個缺陷覆蓋了隨機地址處的內存(這可能導致程序運行出現問題),那么這個缺陷并不會影響到其他進程所使用的內存。
也正是由于進程的地址空間是獨立的(保護模式),因此我們很難編寫能夠與其它進程通信或控制其它進程的應用程序。
所謂的dll注入即是讓程序A強行加載程序B給定的a.dll,并執行程序B給定的a.dll里面的代碼。注意,程序B所給定的a.dll原先并不會被程序A主動加載,但是當程序B通過某種手段讓程序A“加載”a.dll后,程序A將會執行a.dll里的代碼,此時,a.dll就進入了程序A的地址空間,而a.dll模塊的程序邏輯由程序B的開發者設計,因此程序B的開發者可以對程序A為所欲為。
二、什么時候需要dll注入
應用程序一般會在以下情況使用dll注入技術來完成某些功能:
1.為目標進程添加新的“實用”功能;
2.需要一些手段來輔助調試被注入dll的進程;
3.為目標進程安裝鉤子程序(API Hook);
三、dll注入的方法
一般情況下有如下dll注入方法:
1.修改注冊表來注入dll;
2.使用CreateRemoteThread函數對運行中的進程注入dll;
3.使用SetWindowsHookEx函數對應用程序掛鉤(HOOK)迫使程序加載dll;
4.替換應用程序一定會使用的dll;
5.把dll作為調試器來注入;
6.用CreateProcess對子進程注入dll
7.修改被注入進程的exe的導入地址表。
接下來將詳細介紹如何使用這幾種方式完成dll注入。
四、注入方法詳解
(一)、修改注冊表
如果使用過Windows,那么對注冊表應該不會陌生。整個系統的配置都保存在注冊表中,我們可以通過修改其中的設置來改變系統的行為。
首先打開注冊表并定位到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows項,如下圖所示,他顯示了該注冊表項中的條目。
AppInit_DLLs鍵的值可以是一個dll的文件名或一組dll的文件名(通過逗號或空格來分隔),由于空格是用來分隔文件名的,因此dll文件名不能含有空格。第一個dll的文件名可以包含路徑,但其他的dll包含的路徑將被忽略。
LoadAppInit_DLLs鍵的值表示AppInit_DLLs鍵是否有效,為了讓AppInit_DLLs鍵的值有效,需要將LoadAppInit_DLLs的值設置為1。
這兩個鍵值設定后,當應用程序啟動并加載User32.dll時,會獲得上述注冊表鍵的值,并調用LoadLibrary來調用這些字符串中指定的每一個dll。這時每個被載入的dll可以完成相應的初始化工作。但是需要注意的是,由于被注入的dll是在進程生命期的早期被載入的,因此這些dll在調用函數時應慎重。調用Kernel32.dll中的函數應該沒有問題,因為Kernel32.dll是在User32.dll載入前已被加載。但是調用其他的dll中的函數時應當注意,因為進程可能還未載入相應的dll,嚴重時可能會導致藍屏。
這種方法很簡單,只需要在注冊表中修改兩個鍵的值即可,但是有如下缺點:
1.只有調用了User32.dll的進程才會發生這種dll注入。也就是說某些CUI程序(控制臺應用程序)可能無法完成dll注入,比如將dll注入到編譯器或鏈接器中是不可行的。
2.該方法會使得所有的調用了User32.dll的程序都被注入指定的dll,如果你僅僅想對某些程序注入dll,這樣很多進程將成為無辜的被注入著,并且其他程序你可能并不了解,盲目的注入會使得其他程序發生崩潰的可能性增大。
3.這種注入會使得在應用程序的整個生命周期內被注入的dll都不會被卸載。注入dll的原則是值在需要的時間才注入我們的dll,并在不需要時及時卸載。
(二)、使用CreateRemoteThread函數對運行中的進程注入dll
這種方法具有最高的靈活性,同時它要求掌握的知識也很多。從根本上說,dll注入技術要求目標進程中的一個線程調用LoadLibrary函數來載入我們想要注入的dll,由于我們不能輕易的控制別人進程中的線程,因此這種方法要求我們在目標進程中創建一個線程并在線程中執行LoadLibrary函數加載我們要注入的dll。幸運的是Windows為我們提供了CreateRemoteThread函數,它使得在另一個進程中創建一個線程變得非常容易。CreateRemoteThread函數的原型如下:
該函數與CreateThread僅僅只多出第一個參數hProcess,hProcess表示創建的新線程屬于哪一個進程。
參數lpStartAddress表示線程函數的起始地址,注意這個地址在目標進程的地址空間中。
現在問題來了,我們如何調用讓創建的線程執行LoadLibrary函數來加載我們要注入的dll呢?答案很簡單:只需要創建的線程的線程函數地址是LoadLibrary函數的起始地址即可。我們都知道,每一個線程創建時應該指定一個參數只有4個字節,返回值也只是4個字節的函數即可(從匯編的角度看確實如此,只要保證調用前后棧平衡即可),而LoadLibrary函數就滿足這些條件。LoadLibrary函數的原型如下:
可以發現LoadLibrary函數完全滿足上述條件,LoadLibrary的參數是dll路徑的起始地址,這個參數也就是CreateRemoteThread函數的lpParameter參數。但是參數指向的地址應該是目標進程的地址,并且該地址處應保存被加載dll的路徑字符串。但是一開始我們并不知道目標進程是否存在這樣一個地址并且這個地址恰好保存了我們的dll的完整路徑。解決這一問題的最保險的辦法是使用VirtualAllocEx函數在目標進程中開辟一塊內存存放我們的dll的路徑。VirtualAllocEx函數的原型如下:
LPVOID WINAPI VirtualAllocEx(_In_ HANDLE hProcess,_In_opt_ LPVOID lpAddress,_In_ SIZE_T dwSize,_In_ DWORD flAllocationType,_In_ DWORD flProtect );VirtualAllocEx函數允許我們在目標進程中開辟一塊指定大小(以字節為單位)的內存,并返回這塊內存的起始地址。之后就可以用WriteProcessMemory函數將dll文件路徑的數據復制到目標進程中。WriteProcessMemory函數的原型如下:
BOOL WINAPI WriteProcessMemory(_In_ HANDLE hProcess,_In_ LPVOID lpBaseAddress,_In_ LPCVOID lpBuffer,_In_ SIZE_T nSize,_Out_ SIZE_T *lpNumberOfBytesWritten );在開始注入前,還需要確認一件事,就是目標進程使用的字符編碼方式。因為我們所調用的LoadLibrary函數在底層實際調用有兩種可能:
如果目標程序使用的是ANSI編碼方式,LoadLibrary實際調用的是LoadLibraryA,其參數字符串應當是ANSI編碼;
如果目標程序使用的是Unicode編碼方式,LoadLibrary實際調用的是LoadLibraryW,其參數字符串應當是Unicode編碼。
這使得注入過程變得很麻煩,為了減少復雜性,不妨直接使用LoadLibraryA或LoadLibraryW而不是用LoadLibrary函數來避免這一麻煩。另外,即使使用的是LoadLibraryA,LoadLibraryA也會將傳入的ANSI編碼的字符串參數轉換成Unicode編碼后再調用LoadLibraryW。綜上,不妨一致使用LoadLibraryW函數,并且字符串用Unicode編碼即可。
最后,我們可能會為獲得目標進程中LoadLibraryW函數的起始地址而頭疼,但其實這個問題也很簡單,因為目標進程中函數LoadLibraryW的起始地址和我們的進程中的LoadLibraryW函數的起始地址是一樣的。因此我們只需要用GetProcAddress即可獲得LoadLibraryW函數的起始地址。
經過以上漫長的分析,我們對CreateRemoteThread注入方法的原理有了較為清晰的理解,接下來我們就需要總結一下我們必須采取的步驟:
(1).用VirtualAllocEx函數在目標進程的地址空間中分配一塊足夠大的內存用于保存被注入的dll的路徑。
(2).用WriteProcessMemory函數把本進程中保存dll路徑的內存中的數據拷貝到第(1)步得到的目標進程的內存中。
(3).用GetProcAddress函數獲得LoadLibraryW函數的起始地址。LoadLibraryW函數位于Kernel32.dll中。
(4).用CreateRemoteThread函數讓目標進程執行LoadLibraryW來加載被注入的dll。函數結束將返回載入dll后的模塊句柄。
(5).用VirtualFreeEx釋放第(1)步開辟的內存。
在需要卸載dll時我們可以在上述第(5)步的基礎上繼續執行以下步驟:
(6).用GetProcAddress函數獲得FreeLibrary函數的起始地址。FreeLibrary函數位于Kernel32.dll中。
(7).用CreateRemoteThread函數讓目標進程執行FreeLibrary來卸載被注入的dll。(其參數是第(4)步返回的模塊句柄)。
如果不在上述步驟基礎上執行操作,卸載dll時你需要這么做:
(1).獲得被注入的dll在目標進程的模塊句柄。
(2).重復上述步驟的第(6)、(7)兩步。
接下來給出編寫的參考代碼,該程序以控制臺應用程序方式運行,并在Windows 10上測試通過。
(三)、使用SetWindowsHookEx函數對應用程序掛鉤(HOOK)迫使程序加載dll
消息鉤子:Windows操作系統為用戶提供了GUI(Graphic User Interface,圖形用戶界面),它以事件驅動方式工作。在操作系統中借助鍵盤、鼠標、選擇菜單、按鈕、移動鼠標、改變窗口大小與位置等都是事件。發生這樣的事件時,操作系統會把事先定義好的消息發送給相應的應用程序,應用程序分析收到的信息后會執行相應的動作。也就是說,在敲擊鍵盤時,消息會從操作系統移動到應用程序。所謂的消息鉤子就是在此期間偷看這些信息。以鍵盤輸入事件為例,消息的流向如下:
1.發生鍵盤輸入時,WM_KEYDOWN消息被添加到操作系統的消息隊列中;
2.操作系統判斷這個消息產生于哪個應用程序,并將這個消息從消息隊列中取出,添加到相應的應用程序的消息隊列中;
3.應用程序從自己的消息隊列中取出WM_KEYDOWN消息并調用相應的處理程序。
當我們的鉤子程序啟用后,操作系統在將消息發送給用用程序前會先發送給每一個注冊了相應鉤子類型的鉤子函數。鉤子函數可以對這一消息做出想要的處理(修改、攔截等等)。多個消息鉤子將按照安裝鉤子的先后順序被調用,這些消息鉤子在一起組成了"鉤鏈"。消息在鉤鏈之間傳遞時任一鉤子函數攔截了消息,接下來的鉤子函數(包括應用程序)將都不再收到該消息。
像這樣的消息鉤子功能是Windows提供的最基本的功能,MS Visual Studio中提供的SPY++就是利用了這一功能來實現的,SPY++是一個十分強大的消息鉤取程序,它能夠查看操作系統中來往的所有消息。
消息鉤子是使用SetWindowsHookEx來實現的。函數的原型如下:
idHook參數是消息鉤子的類型,可以選擇的類型在MSDN中可以查看到相應的宏定義。比如我們想對所有的鍵盤消息做掛鉤,其取值將是WH_KEYBOARD,WH_KEYBOARD這個宏的值是2。
lpfn參數是鉤子函數的起始地址,注意:不同的消息鉤子類型的鉤子函數原型是不一樣的,因為不同類型的消息需要的參數是不同的,具體的鉤子函數原型需要查看MSDN來獲得。注意:鉤子函數可以在結束前任意位置調用CallNextHookEx函數來執行鉤鏈的其他鉤子函數。當然,如果不調用這個函數,鉤鏈上的后續鉤子函數將不會被執行。
hMod參數是鉤子函數所在的模塊的模塊句柄。
dwThreadId參數用來指示要對哪一個進程/線程安裝消息鉤子。如果這個參數為0,安裝的消息鉤子稱為“全局鉤子”,此時將對所有的進程(當前的進程以及以后要運行的所有進程)下這個消息鉤子。注意:有的類型的鉤子只能是全局鉤子。
注意:鉤子函數應當放在一個dll中,并且在你的進程中LoadLibrary這個dll。然后再調用SetWindowsHookEx函數對相應類型的消息安裝鉤子。
當SetWindowsHookEx函數調用成功后,當某個進程生成這一類型的消息時,操作系統會判斷這個進程是否被安裝了鉤子,如果安裝了鉤子,操作系統會將相關的dll文件強行注入到這個進程中并將該dll的鎖計數器遞增1。然后再調用安裝的鉤子函數。整個注入過程非常方便,用戶幾乎不需要做什么。
當用戶不需要再進行消息鉤取時只需調用UnhookWindowsHookEx即可解除安裝的消息鉤子,函數的原型如下:
hhk參數是之前調用SetWindowsHookEx函數返回的HHOOK變量。這個函數調用成功后會使被注入過dll的鎖計數器遞減1,當鎖計數器減到0時系統會卸載被注入的dll。
這種類型的dll注入的優點是注入簡單,缺點是只能對windows消息進行Hook并注入dll,而且注入dll可能不是立即被注入,因為這需要相應類型的事件發生。其次是它不能進行其他API的Hook,如果想對其它的函數進行Hook,你需要再在被注入的dll中添加用于API Hook的代碼。
接下來將給出這一dll注入方案的示例程序的代碼,代碼包含兩部分,一部分是dll的源文件,另一部分是控制臺程序的源代碼。該程序的功能是屏蔽所有notepad.exe(Windows附帶的記事本程序)的按鍵消息,該程序在Windows xp下測試通過。
(四)、替換應用程序一定會使用的dll
這種方法通常被編寫惡意代碼的人員用來編寫木馬,因此又被稱為使用木馬dll來注入dll。通常我們應當首先確認目標進程一定會載入的dll,然后替換掉它。舉個例子:比如我們知道目標進程一定會載入Xyz.dll,那么我們可以創建自己的dll并與它起同樣的名字。當然,我們必須將原先被替換掉的Xyz.dll改成別的名字,比如改成Xyz_1.dll。
注意:在我們編寫的Xyz.dll(將被注入的dll)內部,我們要導出原來的Xyz.dll所導出的所有符號。這一點很容易實現,可以用dll的函數轉發器實現(轉發到Xyz_1.dll的相同函數),這樣一來我們只需要對需要HOOK(掛鉤)的函數編寫掛鉤代碼即可,這一過程我們僅僅是多了一些重復工作。看起來這個方法是完美的,并且很多木馬程序經常這么干,但是它存在一個很嚴重的問題:如果被替換的dll后來由于程序升級導致替換的dll添加了新的導出函數,而被注入的dll并未及時添加這些新增導出函數的轉發器(或者Hook程序),這將導致使用了新的導出函數的程序不能正常運行。另外,請不要隨意的替換系統的dll,因為在dll注入一般應當只注入到目標進程即可,而注入到別的進程之后將帶來很大的安全隱患。
(五)、把dll作為調試器來注入
使用過OD(OllyDbg)的人員可能會為OD的強大功能感到驚嘆。因為OD可以調試一個程序并任意的修改被調試的程序。OD的工作原理是向目標進程使用了調試功能。調試器可以在被調試進程中執行很多特殊操作,操作系統載入一個被調試程序的時候,會在被調試的主線程尚未開始執行任何代碼前,自動通知調試器(用來調試被調試進程的進程),這時調試器可以將一些代碼注入到被調試進程的地址空間中,保存被調試進程的CONTEXT結構,修改EIP指向我們注入的代碼的起始位置執行這些代碼。最后再讓被調試的進程恢復原來的CONTEXT,繼續執行。整個過程對被調試的進程而言好像沒發生任何事情。
這種注入方式需要對調試功能有所研究,并且能夠對進程的CONTEXT進行操作,最后還需要對不同的CPU平臺進行量身操作。此外,我們可能還需要手工編寫一些匯編指令來讓被調試的程序執行。這對編寫人員的能力要求較高。最后,這種方法在調試器終止后,Windows會自動終止被調試的程序。不過調試器可以通過調用DebugSetProcessKillOnExit函數并傳入FALSE,來改變Windows的默認行為。然后調試器就可以調用DebugActiveProcessStop函數來終止調試了。
為什么要在主線程尚未開始執行任何代碼前執行代碼注入呢?因為這個時候注入最安全,其實你可以在任何時候對被調試的程序下斷點并進行以上注入操作,但是為了保證被調試程序的穩定運行你可能需要做更多的工作。
(六)、用CreateProcess對子進程注入dll
這個方法與把dll作為調試器來注入方法有許多相似之處,同樣也具有較大的難度。這里要求目標進程是注入者進程的子進程。當使用CreateProcess函數來創建一個子進程時,可以選擇創建后立即掛起該進程。這樣,創建的子進程并不會開始執行且EIP指向ntdll.dll的RtlUserThreadStart函數的開始位置(在win10上EIP=0X76F9BA60),此時的子進程處于掛起狀態。因此,我們可以有目的地修改EIP的值讓其從另一個位置繼續執行,但隨意的修改EIP的值往往使創建的子程序崩潰。為了讓創建的子進程載入dll必須調用LoadLibrary函數。在使用CreatRemoteProcess方法中也介紹了一點:必須在目標進程(這里指子進程)中寫入載入的dll的完整路徑。因此我們在修改EIP指向我們的代碼之前需要將一部分代碼注入到目標進程中。其中被注入的代碼至少應包括如下操作:將dll路徑首地址壓棧;調用LoadLibrary函數;跳轉回原先EIP位置,讓程序繼續執行,好像什么都沒發生過。
但是,為了程序的穩定運行,這樣做還不夠。注入的代碼應該在執行后能恢復執行前的所有狀態。因此為了注入dll需要向目標進程注入較為安全的代碼應該包含如下操作:
1.保存所有寄存器的值;
2.將dll路徑首地址壓棧;
3.調用LoadLibrary函數;
4.恢復所有寄存器的值;
5.跳轉到原先EIP位置,讓程序繼續執行,好像什么都沒發生。
該方法有如下優點:在程序未開始執行前執行了dll注入,一般比較難以被發現。幾乎可以對所有的程序進行注入。
該方法同樣具有缺點:首先需要嚴謹的設計注入的代碼,并根據不同的cpu平臺進行設計。其次就是目標進程要是注入著創建的子進程。
接下來將給出一段示例代碼,該程序以控制臺方式運行。并在Windows 10和Windows xp上測試通過。(這段代碼參考自看雪論壇的IamHuskar,這里表示感謝!)
現在對以上代碼做分析,程序首先調用CreateProcess函數來創建一個掛起的進程。創建成功后,prosic結構體保存了子進程的進程句柄和主線程的線程句柄。接下來調用StartHook函數進行代碼注入。
現在我們來詳細地分析StartHook函數,首先它創建了一段ShellCode,ShellCode的內容將被會復制到目標進程的空間中。但是當前的ShellCode還不能正常工作。因為它的很多數據要依靠放入目標進程的地址來決定。ShellCode實際上是一段匯編代碼后面附帶了執行這段代碼所需的變量或數據。所有的匯編代碼已在注釋當中進行標注。ShellCode數組的長度由匯編代碼長度和變量的長度的總和。
接下來的工作是修復ShellCode中部分匯編指令引用的地址,這些地址要以目標進程寫入的地址作為基礎偏移量。那么我們首先應該用VirtualAllocEx在目標進程的空間中調撥一塊可執行的物理內存用來保存ShellCode代碼。當然,LoadLibraryW函數的地址還是要從本進程中獲得。當對ShellCode數據修改完畢后,就可以將ShellCode通過WriteProcessMemory函數將ShellCode復制到目標進程中。接下來需要修改目標進程的EIP指針來使主線程從ShellCode的開始處。最后,恢復目標進程,讓其繼續運行即可。
通過以上分析,對上述代碼的執行步驟做如下總結:
1.創建一個掛起的子進程作為目標進程;
2.準備一份預先設計好的ShellCode(應具有上面所述的基本功能);
3.用VirtualAllocEx在目標進程中調撥一塊可執行的內存;
4.以分配的內存為基準修復ShellCode的匯編代碼引用的地址和數據;
5.用WriteProcessMemory函數將修復完畢的ShellCode復制目標進程在第3步分配的內存中;
6.修改目標進程的主線程的EIP指向第3步分配的內存的首地址;
7.恢復目標進程的主線程。
此方法的難點是設計好ShellCode代碼,這需要編寫者具有較高的匯編和分析設計能力。
總結
以上是生活随笔為你收集整理的2020-11-23(Windows系统的dll注入 )的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020-11-23(dll注入方法)
- 下一篇: 2020-11-23(“花式扫雷” 辅助