Jump to content

加密堆分配為什麼要對堆分配進行加密?堆棧是局部作用域的,通常在函數完成時退出作用域。這意味著在函數運行期間設置在堆棧上的項目會在函數返回並完成時脫離堆棧;這顯然不適用於你長期保存內存變量的期望。此時,就需要用到堆了。堆更像是一種長期內存存儲解決方案。堆上的分配保留在堆上,直到你手動釋放它們。如果你不斷地將數據分配到堆上而從未釋放任何內容,也可能導致內存溢出。也就是說,堆可能包含長期配置信息,例如Cobalt Strike 的犧牲進程、休眠時間、回調路徑等。這意味著如果你的Cobalt Strike 代理在內存中運行,則任何防御者都可以在進程堆空間中以純文本形式看到你的配置。作為防御者,我們甚至不需要識別你注入的線程,就可以輕鬆地HeapWalk()所有分配並確定像“%windir%”這樣簡單的內容來嘗試識別你的犧牲進程:

1.png

如你所見,這是一個非常令人擔憂的想法。既然我們已經了解了這個問題,現在就必須去解決它。

我們有幾個潛在的解決方案,不過每個方案各有其優缺點。讓我們從獨立的EXE情況開始,因為這個要簡單得多。該二進製文件是你的Cobalt Strike 有效載荷。在這種情況下,我們可以很容易地實現我們的目標。使用前面提到的HeapWalk() 函數,我們就可以迭代堆中的每個分配並對其進行加密!為了防止錯誤,我們可以在加密堆之前掛起所有線程,然後在加密後恢復所有線程。

即使你認為你的程序是單線程的,Windows 似乎也在後台提供線程,為RPC 和WININET 等實用程序執行垃圾收集和其他類型的函數。如果你不掛起這些線程,它們將在嘗試引用加密分配時使你的進程崩潰。崩潰示例如下:

2.webp.jpg

Windows 後台線程

3.webp.jpg

wininet.dll 線程崩潰

從理論上講,這是一個很容易實現的方法!最後一個難題是如何在Cobalt Strike 休眠時調用所有這些函數,解決方法很簡單。

掛鉤如果我們查看Cobalt Strike 二進製文件的IAT(導入地址表),我們將看到它利用Kernel32.dll Sleep 來實現其休眠函數。

4.webp.jpg

Cobalt Strike 進口我們需要做的就是在kernel32.dll 中掛鉤Sleep,然後將掛鉤休眠中的行為修改為以下內容:

5.png

基本上,我們掛起所有的線程並運行我們的加密例程,如下所示:

6.png

這將創建一個PROCESS_HEAP_ENTRY 結構,在每次調用時將其清零,然後遍歷堆並將數據放入該結構中。然後我們檢查當前堆條目的標誌並驗證它是否已分配,以便我們只對分配進行加密。

然後我們運行原始/舊休眠函數,該函數將作為掛鉤函數的一部分創建,然後在恢復線程之前解密。這樣我們就可以防止再次引用分配時發生崩潰。總之,這是一個相當簡單的進程。目前我們還沒有用到的是掛鉤功能。

首先,什麼是函數掛鉤?函數掛鉤意味著我們在進程空間內重新路由對函數的調用,例如Sleep() ,以在內存中運行我們的任意函數。這樣,我們局可以改變函數的行為,觀察被調用的參數(因為我們的任意函數現在被調用,可以打印傳遞給它的參數),甚至完全阻止函數工作。在許多情況下,這就是EDR 監控可疑行為並發出警報的方式。他們掛鉤自認為有趣的函數,例如CreateRemoteThread,並記錄所有的參數,以便以後在可疑調用時發出警報。

那如何掛鉤該函數?有很多方法可以實現這一點,但我只會提到兩種並深入研究一種。我將提到的兩種技術是IAT掛鉤和Trampoline Patching

IAT掛鉤IAT 掛鉤背後的想法很簡單,每個進程空間都有所謂的導入地址表。此表包含已由二進製文件導入以供使用的DLL 和相關函數指針的列表。推薦的和最穩定的掛鉤方法是遍歷導入地址表,識別你嘗試掛鉤的DLL,識別你想要掛鉤的函數並覆蓋其指向任意掛鉤函數的函數指針。每當進程調用該函數時,它都會定位指針並調用你的函數。如果你想調用舊函數作為掛鉤函數的一部分,你可以存儲舊指針,ired.team 上已經存在一個示例。

這種方法有優點也有缺點,優點是實現起來非常簡單,而且非常穩定。你只是改變了調用的函數,你並沒有改變任何內容,但卻有很大的崩潰風險。

