DLL注入新姿式:反射式DLL注入研究

在分析koadic滲透利器時,發現它有一個注入模塊,其DLL注入實現方式和通常的注入方式不同。搜索了一下發現是由HarmanySecurity的Stephen Fewer提出的ReflectiveDLL Injection. 因爲目前互聯網上有關這個反射式DLL注入的分析並很少,也沒有人分析其核心的ReflectiveLoader具體是怎麼實現的,所以我就在這拋磚引玉了。數組

0×00 引言

常規的DLL注入方式相信你們都很熟悉了,利用CreateRemoteThread這一函數在目標進程中開始一個新的線程,這個線程執行系統的API函數LoadLibrary,以後DLL就被裝載到目標進程中了。然而,因爲這一技術被大量的惡意軟件利用,各類安全對DLL注入這一塊天然是嚴加看守,而常規的注入方式太過於套路化(CreateRemoteThread+ LoadLibrary),致使它十分容易被檢測出來。同時,常規的DLL注入方式還須要目標DLL必須存在磁盤上,而文件一旦「落地」就也存在着被殺毒軟件查殺的風險。緩存

所以我在這裏介紹一種新的DLL注入方式,它不須要在文件系統存放目標DLL,減小了文件「落地」被刪的風險。同時它不須要像常規的DLL注入方式那麼套路,所以更容易經過殺軟的行爲檢測。因爲反射式注入方式並無經過LoadLibrary等API來完成DLL的裝載,DLL並無在操做系統中」註冊」本身的存在,所以用ProcessExplorer等軟件也沒法檢測出進程加載了該DLL。安全

0×01 核心思路

咱們不想讓DLL文件「落地」, 那咱們能夠在磁盤上存放一份DLL的加密後的版本,而後將其解密以後儲存在內存裏。咱們而後能夠用VirtualAlloc和WriteProcessMemory將DLL文件寫入目標進程的虛擬空間中。然而,要」加載」一個DLL,咱們使用的LoadLibrary函數要求該DLL必須存在於文件系統中。這可怎麼辦呢。markdown

沒錯,咱們須要拋棄LoadLibrary,本身來實現整個裝載過程!咱們能夠爲待注入的DLL添加一個導出函數,ReflectiveLoader,這個函數實現的功能就是裝載它自身。那麼咱們只須要將這個DLL文件寫入目標進程的虛擬空間中,而後經過DLL的導出表找到這個ReflectiveLoader並調用它,咱們的任務就完成了。網絡

因而,咱們的任務就轉到了編寫這個ReflectiveLoader上。因爲ReflectiveLoader運行時所在的DLL尚未被裝載,它在運行時會受到諸多的限制,例如沒法正常使用全局變量等。並且,因爲咱們沒法確認咱們究竟將DLL文件寫到目標進程哪一處虛擬空間上,因此咱們編寫的ReflectiveLoader必須是地址無關的。也就是說,ReflectiveLoader中的代碼不管處於虛擬空間的哪一個位置,它都必須能正確運行。這樣的代碼被咱們稱爲「地址無關代碼」(position-independent code, PIC)。數據結構

0×02 注射器實現

要實現反射式注入DLL咱們須要兩個部分,注射器和被注入的DLL。其中,被注入的DLL除了須要導出一個函數ReflectiveLoader來實現對自身的加載以外,其他部分能夠正常編寫源代碼以及編譯。而注射器部分只須要將被注入的DLL文件寫入到目標進程,而後將控制權轉交給這個ReflectiveLoader便可。所以,注射器的執行流程以下:koa

1. 將待注入DLL讀入自身內存(利用解密磁盤上加密的文件、網絡傳輸等方式避免文件落地)ide

2. 利用VirtualAlloc和WriteProcessMemory在目標進程中寫入待注入的DLL文件函數

3. 利用CreateRemoteThread等函數啓動位於目標進程中的ReflectiveLoaderui

至此,咱們注射器的任務就已經完成了。下一步就是ReflectiveLoader的實現了。

0×03 ReflectiveLoader的實現

ReflectiveLoader要完成的任務是對自身的裝載。所謂的「裝載」具體而言是什麼意義呢?

所謂「裝載」,最重要的一點就是要將自身合適地展開到虛擬空間中。咱們都知道在PE文件包含了許多節,而爲了節省存儲空間,這些節在PE文件中比較緊密地湊在一塊兒的。而在廣闊虛擬空間中,這些節就能夠映射到更大的空間中去。更不用說還存在着.bss這樣的在PE文件中不佔空間,而要在虛擬空間中佔據位置的節了。ReflectiveLoader須要作的一件很重要的事就是按照規則去將這些節映射到對應的地址去。

