如何正確的 "手撕" Cobalt Strike

語言: CN / TW / HK

微信又改版了,為了我們能一直相見

你的 加星 在看 對我們非常重要

點選“長亭安全課堂”——主頁右上角——設為星標:star2:

期待與你的每次見面~

眾所周知, Cobalt Strike 是一款在滲透測試活動當中,經常使用的C2(Command And Control/遠端控制工具)。而Cobalt Strike的對抗是在攻防當中逃不開的話題,近幾年來該領域對抗也愈發白熱化。而絕大多數廠商的查殺,也是基於記憶體進行,然而其檢測方式的不當,導致非常容易被Bypass,包括但不限於

掃描RWX記憶體(正常程序中的Private Data區域一般沒有執行許可權)

DOS頭

•  掃描特徵

字串特徵

•  ReflectiveLoader

•  beacon.x64.dll

•  ...

Beacon Config(使用前)

  ...

上面列出的這些方法,實際都能繞過,核心原因是, 去掉這些特徵可以不影響 Cobalt Strike Beacon 的正常執行

01

BeaconEye核心原理

近日有安全人員開源了一款檢測Cobalt Strike Beacon的工具,名字叫 BeaconEye ,他的核心原理是通過掃描Cobalt Strike中的記憶體特徵,並進行 Beacon Config 掃描解析出對應的Beacon資訊,專案地址是 https://github.com/CCob/BeaconEye 。該專案的最大的特點是繞過難度較高(相比於其他同類型掃描工具),接下來對工具的核心原理進行剖析。

02

Beacon.dll

Cobalt Strike shellcode ,實際都是通過反射載入的方式載入 Beacon.dll ,而 Beacon.dll 中存在 Beacon Config 配置資訊(主要定義通訊目標/通訊方式等),在 Cobalt Strike 中對應的 Resource sleeve/beacon.dll

03

Beacon Config Generate

Beacon Config 的生成在 BeaconPayload 類的 exportBeaconStage 函式中

這上面指向的Settings結構體就是Beacon Config,比如var1,它代表實際通訊的埠

最終Cobalt Strike會將Settings轉化為bytes陣列,然後使用固定的金鑰進行Xor,並對剩餘空白欄位填入隨機字元

最後將生成的beacon.dll嵌入到最終的PE檔案中

04

Beacon Struct

Settings Add 系列函式,如 AddShort ,並不是簡單的將 Short 型別直接追加到 bytes 陣列中,而是追加了一個結構體

第一個欄位是index,第二個是type(short/int/...),第三個是length,第四個則是關鍵的value值,因此根據這個結構即可解析在記憶體或在檔案中的Beacon Config

05

BeaconEye規則

接下來讓我們看一下BeaconEye的 yara規則

32位的Beacon Config規則長這個樣子,如果你認真閱讀了前文一定會覺得很疑惑,因為按照Java當中的結構,它應該分為四個部分

[ ID ] [ DATA TYPE ID ] [ LENGTH OF VALUE ] [ VALUE ]

但是實際的yara規則卻沒有辦法對上java中的Beacon Config結構,說明Beacon.dll在裝載的過程中, 並沒有直接將上述資料memcpy分配到堆中 ,接下來讓我們通過對beacon.dll進行逆向

通過dllmain跟進,發現有一個關鍵函式,裡面首先解密了先前Beacon Config的加密資料,然後遍歷Beacon Config。首先是在拿到了Type之後, 直接往堆中分配的記憶體寫入WORD長度的Type,然後根據Type進行判斷,case 1對應Short,case 2對應Int,case 3對應Data,所以實際上最終的Beacon Config的結構是

DWORD           DWORD
[ DATA TYPE] [ VALUE ]

因此最終的yara規則可以解讀如下

??代表 萬用字元 ,實際匹配的就是beacon.dll當中真正的config結構體,到這一步,後面的結構體還原就是順水推舟了

06

Bypass BeaconEye

而近日有安全人員提出在執行Cobalt Strike的Shellcode之前,通過呼叫 SymInitialize 即可實現Bypass,本著好奇的態度,筆者繼續對原理進行了深入的探究

07

SymInitialize作用

根據官方文件的描述,SymInitialize的作用是用來初始化程序符號控制代碼的

它的傳參有三個

  • hProcess: 代表程序控制代碼

  • UserSearchPath: 符號檔案的搜尋路徑

  • fInvadeProcess: 是否對程序中已載入的每個模組呼叫SymLoadModule64函式

僅僅從傳參來看,並沒有辦法明確的判定為什麼能Bypass,因此我們使用windbg進行對比抓取

08

Windbg除錯

接下來我們分別對呼叫了SymInitialize和沒有呼叫SymInitialize的Cobalt Strike的Beacon進行windbg除錯, 由於我們知道BeaconEye掃描的是堆記憶體,因此我們直接對比兩者的堆記憶體

從上面兩張圖我們可以很清晰的看到,這兩個程序在堆記憶體中的最大的區別是,使用了SymInitialize的第一個heap區域,比沒有使用SymInitialize的第一個heap區域, 多了幾個Segment ,那為什麼多了幾個Segment就導致BeaconEye無法掃描呢?

09

Windows中Heap結構

使用windbg,執行如下命令,檢視一下具體Heap的結構

dt !_heap

可以看到heap結構的欄位非常的多,這裡重點關心3個欄位

• SegmentListEntry: 儲存堆段地址的雙向連結串列

• BaseAddress: 堆段起始地址

• NumberOfPages: 頁面的數量

那一個堆段的範圍是怎麼計算出來的呢?非常簡單

BaseAddress ~ BaseAddress + NumberOfPages * PageSize

而每一個BaseAddress以及NumberOfPages,都 僅僅只針對當前的堆段

A

NtQueryVirtualMemory

BeaconEye 中查詢記憶體資訊實際呼叫的是 NtQueryVirtualMemory ,我們都知道 Nt 系列函式是 Windows Ring3 進入 Ring0 的入口,讓我們檢視該函式的官方文件

可以看到查詢的資訊都存到了 MemoryInformation 中,而 MemoryInformation 對應的結構體是 MEMORY_INFORMATION_CLASS ,MEMORY_INFORMATION_CLASS實際包含了一個 MEMORY_BASIC_INFORMATION ,MEMORY_BASIC_INFORMATION結構如下

檢視RegionSize的描述

翻譯過來的意思是,RegionSize的計算方式是,從起始地址開始,直到記憶體頁的屬性不一致為止,包含的byte數量,就是RegionSize

B

猜想與驗證

首先我們可以初步得出結論,BeaconEye當中獲取堆的資訊時,實際只獲取了第一個堆段(因為堆段和堆段之間是不連貫的,導致記憶體頁屬性不能保持一致),因此假設Beacon Config沒有被釋放在第一個堆段中,就會導致BeaconEye檢測失敗,為了實現這個猜想,筆者將SymInitialize註釋掉, 轉而手動呼叫HeapAlloc進行堆分配(當堆空間分配的足夠多時,就會觸發系統自動生成堆段),如果這個猜想是正確的,那麼BeaconEye將同樣無法掃描

編譯執行,再使用BeaconEye進行檢測,發現已經無法檢測了,猜想bingo

C

BeaconEye修復(偽)

現在不能檢測的原因已經找到了,修復其實非常簡單,前面提到過,heap結構中包含了堆段的雙向連結串列,因此我們只需要在BeaconEye當中,遍歷這個雙向連結串列, 將所有堆段地址都新增到待掃描列表中即可,以下是修復程式碼

這個時候我們重新編譯,掃描原先使用了 SymInitializeCobalt Strike Beacon ,發現已經可以掃出來了

D

為什麼還是被Bypass了?

但是事情遠遠沒有那麼簡單,因為我發現先前手動呼叫HeapAlloc的Cobalt Strike Beacon並沒有掃出來,這令我百思不得其解,為了解決問題,我的思路是先確定Beacon Config在記憶體中哪個位置,這裡同樣使用yara進行確認(掃描完整記憶體),得到具體的位置後,除錯BeaconEye並判斷是否讀取到了對應的記憶體。經過一番除錯,發現BeaconEye確實存在於堆段中,但是BeaconEye並沒有完整的讀取到堆段的所有記憶體,示意圖如下

紅色部分是 BeaconEye實際讀取到的記憶體 ,綠色部分是 實際 Beacon Config存放的位置 ,為什麼會出現這種情況,這個時候就得繼續回到Windows的記憶體設計上

E

HeapBlock

在Windows的堆記憶體當中,除了堆段以外,還有一個概念叫堆塊,每一個堆段都是由多個堆塊組成的,使用 vmmap 工具即可檢視

不難發現每一個堆段包含了大量的堆塊,這也解釋了為什麼BeaconEye會檢測失效,因為堆塊和堆塊之間存在 屬性不一致的記憶體頁 ,導致只能讀取部分記憶體空間

而在實際的程序當中,堆塊對應的結構體是_HEAP_ENTRY

但是在_HEAP_SEGMENT當中,只有 FirstEntry LastValidEntry ,這兩個欄位的含義是 指向第一個以及最後一個堆塊

而經過閱讀相關資料,發現並沒有連結串列將所有堆塊串聯起來(無論堆塊是何種狀態),因此堆塊的位置需要手動計算, 這裡存在一個小插曲,就是windows實際是加密了_HEAP_ENTRY這個結構的,加密方式是Xor,而Xor的金鑰則在_HEAP結構的0x88(x86是0x50),因此在計算堆塊大小時,需要手動解密Size

F

BeaconEye修復(真)

在之前的修復程式碼上,我們手動計算所有堆塊的地址,並新增到待掃描列表當中,程式碼如下(方便演示這裡只寫了x64部分)

編譯修復的BeaconEye,重新掃描手動呼叫了HeapAlloc去Bypass原版BeaconEye的Beacon,發現已經可以掃描了

10

結語

目前這個加強修復版的程式碼,可以通殺Cobalt Strike全版本(3.x的yara規則需要修改),這對攻擊方來說提出了更高的挑戰以及要求。目前該檢測功能已經整合到即將釋出的   牧雲  新版本當中,也歡迎大家來申請試用體驗更強大的主機安全產品。

另外, 牧雲團隊 正在招聘主機安全領域的產品安全研究員,如果你和我一樣,喜歡研究紅藍對抗,並希望將它落地到產品當中,歡迎投遞簡歷,投遞郵箱為[email protected]

11

參考資料

  • https://wbglil.gitbook.io/cobalt-strike/cobalt-strike-gong-ji-fang-yu/untitled-1

  • 《Windows Internals 6 part 1》

點分享

點收藏

點點贊

點在看