如果使用GetProcAddress() 來解析函數,它不會在IAT 中。這是一個非常有針對性的掛鉤方法,可以帶來好處,但如果你想監視更廣泛的調用,例如能夠掛鉤NtCreateThreadEx 與僅使用CreateRemoteThread,理論上也更容易檢測。

Trampoline Patching

現在讓我們談談Trampoline Patching。 Trampoline Patching 更難實現,很不穩定,並且由於必須解決許多相對尋址問題,因此可能需要很長時間才能對x64 進行普遍處理。值得慶幸的是,已經有人花時間製作了一個開源庫,以非常穩定的方式執行所有這些操作。

接下來讓我們繼續看看掛鉤是如何工作的,這樣我們就可以根據需要重新實現我們自己的掛鉤。首先使用GetProcAddress 和LoadLibrary 解析函數。然後,解析有效程序集的前X個指令數,加起來最少為五個字節。更具體地說,我們將使用一種非常常見的技術,該技術使用五字節相對跳轉操作碼(E9) 從函數庫跳轉到+- 2GB 的位置,然後跳轉到我們的任意函數。顯然,要使其工作,我們需要覆蓋函數的前五個字節。如果我們這樣做,就需要再次調用它這樣會破壞原始函數。為了確保我們可以在需要時解決舊函數,就必須保存第一條指令,稍後將寫入代碼cave作為trampoline的一部分,該trampoline將為我們運行它,然後跳回下一條指令的函數。但如果第一條指令只有四個字節,寫入五個字節就會破壞第二條指令的第一個操作碼。然後我們需要將前兩條指令存儲在我們的trampoline中,現在trampoline將運行前兩條指令並跳回到第三條指令繼續執行。這個trampoline所在的地方將是被掛鉤的原始函數的新指針。所以,原來的函數指針現在是這樣運行。

7.png

這個代碼cave也會跳轉到任意函數的某個位置,寫在原始函數基礎的相對五字節跳轉跳轉到這個位置,然後像這樣跳轉到任意函數。

8.png

這樣,我們現在就有了一種方法,可以在調用舊函數時運行任意函數,並根據需要調用原始函數。

現在讓我們開始調試,看看MessageBoxA 是乾淨的還是經過掛鉤的。

首先,我們掛鉤MessageBoxA。代碼看起來像這樣:

9.png

MessageBoxA位於user32.dll中,找到基地址後,Patching所有內容,向cave添加一些代碼,解析相對跳轉並將trampoline存儲在OldMessageBoxA() 中。

任意/掛鉤MessageBoxA 函數將如下所示:

10.png

我們需要匹配返回類型和參數,此時,我們將運行原始MessageBoxA,無論如何我們將修改文本,始終顯示“HOOKED”。

現在看一下前後對比情況:

11.1.webp.jpg

11.2.webp.jpg

Patching未進行掛鉤的消息框之前

12.1.webp.jpg

12.2.jpg

Patching進行掛鉤的消息框之後

如你所見,在BEFORE 屏幕截圖中,第一條指令只有四個字節。這意味著我們需要存儲前兩條指令;然後我們的相對跳轉繼續覆蓋前五個。我們不需要修改剩餘的字節,因為我們將讓trampoline執行我們存儲的前兩個字節,然後跳回位置0x00007FF8EF70AC27。讓我們繼續在調試器中查看新的掛鉤函數是什麼樣的。我們將在運行JMP 後立即開始:

13.webp.jpg

跳轉到掛鉤函數

首先看到兩個00。這樣做是為了確保如果我們將多個trampoline寫入cave,我們不會覆蓋函數指針中00 00 的末尾。接下來我們看到FF 25 00 00 00 00,也就是JMP QWORD PTR指令。緊接著,你將看到八個字節,它們是掛鉤函數的指針!如果我們執行這條指令,就會看到:

14.webp.jpg

掛鉤函數中的第一條指令

最後:

15.webp.jpg

內部掛鉤函數

我們可以看到我們在我們的掛鉤函數中,掛鉤函數只運行並返回舊函數,所以讓我們繼續執行舊函數:

16.webp.jpg

調用舊函數

讓我們看看結果如何:

17.webp.jpg

trampoline

如果你看這張圖,可以看到我們正在執行我們覆蓋的前兩條指令。在復制的字節之後,我們對OriginalFunction+7的位置執行第二個JMP QWORD PTR(因為在這個示例中trampoline的大小是7個字節)。這將使我們處於第三條指令的開頭。讓我們來看看:

18.webp.jpg

繼續執行

可以看到我們現在處於CMP 指令處,從我們停止的地方繼續執行。

通過這個進程,你可以看到像minhook 這樣的實用程序是如何工作的。現在,你可以像我一樣自己實現它,也可以使用像minhook 這樣穩定的內容。

19.png

將EXE 放在一起把所有內容放在一起看看它是什麼樣子的:

Hook Sleep();

在掛鉤函數中,掛起所有線程;

使用HeapWalk() 加密所有分配;

通過trampoline函數運行原始的Sleep();

使用HeapWalk() 解密所有分配;

恢復所有線程;

我將假設你擁有自己的加密、掛鉤和完整的線程掛起函數。代碼如下所示:

20.png

非常簡單,這段代碼顯然不包括你的植入程序。你可以通過以某種方式執行shell 代碼在同一進程空間中運行植入程序,也可以將其轉換為DLL 並將其註入執行後的信標中。由於它使用了HeapWalk(),所以它可以加密過去、現在和將來的分配,只需要掛鉤Sleep() 來開始調用。

在這個演示中,對於任何sleep值為1或更少的內容,我們都不加密。

21.gif

EXE HeapWalk() 加密器演示

如你所見,首先我們進行1 次休眠,BeaconEye 會捕獲我們的配置。將sleep值修改為5,然後開始加密,成功關閉BeaconEye。

注意,由於這會加密所有堆分配,因此它不會作為註入線程工作,因為當Cobalt Strike 處於休眠狀態時,它注入的進程將無法運行。想像一下注入explorer.exe,每次信標休眠時,所有的Explorer 都會凍結。當需要作為線程注入時,這個解決方案顯然不是最佳的。如果我們想要一些可以作為線程工作的內容,將需要做更多的工作。

可以在此處找到演示過程。

線程目標堆加密我們的新設計必須使用單獨的線程,因為無法掛起額外的線程也無法鎖定堆,主進程將需要繼續運行。這意味著當我們注入一個信標線程時,必須確保所有加密的分配都只來自該線程。如果我們正確定位線程,則可以成功地避免該問題。

現在在我們的dropper 中有掛鉤函數。為了操作堆,需要在Windows 中調用了一個函數子集:

HeapCreate()

HeapAllocate()

HeapReAllocate()

HeapFree()

HeapDestroy()

Windows中的Malloc和free,位於msvcrt.dll中,實際上是HeapAllocate()和HeapFree()的高級包裝器,它們是RtlAllocateHeap()和RtlFreeHeap()的高級包裝器,它們是Windows中最低級別的函數,最終直接管理堆。

22.webp.jpg

來自Ghidra的圖

這意味著如果我們掛鉤RtlAllocateHeap()、RtlReAllocateHeap() 和RtlFreeHeap(),則可以在Cobalt Strike 中跟踪堆空間內分配和釋放的所有內容。通過掛鉤這三個函數,我們可以在映射中插入分配和重新分配,並在調用free 時將它們從映射中刪除,但這仍然不能解決我們的線程目標問題。

事實證明,如果你從一個鉤子函數調用GetCurrentThreadId(),你實際上可以獲取調用線程的線程id,使用它,你可以注入你的信標,獲取其線程id 並執行類似於以下的操作:

23.png

這樣做是為了重新分配,也可以免費進行刪除,現在你的目標是一個線程!到目前為止很容易。但是還記得之前的那個問題,我們不得不掛起其他線程的原因嗎?在我們及時解密之前,WININET 和RPC 調用仍會嘗試訪問加密的內存。這裡有幾個選項,但我使用了我認為有趣的選項。由於加載的shell 代碼既不是有效的EXE 也不是DLL,因此我能夠從任何發起調用的對像中進行分配,這些調用源自沒有名稱的模塊。

為了讓這個機制起作用,我們需要解析進行函數調用的模塊。這可以通過以下代碼實現:

24.png

這將獲得_ReturnAddress內在函數,並將其與GetModuleHandleEx和標誌

GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 一起使用,以識別哪個模塊正在進行此調用。然後我們可以將其轉換為小寫字符串,如果字符串不包含DLL 或EXE,我們繼續插入它。這樣,你就有了一個穩定的分配列表,可以在休眠時加密。你將需要重複這個進程為你的掛鉤重新分配。

要運行加密,你需要迭代列表並加密這些分配,而不是執行HeapWalk()。這將取決於你是否決定使用映射、矢量、鍊錶或其他內容。你希望將真正的HeapAlloc或ReAlloc返回的指針存儲到您的數組中,迭代數組並按大小對數據進行加密。上面示例中的Arg3是size 。

現在我們掛鉤了四個不同的函數,將基於線程id 的分配插入向量中,迭代向量並在休眠時加密每個地址。如果成功,我們應該可以再次繞過BeaconEye。

0 Comments

Recommended Comments

There are no comments to display.

Guest
Add a comment...