同時,因爲DLL中可能會用到其餘DLL的函數,裝載一個DLL還須要將這個DLL依賴的其餘動態庫裝入內存,並修改DLL的IAT指向到合適的位置,這樣對其餘DLL函數的引用才能正確運做。

雖然咱們上文提到,ReflectiveLoader的代碼是地址無關的,可是該DLL的其餘部分的代碼卻並非這樣的。在一份源代碼編譯、連接成爲DLL時,編譯器都是假設該DLL會加載到一個固定的位置,生成的代碼也是基於這一個假設。在反射式注入DLL的時候,咱們不太可能申請到這個預先設定好的地址,因此咱們須要面對一個重定位(Rebasing)的問題。

以上就是ReflectiveLoader所面對的問題。接下來咱們看看它是如何解決這些問題的。

1) 定位DLL文件在內存中的基址

ReflectiveLoader作的第一件事就是查找自身所在的DLL具體被寫入了哪一個位置。

ReflectiveLoader首先利用一個重定位技巧找到自身所在的大體位置:

ULONG_PTR caller( VOID ) { return(ULONG_PTR)_ReturnAddress(); }

其中函數_ReturnAddress()返回的是當前調用函數的返回地址,也就是caller()的下一條指令的地址。這個地址位於ReflectiveLoader的內部,而ReflectiveLoader位於被注入的DLL文件內部,所以這個地址離DLL文件的頭部不遠了。

藉助上文找到的地址,咱們逐字節的向上遍歷,當查找到符合PE格式的文件頭以後,就能夠認爲找到了DLL文件在內存中的地址了。

2)獲取所需的系統API。

ReflectiveLoader啓動時,目標進程已在正常的運行狀態中了,此時目標進程已經裝載了一些核心的DLL文件。咱們能夠搜索這些DLL文件,查找須要的API函數,爲後續操做提供方便。具體地,咱們須要的函數是kernel32.dll中的LoadLibraryA(), GetProcAddress(), VirtualAlloc()以及ntdll.dll中的NtFlushInstructionCache()函數。

ReflectiveLoader藉助PEB (ProcessEnvironment Block)來查找kernel32.dll和ntdll.dll在內存中的位置。這一部分須要對TEB (ThreadEnvironment Block)和PEB (Process Environment Block)有一個基本的瞭解,我在此簡略介紹一下。

每個線程都具備一個TEB結構,其中記錄了相關線程的一些基本信息。線程運行時,其FS段寄存器記錄了其TEB的位置。而在TEB結構的0×30偏移處記錄了PEB結構的指針,所以能夠經過以下代碼訪問PEB:

mov EAX, FS:[0x30]       //EAX指向了PEB結構。

PEB結構包含有65個成員,大小達到0×210個字節,在此就不細緻介紹了。須要注意的是,在PEB結構的0x0C偏移處,是一個指向PEB_LDR_DATA結構體的指針,其結構以下:

5.png

 

其中的三個LIST_ENTRY是三個鏈表,按照不一樣的順序規則將當前進程加載的全部模塊連接起來。經過遍歷其中的任意一個LIST_ENTRY,咱們就能夠得到全部模塊的基地址,具體方法就不細緻闡述了。

在獲取了模塊基地址以後,經過對PE文件的解析,找到DLL文件的導出表,再根據導出表就能夠找到任一導出函數的地址了。對PE文件的解析有太多文章,這裏也不細緻闡述了。

在此,咱們獲得了函數LoadLibraryA(), GetProcAddress(), VirtualAlloc()以及NtFlushInstructionCache()。它們將在以後被用到。

 

3) 分配一片用來裝載DLL的空間。

雖然在ReflectiveLoader運行時,DLL文件已經在進程內存中了,可是要裝載這個DLL,咱們還須要更大的空間。藉助在第2)步獲得的函數VirtualAlloc(),咱們能夠分配一片更大的內存空間用於加載DLL。在PE頭中的IMAGE_OPTIONAL_HEADER結構體中的SizeOfImage成員記載DLL被裝載後的大小,咱們按照這個大小分配內存便可。

 

uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );

uiBaseAddress記錄了VirtualAlloc的返回值,也就是分配內存空間的起始地址。因而uiBaseAddress就成爲了DLL被裝載後的基地址。

4) 複製PE文件頭和各個節

分配了用於裝載的空間後,ReflectiveLoader將DLL文件的頭部(也就是DOS文件頭、DOS插樁代碼和PE文件頭)複製到新的空間的首部。再根據PE文件的節表將各個節複製到相應的位置中.

5) 處理DLL的引入表

被注入的DLL可能還依賴於其餘的DLL,所以咱們還須要裝載這些被依賴的DLL,並修改本DLL的引入表,使這些被引入的函數能正常運行。

