使用Cobalt Strike和Gargoyle繞過殺軟的記憶體掃描
本文將主要介紹我在Cobalt Strike的Beacon有效載荷和gargoyle記憶體掃描規避技術繞過殺軟的記憶體掃描方面的研究。並且會提供一個使用gargoyle在計時器上執行Cobalt Strike Beacon有效載荷的概念驗證(PoC)。這個PoC背後的假設是使用記憶體掃描技術來對抗終端檢測與響應解決方案(EDR),這些掃描技術以規則的時間間隔發生,並且不會在非可執行記憶體上發出警報(也可能是因為我們觸發的警報淹沒在了大量密集的警報中)。通過“跳入”記憶體和“跳出”記憶體的方式,實現了避免在掃描器執行時將有效載荷駐留在記憶體中,然後在安全掃描完成後將其再次重新放入記憶體中的攻擊目標。
這篇文章的內容假設讀者熟悉 gargoyle 記憶體掃描規避技術 以及 Matt Graeber 提出的使用 C 語言編寫優化的 Windows shellcode 的技術 。
簡介
現在的很多企業越來越多地選擇採用複雜的終端檢測與響應解決方案(EDR),專門用於在整個企業中大規模檢測高階惡意軟體。常見的這種解決方案包括Carbon Black,Crowdstrike’s Falcon,ENDGAME,Cyber Reason,Countercept,Cylance和FireEye HX。[1] 我們在進行有針對性的攻擊模擬時所面臨的挑戰之一就是我們需要經常在運行了某種型別的EDR解決方案的主機上獲得立足點。因此,在滲透測試中,至關重要的是,我們能夠繞過任何先進的檢測功能以保持攻擊行為的隱藏狀態。
許多EDR解決方案都具有強大的功能,可以有效地檢測受感染主機上的可疑行為,例如:
1.記憶體掃描技術,例如尋找反射載入的DLL,注入執行緒[2]和 inline/IAT/EAT HOOK[3]
2..實時系統跟蹤,例如程序執行,檔案寫入和登錄檔活動
3.命令列記錄和分析
4.網路追蹤
5.常見的跨程序訪問技術,例如監視CreateRemoteThread,WriteProcessMemory和VirtualAllocEx
有許多惡意軟體系列和常見的攻擊框架都利用了典型的程式碼注入技術,例如使用反射載入DLL和執行緒注入,這種攻擊技術可以通過在整個企業中大規模使用記憶體掃描和異常檢測技術進行檢測(請參閱 https://www.countercept.com/our-thinking/advanced-attack-detection/ 和 https://www.endgame.com/blog/technical-blog/hunting-memory ,瞭解有關此類技術的更多資訊)。
因此,許多攻擊者為了隱藏痕跡而對他們的工具進行了很多更改,他們特別關注繞過記憶體掃描的技術。例如,Raphael Mudge的Cobalt Strike 為“記憶體中的威脅模擬” 引入了許多 新功能 ,如下:
1.支援反射載入的DLL修改記憶體許可權(而不是僅將記憶體頁設定為RWX)
2.清除反射載入的DLL的初始記憶體分配
3.模組化 ‘stomping’ 實現繞過注入執行緒的安全掃描程式,以便Beacon可以從DLL的合法TEXT節執行
此外,其他安全研究人員已經研究了通過“程式碼洞穴”(code caves)或使用SetThreadContext等技術繞過注入執行緒的安全掃描程式。尤其要說的是,@xpn發表的關於 ‘Evading Get-InjectedThread ‘ 的優秀博文。
這些繞過方法基本上都是專注於讓已經隱藏在記憶體中的有效載荷更難以被殺軟檢測到。然而,另一種可能的方法是針對記憶體掃描檢測技術本身的固有缺陷。例如,記憶體掃描可能是效能密集型和誤報嚴重的一種檢測手法,這意味著這種檢測技術在數千個終端上進行擴充套件時,效果不佳。因此,許多供應商將專注於監控特定的程序(例如通常會有針對性的對常見的Windows程序進行監控),可疑的可執行記憶體區域(即RWX記憶體頁)和固定時間間隔的掃描(在更密集的掃描技術中,每十五分鐘到一天一次的掃描間隔可能會有所不同。在這裡可以將記憶體與磁碟進行比較)。[4]
因此,如果攻擊者可以在非可執行記憶體區域中隱藏有效載荷並且在沒有進行記憶體掃描的情況下以特定間隔觸發有效載荷的執行,那麼就可以繞過針對記憶體掃描的檢測技術。例如,gargoyle記憶體掃描規避技術使用Windows計時器和特殊構造的程式碼以及rop程式碼來避免殺軟檢測到任何可疑的可執行記憶體。然而,除了能彈個訊息框之外,gargoyle PoC似乎做不了什麼 —— 那麼我們可以用它來執行Cobalt Strike 的Beacon有效載荷嗎?
gargoyle
Gargoyle是一種用於繞過Josh Lospinoso記憶體掃描器的PoC技術。它能夠使攻擊者以特定的時間間隔“喚醒”之前的活動,並將有效載荷隱藏在非可執行記憶體中,然後將自身標記為可執行記憶體來執行某些操作。要實現這個 gargoyle 需要執行以下步驟:
1.建立一個Windows Waitable Timer 物件,該物件在指定的時間間隔後執行對使用者定義函式(通過’pfnCompletionRoutine’引數提供)的回撥。 SetWaitableTimer 函式還使得使用者能夠提供一個“lpArgToCompletionRoutine”引數,這個引數可以通過堆疊傳遞到指定的回撥函式。
2.在這種情況下,’pfnCompletionRoutine’引數指向位於mshtml.dll中的特製ROP程式碼(’pop *; pop esp; ret’),’lpArgToCompletionRoutine’引數是指向攻擊者控制的堆疊的指標。
3.Gargoyle 執行任意程式碼,然後將其自身設定為不可執行的記憶體(通過VirtualProtectEx)並返回執行到’WaitForSingleObjectEx’,這個函式會一直等待直到觸發計時器。
4.當達到了計時器的執行時間時,Waitable Timer物件將通過非同步過程呼叫(APC)執行特製的ROP程式碼。
5.ROP程式碼會通過’lpArgToCompletionRoutine’引數提供的值彈出到esp中並切換堆疊。
6.特殊堆疊包含了一個指向VirtualProtectEx的指標(和引數),這會導致對VirtualProtectEx的返回呼叫,然後將有效載荷區域標記為可執行的記憶體。
7.最後Gargoyle會返回到有效載荷的開頭部分並再次開始執行。
方法
通過gargoyle PoC在計時器上執行有效載荷的方法需要以下兩個關鍵步驟:
1.開發一種技術來檢索和暫存有效載荷,跟蹤其記憶體中的配置檔案(即原始分配的記憶體地址,反射載入的分配記憶體以及任何後續啟動的執行緒),然後在指定的時間段之後從記憶體中取消對映。
2.以上面描述的方式實現這種技術,就可以將其整合到現有的gargoyle PoC中。由於gargoyle PoC使用的有效載荷是用匯編語言編寫的(請參閱主git儲存庫中的 ‘setup.nasm’ ),所以,我們採用的方法是先用C語言編寫程式碼然後將其編譯為位置無關程式碼(PIC),然後替換掉當前程式碼中彈訊息框的gargoyle有效載荷。之所以這麼做是因為第一步生成的程式碼的複雜性和重複(和獨立)測試程式碼的需要以及實現用高階語言而不是組合語言編寫所有內容的想法。
第一步:暫存/刪除Beacon有效載荷
下面的技術是實現該方法的第一步:
1.將Beacon有效載荷以“READ_ONLY”屬性寫入記憶體。這種方法避免了通過網路持續不斷的檢索有效載荷。
2.在只讀記憶體中找到“隱藏”的Beacon有效載荷,為其分配新記憶體,然後將其複製到新記憶體中,並在反射DLL的開頭建立一個新執行緒。作為反射載入過程的一部分,隨後分配另一個“RWX”記憶體區域。
3.保留對原始分配的引用。
4.通過記憶體掃描找到反射載入的記憶體分配。
5.通過執行緒掃描查詢由Beacon啟動的可疑執行緒(Windows核心將在原始記憶體分配時記錄此執行緒的起始地址)。
6.用指定的時間週期執行Sleep。
7.終止屬於Beacon的執行緒。
8.從記憶體中取消原始分配記憶體和反射載入分配記憶體的對映。
這種技術有許多侷限性。首先,記憶體或執行緒掃描效率不高。但是,由於我們沒有控制Beacon用於反射載入過程的bootstrapping shellcode或匯出的 反射載入器函式 ,因此我們無法輕易獲得從VirtualAlloc的附加呼叫返回的指標。所以,我們並不知道beacon在編譯時將自身載入到了記憶體中,也就不能輕易獲得其主執行緒的控制代碼。
其次,TerminateThread WINAPI的呼叫存在著風險,一般來說要儘可能的避免這種風險。微軟建議只有在呼叫者確切知道目標執行緒正在做什麼時才可以使用TerminateThread,在這種情況下我們是不知道的。在此研究期間,我瞭解了請求執行緒退出的不同方法,但發現只有TerminateThread才能工作而不會導致崩潰。然而,雖然這適用於此PoC,但尚未經過全面的測試,可能會導致無法預料的問題。更好的方法可能是連線到beacon的命名管道併發送exit命令指示它正常終止。
第二步:與 Gargoyle PoC 的整合
下一步是在計時器上插入我們的程式碼用於暫存/移除Beacon並作為gargoyle的主要有效載荷。這需要完成以下兩個階段:
1.使用 Matt Graeber 提出的技術用C語言編寫優化的 Windows shellcode 建立在第一階段所描述的位置無關程式碼(PIC)版本,下文中將稱其為“Metalgear”。位置無關的有效載荷包含與上述技術相同的邏輯,但它是通過遍歷已載入模組的連結列表並將每個函式與預先計算的雜湊進行比較來解析Windows API函式。應該強調的是,這種做法是低效的,因為它需要在每次執行程式碼時解析函式指標,而gargoyle允許我們通過 ‘SetupConfiguration’結構 傳遞函式指標 。[5] 但是,出於初始化 PoC 的目的(並且如“方法”章節部分所述),單獨編寫和測試 Metalgear PIC 更有效,更簡單。
2.將 Metalgear 插入到用於接收 ROP 或 堆疊Trampoline 的 gargoyle 程式碼( ‘setup.nasm’ )中並替換彈訊息框的原始有效載荷。在構建過程中,gargoyle 將’setup.nasm’編譯為shellcode,然後將編譯的有效載荷(’setup.pic’)寫入記憶體,並作為 PoC 的一部分。因此,此檔案包括設定 WaitableTimer 物件的所有邏輯、彈出訊息框並設定WaitForSingleObject和VirtualProtectEx的尾呼叫(函數語言程式設計術語)。因此,通過使用 IDA 或 WinDbg ,我們就可以分割已編譯的’setup.pic’有效載荷,刪除彈出訊息框的預設呼叫並替換為我們構造的 Metalgear PIC程式碼。
這個階段有兩個非常重要的點。首先,由於我們基本上將不同位元組位的shellcode混合在了一起,所以我們需要確保Metalgear在完成執行後可以恢復完全相同的執行狀態。這是必需要做的,這樣它就不會破壞特製的堆疊並導致gargoyle的尾部呼叫失敗。當嘗試通過程式碼洞穴(code cave)(例如通過’SuspendThread / GetThreadContext / threadContext.Eip -> 程式碼洞穴’機制)在遠端程序中插入執行緒時也要注意同類問題。[6]
因此,我們可以採用類似的解決方案並使用PUSHAD/PUSHFD指令將暫存器的標誌和eflags PUSH到堆疊中,從而在對 Metalgear PIC執行“上下文切換”之前儲存暫存器的狀態。一旦 Metalgear 完成執行(並確保已正確清除堆疊),我們就可以使用 POPAD/POPFD 指令將 gargoyle 恢復到其原始執行狀態。下面的虛擬碼塊演示了這種方法,並顯示了修改後的包含 Metalgear 有效載荷的“setup.nasm”:
; Replace the return address on our trampoline reset_trampoline: mov ecx, [ebx+ Configuration.VirtualProtectEx] mov [ebx+ Configuration.trampoline], ecx ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;; Arbitrary code goes here. Note that the ;;;; default stack is pretty small (65k). ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Execute Metalgear pushad pushfd Metalgear PIC popfd popad ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;Time to setup tail calls to go down ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
其次,在這種情況下編譯的Metalgear PIC是一個單獨的程式碼塊,由一個函式進行處理,該函式用於將WINAPI函式的雜湊解析為函式指標(’ GetProcAddressWithHash ‘)。但是,在預設情況下,Metalgear PIC一旦完成執行就會崩潰,除非它被引導到gargoyle的尾部呼叫。可以通過兩種方式避免這種崩潰,可以通過在執行完成後向gargoyle的尾部呼叫中新增一個 go/relative jmp(通過內聯asm),也可以通過內聯任何函式呼叫來實現。
演示
以下螢幕截圖展示了 Metalgear PoC 的實際應用。下圖所示的是 gargoyle 正在受害者主機上執行,但目前正處於 “Sleep” 狀態。下圖中高亮的部分顯示了 gargoyle 有效載荷儲存在記憶體地址是0x00BE0000 的記憶體中:
我們可以使用SysInternals工具包的 VMMap 來檢查程式記憶體的狀態。下面的螢幕截圖顯示了儲存有效載荷的記憶體區域(0x00BE0000)被標記為不可執行:
此外,在程序中沒有執行可疑的注入執行緒:
當達到計時器的時間週期時,APC將被執行,並且gargoyle將自己標記為可執行記憶體。然後它將繼續執行Metalgear,之後將Beacon注入記憶體,併為我們提供一個shell:
下面的螢幕截圖顯示了在0x00BE0000處的原始有效載荷的記憶體頁許可權被更改為RWX:
此外,我們現在可以識別到一個注入的執行緒和兩個可疑的RWX記憶體區域(0x02FE0000和0x03090000),它們都屬於注入的Beacon有效載荷(256K和268K區域):
可以使用下面的程序瀏覽器觀察注入的執行緒(可通過起始地址’0x0’識別):
如果此時觸發了記憶體安全掃描,則可能會標記一個可疑執行緒和兩個反射載入的DLL(分別對應於原始分配的記憶體區域以及由反射載入程序隨後將有效載荷“複製”到的另一個可疑的記憶體區域)。應該強調的是,這裡使用的是Cobalt Strike的預設配置,而沒有使用最近新出現的任何記憶體中規避技術。
在指定的時間之後,Metalgear將繼續終止屬於Beacon的執行緒並取消對映RWX記憶體區域,然後返回到gargoyle尾部並將其自身設定為只讀。然後它將等待下一次觸發定時器並重新啟動 Beacon。之後這個過程會無限期地重複執行。
下面的視訊演示了在實際場景中使用的技術:
該視訊以 gargoyle 處於“睡眠”狀態開始。然後演示了我們的 gargoyle 有效載荷當前是不可執行的,並且沒有可疑執行緒或記憶體指示符。此時,唯一的記憶體駐留物是與 gargoyle 相關聯的非可執行區域和不可執行的且“隱藏”的 Beacon 有效載荷。在這個 PoC 中,沒有對隱藏的 Beacon 有效載荷應用混淆或加密技術,不過新增這些東西也是非常簡單的。
當計時器被觸發時,gargoyle 將自己設定為可執行的記憶體並執行 Metalgear,後者繼續將 beacon 注入記憶體,為我們提供shell。我們現在可以識別到兩個 RWX 區域和一個與 Beacon 對應的可疑執行緒。(對於這個演示,Beacon 在終止之前的活動時間為一分鐘。這純粹是為了演示技術,計時器可以被配置為任意時間週期,例如可以設定為15分鐘的活動時間,30分鐘的睡眠時間)。
一分鐘的活動時間結束後,Metalgear 會結束 beacon 並將自己置於不可執行的狀態。任何可疑的特徵(包括注入的執行緒和RWX記憶體)現在均已消失。在下一次觸發定時器時,Metalgear 將在暫存 Beacon 之前會再次隱藏在只讀記憶體中。
限制
由於本文提出的技術的實驗性質,因此,本文中給出的 PoC 受到許多限制:
1.由於 Beacon 在每次迭代時都會被終止,因此必須每次重新建立新的會話,這將使得在實際的滲透測試中難以使用。
2.在 Metalgear 中使用執行緒和記憶體掃描不是最佳的解決方案。
3.一旦我們執行後,我們就沒有考慮過記憶體配置檔案。因此,許多使用可疑特徵進行實時跟蹤的EDR 解決方案,例如通過異常執行緒建立和動態記憶體分配的手段,仍然可以發現 Beacon 有效載荷。
4.由於 Beacon 的設計並沒有考慮本文所描述的這種情況,因此終止/暫停會影響漏洞利用階段的工作。
理想情況下,在每次呼叫時,我們都希望模擬Beacon的‘check-in’,因為它可以獲取新的命令,執行完命令並立即返回到休眠狀態。在此設定中,我們的記憶體配置檔案僅僅反映了網路上Beacon的延遲。因此,另一種可能的方法是暫停與Beacon相關聯的執行緒,並將與反射載入的Beacon有效載荷相關聯的記憶體許可權改變為READ_ONLY。但是,這意味著執行緒將始終存在,並且需要使用另一種技術(例如使用ROP程式碼或SetThreadContext)來隱藏此執行緒的起始位置,使其看起來好像屬於一個合法對映的DLL。
參考
[1] Hexacorn維護了最新的EDR解決方案及其各自的功能列表( http://www.hexacorn.com/blog/2016/08/06/endpoint-detection-and-response-edr-solutions-sheet/ )
[2] Jared Atkinson實現的Get-InjectedThread: https://gist.github.com/jaredcatkinson/23905d34537ce4b5b1818c3e6405c1d2
[3] 有關此類技術的更多資訊,請參閱Countercept的“記憶體分析”白皮書: https://www.countercept.com/our-thinking/memory-analysis-whitepaper/
[4] 此外,許多EDR解決方案使用可疑特徵進行實時跟蹤,例如本地/遠端執行緒建立和動態記憶體分配,但是在本博文中對這類技術的關注較少。
[5] 此外,用於Metalgear的大多數API呼叫都位於kernel32.dll中,後者在每個程序中用相同的地址載入到了記憶體中,因此也可能使用硬編碼的RVA。
[6] 有關此類執行緒劫持技術的更多資訊,請參閱Nick Cano的程式碼: https://github.com/GameHackingBook/GameHackingCode/blob/master/Chapter7_CodeInjection/main-codeInjection.cpp