如何保護您的應用程序免受堆噴射技術的影響
為您的軟件建立強大的安全性至關重要。惡意行為者不斷使用各種類型的惡意軟件和網絡安全攻擊來破壞所有平臺上的應用程序。您需要了解最常見的攻擊并找到緩解它們的方法。
本文不是關于堆溢出或堆利用的教程。在其中,我們探討了允許攻擊者利用應用程序中的漏洞并執(zhí)行惡意代碼的堆噴射技術。我們定義什么是堆噴射,探索它的工作原理,并展示如何保護您的應用程序免受它的影響。
什么是堆噴射技術,它是如何工作的?
堆噴射是一種用于促進執(zhí)行任意代碼的漏洞利用技術。這個想法是在目標應用程序中的可預測地址上提供一個shellcode,以便使用漏洞執(zhí)行這個 shellcode。該技術是由稱為heap spray的漏洞利用源代碼的一部分實現的。
在實現動態(tài)內存管理器時,開發(fā)人員面臨許多挑戰(zhàn),包括堆碎片。一個常見的解決方案是以固定大小的塊分配內存。通常,堆管理器對塊的大小以及分配這些塊的一個或多個保留池有自己的偏好。堆噴射使目標進程連續(xù)地逐塊分配所需內容的內存,依靠將 shellcode 放置在所需地址的分配之一(不檢查任何條件)。
堆噴射本身不會利用任何安全問題,但它可用于使現有漏洞更容易被利用。
必須了解攻擊者如何使用堆噴射技術來了解如何緩解它。以下是普通攻擊的樣子:
堆噴射如何影響進程內存
堆噴射攻擊有兩個主要階段:
1.內存分配階段。一些流連續(xù)分配大量具有相同內容的固定大小的內存塊。
2.執(zhí)行階段。這些堆分配之一接收對進程內存的控制。
如您所見,堆噴射漏洞利用技術看起來像連續(xù)的垃圾郵件,形式為大小相同且內容相同的塊。如果堆噴射攻擊成功,控制權將傳遞給這些塊之一。
為了執(zhí)行這種攻擊,惡意行為者需要有機會在目標進程中分配大量所需大小的內存,并用相同的內容填充這些分配。這個要求可能看起來過于大膽,但最常見的堆噴射攻擊案例包括破壞Web 應用程序漏洞。任何支持腳本語言的應用程序(例如,帶有 Visual Basic 的 Microsoft Office)都是堆噴射攻擊的潛在受害者。
因此,在一個流的上下文中預期攻擊是有意義的,因為腳本通常在單個流中執(zhí)行。
但是,攻擊者不僅可以使用腳本語言執(zhí)行堆噴射攻擊。其他方法包括將圖像文件加載到進程中,并通過使用 HTML5 引入的技術以非常高的分配粒度噴射堆。
這里的問題是哪個階段可疑,我們可以干預并試圖弄清楚是否存在正在進行的攻擊?
內存分配階段,當一些流填滿大量內存時,已經很可疑了。但是,您應該問自己是否可能存在誤報。例如,您的應用程序中可能存在確實在一個循環(huán)中分配內存的腳本或代碼,例如數組或特殊內存池。當然,腳本在完全相同的堆塊中分配內存的可能性很小。但是,它仍然不是堆噴射的關鍵要求。
相反,您應該注意執(zhí)行階段,因為分析接收進程內存控制權的堆分配總是有意義的。因此,我們的分析將特別關注包含潛在 shellcode 的分配內存。
為了將堆噴射 shellcode 的執(zhí)行與普通JIT代碼生成區(qū)分開來,您可以分析分配某個內存塊的最新流分配,包括流中的相鄰分配。請注意,堆中的內存始終分配有執(zhí)行權限,這允許攻擊者使用堆噴射技術。
堆噴射緩解基礎知識
為了成功緩解堆噴射攻擊,我們需要管理接收內存控制的過程,應用鉤子,并使用額外的安全機制。
保護您的應用程序免受堆噴射執(zhí)行的三個步驟是:
1.攔截NtAllocateVirtualMemory調用
2.在嘗試分配可執(zhí)行內存期間使其無法執(zhí)行
3.注冊結構化異常處理程序 (SEH) 以處理由于執(zhí)行不可執(zhí)行內存而發(fā)生的異常
現在讓我們詳細探討每個步驟。
接收對內存的控制
我們既需要監(jiān)控目標進程如何分配內存,又需要檢測動態(tài)分配內存的執(zhí)行情況。后者假設在堆噴射期間分配的內存具有執(zhí)行權限。如果數據執(zhí)行保護 ( DEP ) 處于活動狀態(tài)(對于 x64,默認情況下始終處于活動狀態(tài))并且嘗試執(zhí)行沒有執(zhí)行權限分配的內存,則會生成異常訪問沖突。
惡意 shellcode 可以預期在沒有 DEP 的應用程序中執(zhí)行(這不太可能),或者使用腳本引擎在默認情況下具有執(zhí)行權限的堆中分配內存。
我們可以通過攔截可執(zhí)行內存的分配并以分配它的漏洞無法察覺的方式使其不可執(zhí)行來防止惡意代碼的執(zhí)行。因此,當漏洞利用認為噴射是安全的執(zhí)行并嘗試將控制權委托給噴射的堆時,將觸發(fā)系統(tǒng)異常。然后,我們可以分析這個系統(tǒng)異常。
首先,讓我們從用戶模式進程的角度來探索 Windows 中的內存工作是什么樣的。以下是通常分配大量內存的方式:
在哪里:
- HeapAlloc和RtlAllocateHeap是從堆中分配一塊內存的函數。
- NtAllocateVirtualMemory是一個低級函數,它是 NTDLL 的一部分,不應直接調用。
- sysenter是用于切換到內核模式的處理器指令。
如果我們設法替換NtAllocateVirtualMemory,我們將能夠攔截進程內存中的堆分配流量。
應用掛鉤
為了攔截目標函數NtAllocateVirtualMemory的執(zhí)行,我們將使用 mhook 庫。您可以選擇原始庫或改進版本。
使用 mhook 庫很容易:您需要創(chuàng)建一個與目標函數具有相同簽名的鉤子,并通過調用Mhook_SetHook來實現它。鉤子是通過在函數體上使用jmp指令覆蓋函數prolog來實現的。如果您已經使用過鉤子,那么您應該沒有任何困難。
安全機制
有兩種安全機制可以幫助我們緩解堆噴射攻擊:數據執(zhí)行預防和結構化異常處理。
結構化異常處理或 SEH是一種特定于 Windows 操作系統(tǒng)的錯誤處理機制。當發(fā)生錯誤(例如,除以零)時,應用程序的控制權被重定向到內核,內核會找到一系列處理程序并逐個調用它們,直到其中一個處理程序將異常標記為“已處理”。通常,內核將允許流程從檢測到錯誤的那一刻起繼續(xù)執(zhí)行。
從進程的角度來看,DEP 看起來像是在內存執(zhí)行時出現 EXCEPTION_ACCESS_VIOLATION 錯誤代碼的 SEH 異常。
對于 x86 應用程序,我們有兩個陷阱:
DEP可以在系統(tǒng)參數中關閉。
- 指向處理程序列表的指針存儲在堆棧中,它提供了兩個潛在的攻擊向量:處理程序指示器覆蓋和堆棧替換。
- 在 x64 應用程序中,不會出現這些問題。
防止堆噴射攻擊
現在,讓我們開始練習。為了減輕堆噴射攻擊,我們將采取以下步驟:
1.形成分配歷史
2.檢測 shellcode 執(zhí)行
3.檢測噴霧
形成分配歷史
為了攔截動態(tài)分配內存的執(zhí)行,我們將 PAGE_EXECUTE_READWRITE 標志更改為 PAGE_READWRITE。
讓我們創(chuàng)建一個結構來保存分配:
接下來,我們將為NtAllocateVirtualMemory定義一個鉤子。此掛鉤將重置 PAGE_EXECUTE_READWRITE 標志并保存已重置標志的分配:
一旦我們設置了鉤子,任何帶有 PAGE_EXECUTE_READWRITE 位的內存分配都會被修改。當試圖將控制權傳遞給該內存時,處理器將生成一個我們可以檢測和分析的異常。
在本文中,我們忽略了多線程問題。然而,在現實生活中,最好單獨存儲每個流的分配,因為 shellcode 執(zhí)行預計是單線程的。
檢測 shellcode 執(zhí)行
現在,我們將為 SEH 注冊一個處理程序。這就是這個處理程序通常的工作方式:
1.提取觸發(fā)異常的指令的地址。如果此地址屬于我們保存的區(qū)域之一,則此異常已由我們的操作觸發(fā)。否則,我們可以跳過它,讓系統(tǒng)繼續(xù)搜索相關的處理程序。
2.搜索堆噴射。如果動態(tài)分配的內存被可疑執(zhí)行,我們必須對檢測到的攻擊做出反應。否則,我們需要恢復原樣,以便應用程序可以繼續(xù)工作。
3.使用NtProtect函數 (PAGE_EXECUTE_READWRITE)恢復區(qū)域的原始參數。
4.將控制權交還給工藝流程。
下面是一個 shellcode 檢測的代碼示例:
目前,我們有一種機制可以監(jiān)控應用程序中的 shellcode,并可以檢測其執(zhí)行時刻。在現實生活中,我們需要再執(zhí)行兩個步驟:
- 攔截NtProtectVirtualMemory和NtFreeVirtualMemory函數。否則,我們將沒有機會監(jiān)控進程內存的相關狀態(tài)。這是一個碎片問題:我們需要存儲和更新進程的可執(zhí)行內存的映射,這是一項不平凡的任務。例如,我們的應用程序可以使用NtFree函數釋放我們保存區(qū)域中間的部分頁面,或者將它們的標志更改為 NtProtect。我們需要跟蹤和監(jiān)控此類案件。
- 使用 Execute 分析所有可能的標志(一組允許我們執(zhí)行內存內容的可能值),例如 PAGE_EXECUTE_WRITECOPY 標志。
檢測堆噴射
使用上面的代碼,我們在動態(tài)內存執(zhí)行時停止了一個應用程序,并獲得了最新分配的歷史記錄。我們將使用這些信息來確定我們的應用程序是否受到攻擊。讓我們探索一下我們的堆噴射檢測技術的兩個步驟:
- 首先,我們需要確定我們將存儲多少分配以及在發(fā)生異常時我們將分析其中的多少。請注意,我們對相同大小的分配感興趣。因此,如果流中的內存以不同的大小分配,我們可以允許流繼續(xù)執(zhí)行,因為這不太可能是堆噴射攻擊。此外,在分配邊界之間存在空間的情況下,我們可以排除堆噴射攻擊的可能性,因為堆噴射意味著連續(xù)的內存分配。
- 接下來,我們需要選擇堆噴射檢測的標準。檢測堆噴射的一種有效方法是在內存分配中搜索相同的內容。這個重復的內容很可能是shellcode的副本。例如,假設我們有 10,000 個分配具有相同數據的相同位移。在這種情況下,最好從接收控制的當前分配的位移開始搜索。
用于識別堆噴射的建議算法
我們建議使用所描述的技術并注意以下四個標準,以排除可能會顯著減慢您的應用程序的不必要檢查:
1.為每個線程定義已保存的內存分配數量。
2.設置已保存內存分配的最小大小。攔截大小為一頁的分配將導致不合理地節(jié)省內存。堆噴射通常使用為某個應用程序的特定堆管理器選擇的巨大值進行操作。數十頁和數百頁似乎更相關。
3.定義發(fā)生異常時將分析的最新分配數。如果我們處理過多的分配,它會降低應用程序的效率,因為對于動態(tài)內存的每次執(zhí)行,我們都必須讀取大區(qū)域的內容。
4.設置 shellcode 的預期最小大小。如果我們要搜索的代碼太小,就會增加誤報的數量。
結論
我們探索了一種使用鉤子和內存保護機制檢測堆噴射攻擊的方法。在我們的項目中,這種方法在測試和堆噴射檢測過程中顯示出出色的效果。