PE文件的引入表是一個元素爲IMAGE_IMPORT_DESCRIPTOR的數組。每個被依賴的DLL都對應着數組中的一個元素。下圖表示了IMAGE_IMPORT_DESCRIPTOR結構以及咱們須要進行的處理。

3.png

咱們要作的就是根據IMAGE_IMPORT_DESCRIPTOR中的NAME成員找到DLL的名稱,根據名稱裝載這些被依賴的DLL。 IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk指示了要從該DLL中引入哪些函數。有的函數是由名稱導入的,此時IMAGE_THUNK_DATA會指向這個函數名;有的函數是由函數序號導入,此時分析IMAGE_THUNK_DATA咱們會獲得這個序號。不管是以什麼方式導入,咱們都要須要找到對應的函數,而後將其地址填入FirstThunk指向的IMAGE_THUNK_DATA數組中。裝載這些被依賴的DLL就不須要咱們手工操做了,咱們直接利用步驟2)中得到的LoadLibraryA()來裝載它們。對於那些經過函數名導入的函數來講,咱們能夠直接用GetProcAddress()來獲得它們的地址;而對於經過序數導入的函數來講,則須要咱們再次手工分析PE文件的導出表來找到它們的位置。

在獲得所需的函數的地址後,將它們填入上圖的相應位置,這樣咱們就完成了對引入表的處理了。

 

 

 

 

6) 對DLL進行重定位

 

 

被注入的DLL只有其ReflectiveLoader中的代碼是故意寫成地址無關、不須要重定位的,其餘部分的代碼則須要通過重定位才能正確運行。幸運的是DLL文件提供了咱們進行重定位所需的全部信息,這是由於每個DLL都具備加載不到預約基地址的可能性,因此每個DLL都對自身的重定位作好了準備。

 

 

PE可選印象頭的DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]就指向了重定位表。重定位表的數據結構以下:

 

2.png

 

 

 

 

從定義上看,IMAGE_BASE_RELOCATION只包含了兩個DWORD,其實在內存中它以後還跟了若干個大小爲兩個字節的元素,就是定義中被註釋掉的「WORD Typeoffset[1]「。IMAGE_BASE_RELOCATION結構和後面緊跟的若干個Typeoffset組成了一個塊,其大小爲結構體中的SizeOfBlock。所以,Typeoffset的數量能夠根據SizeofBlock算出。當一個塊結束時,後面緊跟的就是下一個塊。若SizeofBlock爲0則標誌着重定位表結束了。

 

1.png

 

 

 

 

Typeoffset的高4位表明重定位類型,通常爲3,低12位則表示重定位地址。這個地址和IMAGE_BASE_RELOCATION中的VirtualAddress加起來則指向一個須要重定位的指令。

 

 

找到須要重定位的地點以後,怎麼重定位呢?前文說到Typeoffset指示了多種重定位類型,其中最多見的爲3,在此我只介紹這種狀況。其餘重定位類型的主體思想基本是類似的,只有細微的不一樣。

 

 

咱們首先計算獲得基地址的偏移量,也就是實際的DLL加載地址減去DLL的推薦加載地址。DLL推薦加載地址保存在NT可選印象頭中的ImageBase成員中,而實際DLL加載地址則是咱們在第3)步中函數VirtualAlloc()的返回值。而後咱們將VirtualAddress和Typeoffset協力組成的地址所指向的雙字加上這個偏移量,重定位就完成了。

 

 

 

 

*(DWORD*)(VirtualAddress + Typeoffset的低12位) += (實際DLL加載地址 – 推薦DLL加載地址)

 

 

 

 

 

在完成全部的重定位後,咱們最後調用第2)步獲得的NtFlushInstructionCache()清除指令緩存以免問題。

 

 

 

 

7) 調用DLL入口點

 

 

至此,ReflectiveLoader的任務所有完成,最後它將控制權轉交給DLL文件的入口點,這個入口點能夠經過NT可選印象頭中的AddressOfEntryPoint找到。通常地,它會完成C運行庫的初始化,執行一系列安全檢查並調用dllmain。

 

 

0×04 總結

 

 

反射式DLL注入是一種新型的DLL注入方式,它不須要像傳統的注入方式同樣須要DLL落地存儲,避免了注入DLL被安全軟件刪除的危險。因爲它沒有經過系統API對DLL進行裝載,操做系統無從得知被注入進程裝載了該DLL,因此檢測軟件也沒法檢測它。同時,因爲操做流程和通常的注入方式不一樣,反射式DLL注入被安全軟件攔截的機率也會比通常的注入方式低。

 

 

反射式DLL注入的實現中運用了大量對PE文件結構的解析。瞭解,以及動手實踐這個注入方式會讓您對PE文件格式,PE文件加載的理解更加深入。

相關文章
相關標籤/搜索