高級進程注入總結

本文將重點描述值得留意的方法(比如Module Stomping),和一些思路新穎的方法。
1
進程注入簡介
進程注入就是將代碼注入到另一個進程中,shellcode注入和DLL注入都屬于進程注入。
進程注入的作用是隱藏自身或者利用另一個進程的權限做一些不好的事情,比如病毒等惡意程序注入到一個正常的進程,以此隱藏自己。
進程注入的方法非常之多,很多與DLL注入有關,比如注冊表(Image File Execution Options)、DLL劫持、輸入法、COM、LSP劫持(LayerService Provider,與winsock有關)
除了DLL注入,還有shellcode注入,因為shellcode更小,所以shellcode的使用也更加多樣。
2017年,在黑客大會上Eugene Kogan和Tal Liberman又分享了更加隱蔽和特別的方法,比如Process Doppelganging。那么接下來就開始進程注入方法的介紹。
2
Module Stomping
該方法通過在目標進程中加載一個合法DLL,然后將shellcode或惡意DLL覆寫到這個合法DLL的地址空間里。
偽代碼如下(省略錯誤檢查):
HANDLE hTargetHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_ALL_ACCESS | PROCESS_SUSPEND_RESUME, FALSE, targetPid);//... // Use OpenProcess + VirtualAllocEx + CreateRemoteThread(the traditional dll injection method)injectLoadLibrary(hTargetHandle, legitimateDll);//... // Copy every malware dll's section into legitimate dll's corresponding sections within the target process address space.for(section : malwareDll){ WriteProcessMemory(hTargetHandle, legitimateDllSection, section, section_size...); if (has executable section) BypassCFG();} RebuildImportTable();RebuildRelocationTable(); callTLSCallback(); CreateRemoteThread(malwareDllEntryPoint);
通過該方法可以將惡意代碼隱藏,使得Defender掃描目標進程空間時不會發現惡意代碼的存在(因為惡意代碼潛伏在合法的模塊里)。
該方法有兩個值得關注的點:
- 先在目標進程加載一個合法DLL,以此隱藏惡意代碼。
- 它用了SetProcessValidCallTargets,該方法可以bypass開啟CFG的程序。
與CFG(control flow guard)相關的知識可參考MSDN或者System Internals 7th part1 security chapter。
不過該方法還是有明顯的不足,比如會調用顯眼的VirtualAllocEx、WriteProcessMemory、VirtualProtectEx和CreateRemoteThread,通常的EDR都能檢測出可能的惡意行為。
該方法的詳細細節可參考此篇文章:Hiding malicious code with “Module Stomping”: Part 1 - F-Secure Blog
3
Process Hollowing
該方法也比較經典,并被廣泛使用,其基本流程如下:
CreateProcess(“svchost.exe” , …, CREATE_SUSPENDED, …);NtUnmapViewOfSection(…) and VirtualAllocEx(…);For each section: WriteProcessMemory(..., EVIL_EXE, …);Relocate Image*;Set base address in PEB*;SetThreadContext(…);ResumeThread(…);
該方法和Module Stomping很像,都是替換掉了一個模塊,不過有以下三個不同:
- Process Hollowing替換的模塊是目標EXE程序
- 該方法使用NtUnmapViewOfSection將EXE程序解除映射
- 用ResumeThread來啟動惡意程序的入口點
該方法的詳細實現可參考:Process Hollowing and Portable Executable Relocations - Red Teaming Experiments (ired.team)
4
Process Doppelganging
接下來描述的幾個進程注入方法使用了不同的注入思路,它們之間只有細微的差別,最后會總結它們的異同點。
該方法是2017年在黑客大會介紹的,演講者首先說明了Process Hollowing涉及的敏感操作和一些不足:
- Unmap目標進程的EXE模塊(非常可疑),現代的安全檢查一般都有Unmap檢查。
- 如果不Unmap,而是直接覆寫程序,那么覆寫地址的頁屬性就不是共享的,也很可疑。
頁屬性共享涉及到操作系統的共享內存機制,這里簡單描述一下:
為了節省內存,當一個模塊加載進內存時,比如ntdll.dll,操作系統會看該模塊是否已被其他進程加載過了(即已經在物理內存中了),如果在,那么操作系統只需要簡單地將該物理區域映射到需要加載模塊的進程中。因此,這些模塊對應的內存是共享的,這些模塊內存對應的進程pte(page table entry)會指向一個叫prototypePTE的結構,該結構會指向這些共享的物理內存。
不過,如果我們向模塊,比如ntdll.dll,的數據段寫入數據,那么這時操作系統就會對ntdll.dll的數據段分配一段物理內存,然后當前進程對應數據段的pte就不會再指向prototypePTE,而是指向操作系統分配的物理內存。
關于內存共享機制的詳細描述,讀者看參考Windows Internals 7th,第五章的Page fault handling部分。
直接覆寫程序不夠好,那就Unmap后再Remap:
- 如果remap時的類型不是IMAGE,通過檢查節的類型可判定是否可疑
- 如果remap時的類型是IMAGE,這時可疑的點就不多了。不過因為Process Hollowing用SetThreadContext修改了初始線程的執行入口點(ETHREAD.Win32StartAddress),那么我們可以檢測其執行入口點是否是ETHREAD.Win32StartAddress。如果不是,那值得懷疑,并且我們可以檢測其執行入口點對應的文件名,這樣可進一步判定這段內存是否是可疑payload。
檢測文件名可通過該字段查看:VAD(ETHREAD.Win32StartAddress).Subsection.ControlArea.FilePointer。
另外,如果要使用Remap,那就需要一個section,打開section需要一個文件句柄,也就是說Remap需要一個落地的文件,因此采取process hollowing時,攻擊者很少會使用Remap的方式。
綜上所述,Process Hollowing已經不是那么好了,那還有什么其他更隱蔽的方法嗎?
于是演講者介紹了Process Doppelganging,該方法由CreateFileTransactedAPI開始。
由于其內容較多,且不易描述,讀者可查看這個PPT和這個源碼和這篇文章,這些資料講得非常詳細。
這里歸納了簡要的流程:
- 創建一個transaction(事務)
- 打開原程序句柄(通過CreateFileTransacted)
- 向原程序句柄寫入惡意代碼,根據此時的文件內容,創建一個section
- rollback(回滾)之前的寫操作
雖然回滾了文件的內容,但已生成的section映射的內容是修改后的,即內容是payload,解釋可參考Process Herpaderping小節。
- 通過剛剛創建的section,創建進程(通過NtCreateProcessEx)
- 準備參數到目標進程(跨進程)
- 創建初始線程(NtCreateThreadEx),之后喚醒線程(NtResumeThread)
以上流程可抽象如下:
transact -> write -> map -> rollback -> execute
該方法的新穎點在于通過Windows提供的事務API,將惡意代碼寫入打開的文件,并創建一個section,用其創建進程,之后回滾寫入操作。
這樣可以隱藏執行的惡意代碼,雖然你查看該進程時(比如procExp),其顯示的是原程序的信息,但其真正執行的代碼是惡意代碼。
同時,它比Process Hollowing更隱蔽,因為它不用Unmap,也不需要Remap,它就像正常啟動一個進程一樣。
最近,我編譯了這份代碼,發現win10、win8.1和win7都失敗了,說明windows已經patch了該方法,以下是win10和win8.1測試的結果:
- 如果覆蓋用的是非PE文件,NtCreateSection返回錯誤,提示無效的PE。
- 如果覆蓋用的是PE文件,創建進程成功,不過我們在procExp中看到的是名叫System Idle Process,觀察該進程信息,其提示“請求的操作是在不再活動的事務的上下文中進行的”。

雖然該方法已經失效了,不過它的思路很好。之后介紹的Process Herpaderping借鑒了該方法,且目前也是有效的。
可能我實踐時,代碼的參數沒設置好,所以沒有復現demo里的結果。從現在的惡意攻擊看,基于Process Doppelganging的攻擊很少,不知道是不是因為被patch了。不管怎么說,該方法是一個很好的起點。
關于該方法的描述詳情,讀者可參考:Process Doppelg?nging – a new way to impersonate a process | hasherezade's 1001 nights (wordpress.com)
5
Transacted Hollowing
該方法借鑒了Process Doppelganging的事務特性和Process Hollowing啟動進程的便捷性。
通過事務特性,可以更好的隱藏惡意代碼;通過便捷性,可以免去Process Doppelganging創建進程、準備進程參數的復雜過程。
該方法的大致流程是:
- 采用Process Doppelganging的前半段,transact -> write -> map -> rollback
- Remap惡意代碼的section到目標進程
- 采用process hollowing的技巧,通過SetThreadContext和ResumeThread的執行惡意代碼
在Process Doppelganging小節,我們講到process hollowing如果要Remap,需要有一個落地的文件。
通過事務的回滾,可以免去這個落地的文件。因此,我們可以把Transacted Hollowing當做是增強版的process hollowing。
關于該方法的描述細節,可參考:Process Doppelg?nging meets Process Hollowing in Osiris dropper | Malwarebytes Labs
6
Process Ghosting
講該方法之前,我們先說一下背景知識。
在windows操作系統里,如果我們映射了可執行程序,那么可執行程序就不應該被修改,如果嘗試修改,則返回錯誤。
但這也是一個限制,即只針對映射過的可執行程序不能被修改,也就是說我們可以打開一個文件、對其設置刪除標志、寫payload到文件,之后映射文件,最后刪除這個文件。
注:如果我們以GENERIC_READ和GENERIC_WRITE打開文件,那么映射可執行程序之后,我們還是可以修改文件,這在Process Herpaderping將會體現。
該方法與Process Doppelganging相似,它們的目的都是做的比process hollowing更隱蔽,該方法與Process Doppelganging從實現上幾乎一樣,唯一的區別就是處理不落地文件的方式:
- Process Doppelganging:通過事務API打開文件,修改文件(寫入payload),創建section,再回滾修改的內容。
- Process Ghosting:打開文件,設置刪除標志,修改文件(寫入payload),創建section,刪除文件。這樣進程運行時,反病毒軟件打不開文件,因此無法做檢測。
調用GetProcessImageFileName會返回空字符串。
關于該方法的描述細節,可參考:What you need to know about Process Ghosting, a new executable image tampering attack | Elastic Blog
7
Ghostly Hollowing
了解完Process Ghosting,我們來看Ghostly Hollowing。與Transacted Hollowing類似,該方法也是為了免去創建進程和準備進程參數的復雜過程。于是可以得到以下結論:

8
Process Herpaderping
這是本文描述的最后一個方法,因此我們先來一個方法小總結:

該方法的原理、實現都和Ghosting、Doppelganging類似,讀者可以把Ghosting和Herpaderping都理解為Doppelganging的變體。
Ghosting是刪除文件,Doppelganging是替換文件的內容(不替換文件),Herpaderping是替換文件和文件內容,其結果是反病毒軟件檢測執行的進程時,其打開的程序文件內容是我們設定的(比如lsass.exe,包括文件簽名)。
Herpaderping的流程如下:
- 打開一個可讀可寫的文件
- 向文件寫入payload(calc.exe),創建section
- 創建進程A(和Doppelganging一樣,使用NtCreateProcessEx)
- 向同一個文件寫入偽裝的程序,比如lsass.exe
- 關閉并保存文件為output.exe(文件保存至磁盤,磁盤的內容是lsass.exe)
- 準備進程參數,創建線程(這時payload開始執行)
一個有趣的現象是如果我們不關閉執行payload(計算器)的進程A,那么雙擊output.exe時會啟動另一個進程B,彈出另一個計算器。
其原因是進程A有一個section,這個section指向的文件路徑是output.exe,當我們啟動進程B時,操作系統發現路徑一樣,于是使用了進程A的section對應的SectionObjectPointer,以此實現文件的共享,也就是使用已經映射到內存的output.exe來啟動另一個計算器。
但如果我們打開output.exe文件,會發現內容又是lsass.exe的。因為文件映射到內存包括data和image類別,而讀文件是data類,所以data類對應的內存和image類對應的內存是分開的,也就是說操作系統的內存有兩份output.exe文件的數據。
下面貼一張關于進程A的section對應的SectionObject示意圖。

這里我們通過windbg講述剛剛的解釋。首先拿到herpaderping的demo源碼,用visual studio編譯完成后,我們啟動一個windbg,啟動命令如下:
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe" ProcessHerpaderping.exe "E:\my_knowledge\Reverse\Tools\CFF_Explorer\CFF Explorer.exe" E:\tmp\cpp_test_ano.exe
其中CFF Explorer.exe是要執行的payload(實際利用場景下,一般沒有這樣的落地文件,payload是惡意進程解密的出來的),cpp_test_ano.exe是一個合法的可讀可寫文件。
啟動命令后,我們在herpaderp.cpp的142行下斷點:
wil::unique_handle sectionHandle;auto status = NtCreateSection(§ionHandle, SECTION_ALL_ACCESS, nullptr, nullptr, PAGE_READONLY, SEC_IMAGE, targetHandle.get()); //windbg命令如下: bp `ProcessHerpaderping!herpaderp.cpp:142`
142行是創建section,類型是SEC_IMAGE。在創建section之前,我們觀察一下目標文件(targetHadnle)的句柄:

打開管理員權限的windbg,啟動本地內核調試,查看這個句柄,如下:
lkd> !process 0 1 ProcessHerpaderping.exePROCESS ffffac0f0ede7080...lkd> .process /p ffffac0f0ede7080Implicit process is now ffffac0f`0ede7080lkd> !handle ac...00ac: Object: ffffac0f2281a300 GrantedAccess: 0012019f (Protected) (Audit) Entry: ffff818476fff2b0Object: ffffac0f2281a300 Type: (ffffac0efc2d4d20) File ObjectHeader: ffffac0f2281a2d0 (new version) HandleCount: 1 PointerCount: 32655 Directory Object: 00000000 Name: \tmp\cpp_test_ano.exe {HarddiskVolume2}lkd> dt nt!_FILE_OBJECT ffffac0f2281a300... +0x028 SectionObjectPointer : 0xffffac0f`066a6358 _SECTION_OBJECT_POINTERS...lkd> dx -id 0,0,ffffac0f0ede7080 -r1 ((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358) : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *] [+0x000] DataSectionObject : 0xffffac0f0727b1d0 [Type: void *] [+0x008] SharedCacheMap : 0xffffac0f07911dc0 [Type: void *] [+0x010] ImageSectionObject : 0x0 [Type: void *]
因為我們打開并寫了目標文件,所以SectionObjectPointer的DataSectionObject不為空,即文件內容映射到了內存。
我們單步步過142行,再觀察一下SectionObjectPointer:
lkd> dx -id 0,0,ffffac0f0ede7080 -r1 ((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358) : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *] [+0x000] DataSectionObject : 0xffffac0f0727b1d0 [Type: void *] [+0x008] SharedCacheMap : 0xffffac0f07911dc0 [Type: void *] [+0x010] ImageSectionObject : 0xffffac0f0ebd3720 [Type: void *]
現在目標文件以Image(可執行程序)這一類別加載進了內存,因此內存中現在有兩份目標文件,一份是data類的,一份是image類的。
注意這兩類現在對應的內容是一樣的,之后ProcessHerpaderping會向目標文件再寫入數據,即修改data類所在的內存,然后關閉目標文件的句柄。
此時image類和data類的內容就不同了,但在windows的設計里這是不應該出現的,詳情可參考下面推薦的書。
執行ProcessHerpaderping的剩下部分,它會等待創建的cpp_test_ano.exe進程退出(實際執行的是CFF Explorer.exe)。
這時,如果我們用二進制編輯器打開cpp_test_ano.exe,會發現全是明文數據,不是可執行代碼:

如果我們雙擊cpp_test_ano.exe,會發現又彈出了一個CFF Explorer.exe進程,這時觀察我們剛剛創建的進程:
lkd> !process 0 1 cpp_test_ano.exePROCESS ffffac0f2b92f080...PROCESS ffffac0f1ccb6080(第二個是剛剛創建的進程)...lkd> dt nt!_EPROCESS sectionobject imagefilepointer ffffac0f1ccb6080 +0x3c0 SectionObject : 0xffff8184`7b06ecf0 Void +0x448 ImageFilePointer : 0xffffac0f`159a2180 _FILE_OBJECTlkd> dx -id 0,0,ffffac0f0ede7080 -r1 ((ntkrnlmp!_FILE_OBJECT *)0xffffac0f159a2180)((ntkrnlmp!_FILE_OBJECT *)0xffffac0f159a2180) : 0xffffac0f159a2180 [Type: _FILE_OBJECT *] [+0x028] SectionObjectPointer : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *]lkd> dx -r1 ((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358) : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *] [+0x000] DataSectionObject : 0xffffac0f0727b1d0 [Type: void *] [+0x008] SharedCacheMap : 0x0 [Type: void *] [+0x010] ImageSectionObject : 0xffffac0f0ebd3720 [Type: void *]
剛創建的進程對應的ImageSectionObject和之前在ProcessHerpaderping進程看到的結果一樣,代表剛創建的進程和ProcessHerpaderping啟動的cpp_test_ano.exe進程共享了image類對應的內存,共享了對應的CFF Explorer程序代碼。
關于上面的解讀,這涉及到windows操作系統對section的管理,比如文件的映射細節和文件的緩存管理。
有興趣的讀者可以參考 Windows Internals 7th part1 第五章內存管理的section小節和Windows Internals 7th part2第十一章緩存管理器部分。
該方法的作者用windbg進一步分析了共享的更多細節,可參考:herpaderping/DivingDeeper.md at main · jxy-s/herpaderping · GitHub
9
Conclusion
雖然進程注入在不斷更新,不過安全廠商也在與時俱進,目前很多安全廠商都有這些方法的監控了(比如上面提到的分析文章,大部分是安全廠商寫的)。從Process Doppelganging開始,我們能看到新的方法都是源于操作系統的不足,并慢慢衍生,可能以后的進程注入會越來越底層,越來越復雜。
本文涉及的細節很多,不能面面俱到,推薦讀者看本文各個小節的推薦文章,最后再來看本文,相信讀者會有更多的收獲。