反射式DLL注入研究


反射式DLL注入研究

https://www.freebuf.com/articles/system/151161.html

https://www.cnblogs.com/h2zZhou/p/7721797.html

https://github.com/haidragon/ReflectiveInjection

https://github.com/infosecguerrilla/ReflectiveSOInjection

 

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必須存在於文件系統中。這可怎么辦呢。

沒錯,我們需要拋棄LoadLibrary,自己來實現整個裝載過程!我們可以為待注入的DLL添加一個導出函數,ReflectiveLoader,這個函數實現的功能就是裝載它自身。那么我們只需要將這個DLL文件寫入目標進程的虛擬空間中,然后通過DLL的導出表找到這個ReflectiveLoader並調用它,我們的任務就完成了。

於是,我們的任務就轉到了編寫這個ReflectiveLoader上。由於ReflectiveLoader運行時所在的DLL還沒有被裝載,它在運行時會受到諸多的限制,例如無法正常使用全局變量等。而且,由於我們無法確認我們究竟將DLL文件寫到目標進程哪一處虛擬空間上,所以我們編寫的ReflectiveLoader必須是地址無關的。也就是說,ReflectiveLoader中的代碼無論處於虛擬空間的哪個位置,它都必須能正確運行。這樣的代碼被我們稱為“地址無關代碼”(position-independent code, PIC)。

0×02 注射器實現

要實現反射式注入DLL我們需要兩個部分,注射器和被注入的DLL。其中,被注入的DLL除了需要導出一個函數ReflectiveLoader來實現對自身的加載之外,其余部分可以正常編寫源代碼以及編譯。而注射器部分只需要將被注入的DLL文件寫入到目標進程,然后將控制權轉交給這個ReflectiveLoader即可。因此,注射器的執行流程如下:

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

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

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

至此,我們注射器的任務就已經完成了。下一步就是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結構體的指針,其結構如下:

其中的三個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結構以及我們需要進行的處理。

我們要做的就是根據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]就指向了重定位表。重定位表的數據結構如下:

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

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文件加載的理解更加深刻。

 

推薦注入的Windows進程: ( dwm.exe explorer.exe taskhostw.exe ApplicationFrameHost.exe )

PS C:\Users\LSGX> tasklist | findstr dwm
dwm.exe 1548 RDP-Tcp#7 1 122,140 K
dwm.exe 7872 Console 2 29,460 K
PS C:\Users\LSGX> tasklist | findstr taskhostw
taskhostw.exe 4500 RDP-Tcp#7 1 8,536 K
PS C:\Users\LSGX> tasklist | findstr ApplicationFrameHost
ApplicationFrameHost.exe 11344 RDP-Tcp#7 1 480 K
PS C:\Users\LSGX>

 

========= End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM