繞過用戶模式EDR Hook原理及思路
1.什么是系統(tǒng)調(diào)用
系統(tǒng)調(diào)用是從用戶模式過渡到內(nèi)核模式的標(biāo)準(zhǔn)方式。它們是現(xiàn)代版的軟件中斷,速度更快。
系統(tǒng)調(diào)用接口極其復(fù)雜,但由于大部分內(nèi)容與我們的工作無關(guān),我只想做一個(gè)較高層次的總結(jié)。在大多數(shù)情況下,你并不需要深入了解它是如何工作的,就可以使用這些技術(shù),但了解一下還是有幫助的。
在 Windows 中,內(nèi)核有一張?jiān)试S從用戶模式調(diào)用的函數(shù)表。這些函數(shù)有時(shí)被稱為系統(tǒng)服務(wù)、本地函數(shù)或 Nt 函數(shù)。它們是以 Nt 或 Zw 開頭的函數(shù),位于 ntoskrnl.exe 中。系統(tǒng)服務(wù)表稱為系統(tǒng)服務(wù)描述符表,簡稱 SSDT。
要從用戶模式調(diào)用系統(tǒng)服務(wù),必須執(zhí)行系統(tǒng)調(diào)用,通過 syscall 指令完成。應(yīng)用程序?qū)⑾到y(tǒng)服務(wù) ID 保存在 eax 寄存器中,以此告訴內(nèi)核要調(diào)用哪個(gè)系統(tǒng)服務(wù)。系統(tǒng)服務(wù) ID(通常稱為系統(tǒng)服務(wù)號(hào)、系統(tǒng)調(diào)用號(hào)或簡稱 SSN)是該函數(shù)在 SSDT 中的索引項(xiàng)。因此,將 eax 設(shè)置為 0 將調(diào)用 SSDT 中的第一個(gè)函數(shù),1 將調(diào)用第二個(gè)函數(shù),2 將調(diào)用第三個(gè)函數(shù),依此類推...
查詢結(jié)果如下:entry = nt!KiServiceTable+(SSN * 4)。
syscall 指令會(huì)使 CPU 切換到內(nèi)核模式并調(diào)用系統(tǒng)調(diào)用處理程序,該程序會(huì)從 eax 寄存器中獲取 SSN 并調(diào)用相應(yīng)的 SSDT 函數(shù)。
假設(shè)一個(gè)應(yīng)用程序調(diào)用 kernel32.dll 中的 OpenProcess() 函數(shù)來打開一個(gè)進(jìn)程的句柄。
圖片
正如你所看到的,該函數(shù)的真正作用是調(diào)用位于 ntdll.dll 中的 NtOpenProcess()?,F(xiàn)在,讓我們來看看 NtOpenProcess() 的邏輯。
圖片
在 NtOpenProcess() 中,幾乎沒有任何代碼。這是因?yàn)榕c所有以 Nt 或 Zw 開頭的函數(shù)一樣,NtOpenProcess() 實(shí)際上位于內(nèi)核中。這些函數(shù)的 ntdll(用戶模式)版本只是執(zhí)行系統(tǒng)調(diào)用來調(diào)用其內(nèi)核模式對(duì)應(yīng)函數(shù),這就是為什么它們經(jīng)常被稱為系統(tǒng)調(diào)用存根。
在我們的例子中,NtOpenProcess 的 SSN 是 0x26,但這個(gè)數(shù)字會(huì)隨著 Windows 版本的變化而變化,所以不要指望它對(duì)你來說也是一樣的。從簡化的高層視圖來看,調(diào)用流程大致如下:
圖片
下面是關(guān)于 x86 系統(tǒng)調(diào)用流程的更詳細(xì)概述:
圖片
注意:在用戶模式下,函數(shù)的 Nt 和 Zw 版本完全相同。在內(nèi)核模式下,Zw 函數(shù)的運(yùn)行路徑略有不同。這是因?yàn)?Nt 函數(shù)是為從用戶模式調(diào)用而設(shè)計(jì)的,因此要對(duì)函數(shù)參數(shù)進(jìn)行更廣泛的驗(yàn)證。
2.EDR和用戶模式鉤子
自 2005 年微軟推出內(nèi)核補(bǔ)丁保護(hù)(又稱 PatchGuard)以來,許多對(duì)內(nèi)核的修改現(xiàn)在都被阻止了。以前,安全產(chǎn)品通過掛鉤 SSDT 從內(nèi)核內(nèi)部監(jiān)控用戶模式調(diào)用。由于所有 Nt/Zw 功能都是在內(nèi)核中實(shí)現(xiàn)的,因此所有用戶模式調(diào)用都必須通過 SSDT,并因此受到 SSDT 掛鉤的影響。補(bǔ)丁防護(hù)使 SSDT 鉤子成為禁區(qū),因此許多 EDR 鉤子轉(zhuǎn)向掛鉤 ntdll。
圖片
由于 SSDT 存在于內(nèi)核中,因此用戶模式應(yīng)用程序無法在不加載內(nèi)核驅(qū)動(dòng)程序的情況下干擾這些鉤子。現(xiàn)在,鉤子被放置在用戶模式下,與應(yīng)用程序并存。
那么,用戶模式鉤子是什么樣的呢?
圖片
要掛接 ntdll.dll 中的函數(shù),大多數(shù) EDR 只需用 jmp 指令覆蓋函數(shù)代碼的前 5 個(gè)字節(jié)。jmp 指令會(huì)將代碼執(zhí)行重定向到 EDR 自身 DLL(會(huì)自動(dòng)加載到每個(gè)進(jìn)程中)中的某些代碼。CPU 被重定向到 EDR 的 DLL 后,EDR 可以通過檢查函數(shù)參數(shù)和返回地址來執(zhí)行安全檢查。一旦 EDR 完成檢查,它就可以通過執(zhí)行覆蓋指令恢復(fù) ntdll 調(diào)用,然后跳轉(zhuǎn)到鉤子(jmp 指令)之后的 ntdll 位置。
圖片
在上例中,NtWriteFile 被掛鉤。綠色指令是 NtWriteFile 的原始指令。NtWriteFile 的前 3 條指令已被 EDR 的鉤子(將執(zhí)行重定向到 edr.dll 中名為 NtWriteFile 的函數(shù)的 jmp)覆蓋。每當(dāng) EDR 想要調(diào)用真正的 NtWriteFile 時(shí),它會(huì)執(zhí)行 3 條被覆蓋的指令,然后跳轉(zhuǎn)到掛鉤函數(shù)的第 4 條指令,完成系統(tǒng)調(diào)用。
雖然不同廠商的 EDR 掛鉤可能略有不同,但原理仍然相同,而且都有一個(gè)共同的弱點(diǎn):它們都位于用戶模式下。由于鉤子和 EDR 的 DLL 都必須放在每個(gè)進(jìn)程的地址空間內(nèi),因此惡意進(jìn)程可以篡改它們。
3.繞過EDR鉤子
繞過 EDR 鉤子的方法有很多,我只介紹主要的幾種。
卸載EDR鉤子
由于掛鉤的 ntdll 位于我們自己進(jìn)程的內(nèi)存中,因此我們可以使用 VirtualProtect() 使內(nèi)存可寫,然后用原始函數(shù)代碼覆蓋 EDR 的 jmp 指令。為了替換鉤子,我們當(dāng)然需要知道原來的匯編指令是什么。最常見的方法是從磁盤讀取 ntdll.dll 文件,然后將內(nèi)存版本與磁盤版本進(jìn)行比較。前提是 EDR 不會(huì)檢測或阻止從磁盤手動(dòng)讀取 ntdll.dll。
這種方法的主要缺點(diǎn)是,EDR 可以定期檢查 ntdll 的內(nèi)存,查看其鉤子是否已被刪除。如果 EDR 檢測到其鉤子已被移除,它可能會(huì)將鉤子寫回,更有甚者會(huì)終止進(jìn)程并觸發(fā)檢測事件。雖然鉤子可能需要放在用戶模式下,但檢查鉤子可以在內(nèi)核模式下進(jìn)行,因此我們也沒有什么辦法來防止這種情況發(fā)生。
手動(dòng)映射DLL
與其從磁盤中讀取 ntdll 的純凈拷貝來解鎖原始 ntdll,我們還不如直接將純凈拷貝加載到進(jìn)程內(nèi)存中,然后使用它來代替原始 ntdll。由于 LoadLibrary() 和 LdrLoadDll() 等函數(shù)不允許系統(tǒng)兩次加載同一個(gè) DLL,所以我們必須手動(dòng)加載。手動(dòng)映射 DLL 的代碼可能會(huì)很繁雜,而且容易出錯(cuò)或被檢測到。
DLL 通常也會(huì)調(diào)用其他 DLL,因此我們要么只能使用手動(dòng)加載的 ntdll 中的函數(shù),要么為我們需要的每個(gè) DLL 加載第二個(gè)副本,并修補(bǔ)它們,使其只能使用其他手動(dòng)加載的 DLL,這可能會(huì)變得非?;靵y。如果殺毒軟件在進(jìn)行內(nèi)存掃描時(shí),發(fā)現(xiàn)每個(gè) DLL 都有多個(gè)副本加載到內(nèi)存中,那么也很有可能被發(fā)現(xiàn)。
直接系統(tǒng)調(diào)用
正如前面討論的那樣,用戶模式下的Nt/Zw函數(shù)實(shí)際上除了執(zhí)行系統(tǒng)調(diào)用之外并不執(zhí)行其他任何操作。因此,我們實(shí)際上不需要映射整個(gè)新的ntdll副本來執(zhí)行一些系統(tǒng)調(diào)用。相反,我們可以直接將系統(tǒng)調(diào)用邏輯實(shí)現(xiàn)到我們自己的代碼中。我們只需將要調(diào)用的函數(shù)的SSN(函數(shù)號(hào))移動(dòng)到eax寄存器中,然后執(zhí)行syscall指令。
__asm {
mov r10, rcx
mov eax, 0x123
syscall
ret
}
不幸的是,由于EDR的鉤子通常會(huì)覆蓋設(shè)置eax寄存器的指令,我們不能簡單地從被掛鉤的函數(shù)中提取它。但是...有一些方法我們可以找出它是什么。
從ntdll讀取一個(gè)干凈的拷貝
你可能已經(jīng)對(duì)這個(gè)想法感到厭倦了,但我們可以從磁盤上讀取一個(gè)干凈的ntdll副本,然后從中提取SSN。由于SSN始終被放入eax寄存器,我們只需掃描我們想要調(diào)用的函數(shù)以找到"mov eax, imm32"指令即可。但是,如果我們想要一種不僅僅是從磁盤讀取ntdll的變體呢?別擔(dān)心!
根據(jù)函數(shù)順序計(jì)算系統(tǒng)調(diào)用號(hào)
系統(tǒng)調(diào)用ID是索引,因此是順序的。如果我們想要調(diào)用的函數(shù)的SSN是0x18,那么直接在它之前的可能是0x17,直接在它之后的可能是0x19。由于EDR并不掛鉤每個(gè)Nt函數(shù),我們可以簡單地從最近的未被掛鉤的函數(shù)中獲取SSN,然后通過添加或減去在它和我們目標(biāo)函數(shù)之間有多少個(gè)函數(shù)來計(jì)算我們想要的函數(shù)的SSN。
圖片
這種方法確實(shí)有一個(gè)缺陷:我們無法百分之百地保證系統(tǒng)調(diào)用號(hào)將永遠(yuǎn)保持連續(xù),或者DLL不會(huì)跳過一些。
硬編碼
最簡單的方法就是直接硬編碼系統(tǒng)調(diào)用號(hào)。雖然它們在不同版本之間會(huì)有所改變,但在過去它們的變化并不是很大。檢測操作系統(tǒng)版本并加載正確的SSN集并不是太難的工作。事實(shí)上,j00ru友好地發(fā)布了每個(gè)Windows版本的每個(gè)系統(tǒng)調(diào)用號(hào)的列表。這種方法唯一的缺點(diǎn)是,如果系統(tǒng)調(diào)用號(hào)發(fā)生變化,代碼可能在新的Windows版本上無法自動(dòng)運(yùn)行。
直接系統(tǒng)調(diào)用的問題
在過去的十多年里,直接系統(tǒng)調(diào)用一直是繞過用戶模式鉤子的首選方法。實(shí)際上,我自己在2012年初次嘗試了這種方法。不幸的是,為了防止這種繞過方式,已經(jīng)進(jìn)行了很多工作。最常見的檢測方法是讓EDR的內(nèi)核模式驅(qū)動(dòng)程序檢查調(diào)用堆棧。
盡管EDR不能再在內(nèi)核中掛鉤很多地方,但它可以利用操作系統(tǒng)提供的監(jiān)視功能,比如:
- ETW事件
- 內(nèi)核回調(diào)
- 過濾驅(qū)動(dòng)程序
如果我們執(zhí)行手動(dòng)系統(tǒng)調(diào)用,而在調(diào)用的內(nèi)核函數(shù)經(jīng)過以上任何一種情況時(shí),EDR可以利用機(jī)會(huì)檢查我們線程的調(diào)用堆棧。通過展開調(diào)用堆棧并檢查返回地址,EDR可以看到導(dǎo)致此系統(tǒng)調(diào)用的整個(gè)函數(shù)調(diào)用鏈。
如果我們執(zhí)行對(duì)kernel32!VirtualAlloc()的正常調(diào)用,調(diào)用堆??赡苋缦滤荆?/p>
圖片
在這種情況下,對(duì)VirtualAlloc()的調(diào)用是由ManualSyscall!main+0x53啟動(dòng)的。按照調(diào)用的順序,調(diào)用堆棧的相關(guān)部分如下:
- ManualSyscall!main+0x53
- KERNELBASE!VirtualAlloc+0x48
- ntdll!NtAllocateVirtualMemory+0x14
- nt!KiSystemServiceCopyEnd+0x25
這告訴我們(或者EDR)可執(zhí)行文件(ManualSyscall.exe)調(diào)用了VirtualAlloc(),這個(gè)函數(shù)調(diào)用了NtAllocateVirtualMemory(),然后執(zhí)行了一個(gè)系統(tǒng)調(diào)用以切換到內(nèi)核模式。
現(xiàn)在讓我們看看進(jìn)行直接系統(tǒng)調(diào)用時(shí)的調(diào)用堆棧:
圖片
調(diào)用堆棧的相關(guān)部分按順序如下:
- ManualSyscall!direct_syscall+0xa
- nt!KiSystemServiceCopyEnd+0x25
在這里,很明顯內(nèi)核轉(zhuǎn)換是由ManualSyscall.exe內(nèi)部的代碼觸發(fā)的,而不是ntdll。但是,這有什么問題嗎?
嗯,在像Linux這樣的系統(tǒng)上,應(yīng)用程序直接發(fā)起系統(tǒng)調(diào)用是完全正常的。但請(qǐng)記住我提到過Windows版本之間系統(tǒng)調(diào)用號(hào)會(huì)發(fā)生變化嗎?結(jié)果,編寫依賴于直接系統(tǒng)調(diào)用的Windows軟件是非常不切實(shí)際的。由于ntdll已經(jīng)為您實(shí)現(xiàn)了每個(gè)系統(tǒng)調(diào)用,幾乎沒有理由進(jìn)行手動(dòng)系統(tǒng)調(diào)用。除非你正在編寫繞過EDR鉤子的惡意軟件。你是在寫用于繞過EDR鉤子的惡意軟件嗎?
由于直接系統(tǒng)調(diào)用是惡意活動(dòng)的強(qiáng)有力指標(biāo),更復(fù)雜的EDR系統(tǒng)將記錄源自于ntdll之外的系統(tǒng)調(diào)用的檢測情況。說實(shí)話,你仍然可以在很多時(shí)候逃脫檢測,但這有什么樂趣呢?
4.間接系統(tǒng)調(diào)用
大多數(shù)EDR在Nt函數(shù)的開頭寫入它們的鉤子,覆蓋SSN但保留系統(tǒng)調(diào)用指令不變。這使我們能夠利用ntdll已經(jīng)提供的系統(tǒng)調(diào)用指令,而不是引入我們自己的。我們只需自己設(shè)置r10和eax寄存器,然后跳轉(zhuǎn)到被掛鉤的ntdll函數(shù)內(nèi)的系統(tǒng)調(diào)用指令(位于EDR鉤子之后)。
圖片
注意:我們并不嚴(yán)格需要test或jnz指令,它們只是為了向后兼容。一些古老的CPU不支持syscall指令,而是使用int 0x2e。test指令檢查系統(tǒng)調(diào)用是否啟用,如果沒有啟用,則回退到軟中斷。如果我們希望支持這些系統(tǒng),我們可以自己執(zhí)行檢查,然后根據(jù)需要跳轉(zhuǎn)到int 0x2e指令(也位于Nt函數(shù)內(nèi))。
就像直接系統(tǒng)調(diào)用一樣,我們?nèi)匀恍枰到y(tǒng)調(diào)用號(hào)放入eax寄存器,但我們可以使用在直接系統(tǒng)調(diào)用部分詳細(xì)介紹的所有相同技術(shù)。
通過這種方式設(shè)置系統(tǒng)調(diào)用將給我們一個(gè)類似以下的調(diào)用堆棧:
圖片
正如你所看到的,調(diào)用堆棧現(xiàn)在看起來好像是來自ntdll!NtAllocateVirtualMemory()而不是我們的可執(zhí)行文件,因?yàn)閺募夹g(shù)上講確實(shí)是這樣的。
我們可能會(huì)遇到的一個(gè)問題是,如果EDR鉤子或覆蓋了Nt調(diào)用中的syscall指令的部分。我從未見過這種情況發(fā)生,但理論上可能會(huì)發(fā)生。在這種情況下,我們可以跳轉(zhuǎn)到另一個(gè)未被掛鉤的Nt函數(shù)內(nèi)的syscall指令。這仍然可以繞過僅驗(yàn)證調(diào)用名稱是否來自ntdll的EDR,但對(duì)于檢查內(nèi)核函數(shù)是否與來自ntdll的函數(shù)相匹配的這種檢查通常都會(huì)失敗。
更大的問題是,如果EDR檢查的不僅僅是第一個(gè)返回地址。不僅僅是系統(tǒng)調(diào)用的來源,還有執(zhí)行系統(tǒng)調(diào)用的函數(shù)是誰調(diào)用的。如果我們正在從位于動(dòng)態(tài)分配內(nèi)存中的某個(gè)shellcode進(jìn)行間接系統(tǒng)調(diào)用,那么EDR將會(huì)察覺到。來自于有效PE節(jié)(exe或DLL內(nèi)存)之外的調(diào)用是相當(dāng)可疑的。
此外,由于函數(shù)被EDR掛鉤,EDR的鉤子預(yù)期會(huì)出現(xiàn)在調(diào)用堆棧中。實(shí)際上,我并不確定哪些EDR,如果有的話,會(huì)檢查這一點(diǎn)。但是,正如你在這里看到的,從調(diào)用堆棧中很明顯我們繞過了EDR的鉤子。
圖片
理想情況下,我們希望偽造的不僅僅是系統(tǒng)調(diào)用的返回地址。對(duì)此的一個(gè)有趣解決方案是調(diào)用堆棧欺騙,我可能會(huì)在另一篇文章中詳細(xì)介紹。使用調(diào)用堆棧欺騙,可以偽造整個(gè)調(diào)用堆棧,但要保持調(diào)用堆棧穩(wěn)定不崩潰可能會(huì)遇到一些挑戰(zhàn)。