作者:非攻安全團隊
原文鏈接:https://mp.weixin.qq.com/s/peIpPJLt4NuJI1a31S_qbQ
Cobalt Strike,是一款國外開發的滲透測試神器,其強大的內網穿透能力及多樣化的攻擊方式使其成為眾多APT組織的首選。如何有效地檢測和識別Cobalt Strike服務器一直以來都是安全設備廠商和企業安全關注的焦點。
近日,F-Secure的安全研究員發布了一篇文章講述了如何探測Cobalt Strike DNS重定向服務。其主要探測方式是向Cobalt Strike服務器發起多個不同域名的查詢(包括A記錄和TXT記錄),然后對比每個查詢的返回結果。如果返回結果相同,那么對應的服務器很可能就是潛在的Cobalt Strike C2服務器。隨后,我們對Cobalt Strike DNS 服務代碼層面進行了分析,發現了檢測Cobalt Strike DNS 服務的另一種方法,并選擇在某大型演練活動后進行發布。
01 Stager 分析
在對代碼分析前,我們有必要通過抓包簡單了解Cobalt Strike DNS Beacon與DNS Server的通信過程。DNS Beacon主要有兩種形式。一種是帶階段下載的Stager,另一種是無階段的Stageless。這里我們主要分析Stager Beacon,本地搭建的Cobalt Strike版本為4.2,IP地址192.168.100.101,DNS Listener綁定的域名為ns.dns.com,用到的profile配置如下:
set host_stage "true";
set maxdns "255";
set dns_max_txt "252";
set dns_idle "74.125.196.113"; #google.com (change this to match your campaign)
set dns_sleep "0"; # Force a sleep prior to each individual DNS request. (in milliseconds)
set dns_stager_prepend ".resources.123456.";
set dns_stager_subhost ".feeds.123456.";
運行Stager的Beacon后,通過WireShark可以觀察到Beacon與Cobalt Strike的通信過程。捕獲的數據看下圖:

其中ns.dns.com是Cobalt Strike Listener中綁定的域名,而.feeds.123456.是我們在profile中配置的dns_stager_subhost值。整個通信的過程中Beacon請求的都是TXT記錄。
通過nslookup請求aaa.feeds.123456.ns.dns.com的TXT記錄,查看返回結果可以看到傳輸的數據都在text字段中,而數據開頭的.resource.123456.是我們profile中dns_stager_prepend的值。

進一步分析后發現,Beacon請求的第一個域名是aaa.feeds.123456.ns.dns.com,然后是baa.feeds.123456.ns.dns.com,隨后按照一定順序發出大量的TXT記錄查詢,直到最后一個請求tkc.feeds.123456.ns.dns.com。請求順序可以表示如下:
aaa.feeds.123456.ns.dns.com
baa.feeds.123456.ns.dns.com
:
zaa.feeds.123456.ns.dns.com
aba.feeds.123456.ns.dns.com
cba.feeds.123456.ns.dns.com
:
zba.feeds.123456.ns.dns.com
aca.feeds.123456.ns.dns.com
cca.feeds.123456.ns.dns.com
:
zza.feeds.123456.ns.dns.com
aab.feeds.123456.ns.dns.com
cab.feeds.123456.ns.dns.com
:
tkc.feeds.123456.ns.dns.com
不難發現,每次請求域名中的第一個子域都是固定三個字母,并按照一定順序進行排列。排列規則看起來是包含26個字母的集合連續進行了2次笛卡爾積。所以很容易就可以模擬Stager Beacon從Cobalt Strike DNS服務請求數據。
def stager():
buff = ""
str1 = 'abcdefghijklmnopqrstuvwxyz'
resolver = dns.resolver.Resolver()
resolver.nameservers = ['192.168.100.101']
for i in product(str1, str1, str1):
dnsc = '{0}.feeds.123456.ns.dns.com'.format(''.join(i[::-1])).strip()
try:
text = resolver.resolve(dnsc, 'txt')[0].to_text().strip('"')
except NoNameservers:
break
except:
return
if text=="":
break
#time.sleep(0.3)
buff = buff + text
return buff
查詢結束后,將得到的數據進行拼接,最終數據可簡單表示如下:
.resources.123456.WYIIIIIIIIIIIIIIII7QZjAX...8ioYp8hnMyoYoIoAAgogoJAJAJAJAJAJAJAJAJAENFKFCEFOIAAAAAAAAFLIJNPFFIJOFIBMDPPHJAAAAPPNDGIPALFKCFGGIAEAAAAAAFHPPNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHKDPGLIOCHPPLNKGNJINHEIMMEABKBEIKCFPBOAOAHDDPPFPKOGFBCDFFODANEJGBDANKODPGJIIIIPDDCODOGNCBLCMHHMPCEBNBMJKCF...AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
由于數據并不直觀,所以還需要逆向Cobal Strike的jar包源代碼還原數據處理的過程。在使用Idea反編譯后,可以直接定位到加密的入口是在beacon\beaconDns.java中的setPayloadStage()函數,而傳入的數據var1則是DNS Beacon的Shellcode,也就是Stager Beacon請求的最終數據。
public void setPayloadStage(byte[] var1) {
this.stage = this.c2profile.getString(".dns_stager_prepend") + ArtifactUtils.AlphaEncode(var1);
}
setPayloadStage()函數首先獲取的是profile中dns_stager_prepend值,也就是.resource.123456.,然后調用了AlphaEncode()函數加密Shellcode并與前面獲取的值拼接。 跟進AlphaEncode()函數發現其位于common\BaseArtifactUtils.java
public static String AlphaEncode(byte[] var0) {
AssertUtils.Test(var0.length > 16384, "AlphaEncode used on a stager (or some other small thing)");
return _AlphaEncode(var0);
}
public static String _AlphaEncode(byte[] var0) {
String var1 = CommonUtils.bString(CommonUtils.readResource("resources/netbios.bin"));
var1 = var1 + "gogo";
var1 = var1 + NetBIOS.encode('A', var0);
var1 = var1 + "aa";
return var1;
}
可以看到,對Shellcode只是進行簡單的NetBios編碼,編碼后再和固定字符拼接。所以我們只需將字符串aa和gogo中間部分的數據提取出來進行NetBios解碼便可以得到Shellcode。
以上過程很容易就可以用Python實現,可以參考如下代碼:
import time
from dns.resolver import *
from itertools import *
def stager():
buff = ""
str1 = 'abcdefghijklmnopqrstuvwxyz'
resolver = dns.resolver.Resolver()
resolver.nameservers = ['192.168.100.101']
for i in product(str1, str1, str1):
dnsc = '{0}.feeds.123456.ns.dns.com'.format(''.join(i[::-1])).strip()
try:
text = resolver.resolve(dnsc, 'txt')[0].to_text().strip('"')
except NoNameservers:
break
except:
return
if text=="":
break
#time.sleep(0.3)
buff = buff + text
if "aa" in buff and "gogo" in buff:
f = open("beacon.bin", "wb")
f.write(bytearray(netbios_decode(buff.split('gogo')[-1].split('aa')[0])))
f.close()
def netbios_decode(netbios):
i = iter(netbios.upper())
try:
return [((ord(c)-ord('A'))<<4)+((ord(next(i))-ord('A'))&0xF) for c in i]
except:
return ''
if __name__=="__main__":
stager()
運行上面的Python腳本后會在腳本目錄下生成beacon.bin文件,可以直接使用Beacon Parser腳本解析配置,也可以直接使用Shellcode Loader加載上線。

02 特征分析
對代碼進一步分析后,我們在beacon/beaconDns.java中還發現了有趣的地方。
public DNSServer.Response respond_nosync(String var1, int var2) {
StringStack var3 = new StringStack(var1.toLowerCase(), ".");
if (var3.isEmpty()) {
return this.idlemsg;
} else {
String var4 = var3.shift();
if (var4.length() == 3 && "stage".equals(var3.peekFirst())) {//判斷第二個子域是非為stage
return this.serveStage(var4);
} else {
String var5;
String var6;
if (!"cdn".equals(var4) && !"api".equals(var4) && !"www6".equals(var4)) {
if (!"www".equals(var4) && !"post".equals(var4)) {
if (this.stager_subhost != null && var1.length() > 4 && var1.toLowerCase().substring(3).startsWith(this.stager_subhost)) {
return this.serveStage(var1.substring(0, 3));
} else if (CommonUtils.isHexNumber(var4) && CommonUtils.isDNSBeacon(var4)) {
var4 = CommonUtils.toNumberFromHex(var4, 0) + "";
...
...
}
}
}
}
Cobalt Strike服務器在處理DNS查詢的時候會先對請求域名的前兩個子域進行判斷,比如請求的域名為aaa.bbb.ccc.com,會判斷aaa的長度是不是等于3,bbb的值是不是等于stage。如果都滿足就進入serveStage()函數。跟進后發現serveStage()函數也只是簡單判斷了stage的長度后就返回了請求對應的值。
protected DNSServer.Response serveStage(String var1) {
int var2 = CommonUtils.toTripleOffset(var1) * 255;
if (this.stage.length() != 0 && var2 <= this.stage.length()) {
return var2 + 255 < this.stage.length() ? DNSServer.TXT(CommonUtils.toBytes(this.stage.substring(var2, var2 + 255))) : DNSServer.TXT(CommonUtils.toBytes(this.stage.substring(var2)));
} else {
return DNSServer.TXT(new byte[0]);
}
}
也就是說,當請求的域名以aaa.stage.開頭時,Cobalt Strike 服務器會直接響應我們的請求,請求aaa.stage.ns.dns.com等同于請求aaa.feeds.123456.ns.dns.com。

同時,由于Cobalt Strike服務器并沒判斷請求的域名后綴,當我們可以直接訪問Cobalt Strike DNS服務的時候,可以直接忽略DNS Listener綁定的域名直接請求數據。當然,在profile配置host_stage為true的時候,可以使用將上面的Python代碼替換feeds.123456.ns.dns.com為stage.xxx,運行后依然可以下載DNS Beacon的Shellcode。

當host_stage配置為false的時候,返回的結果有些不一樣。

可以看到,Cobalt Strike服務器沒有再返回Shellcode的數據,但是對以aaa.stage.開頭的域名的TXT記錄查詢,Cobalt Strike服務器依舊響應了TXT記錄。而其它的域名則像F-Secure研究員發現的那樣,返回的是A記錄,并且解析的IP就是profile中dns_idle的值。
當請求的域名第一個子域長度不為3開頭并且第二個子域不是stage的時候,Cobalt Strike服務器還會進一步判斷域名的第一個子域是否為cdn、api、www6、www、post。
if (var4.length() == 3 && "stage".equals(var3.peekFirst())) {
return this.serveStage(var4);
} else {
String var5;
String var6;
if (!"cdn".equals(var4) && !"api".equals(var4) && !"www6".equals(var4)) {
if (!"www".equals(var4) && !"post".equals(var4)) {
...
} else {
...
}
} else {//當請求域名的第一個子域是cdn、api、www6的時候
var3 = new StringStack(var1.toLowerCase(), ".");
var5 = var3.shift();
var6 = var3.shift();
var4 = CommonUtils.toNumberFromHex(var3.shift(), 0) + "";
if (this.cache.contains(var4, var6)) {
return this.cache.get(var4, var6);
} else {
SendConversation var7 = null;
if ("cdn".equals(var5)) {
var7 = this.conversations.getSendConversationA(var4, var5, var6);
} else if ("api".equals(var5)) {
var7 = this.conversations.getSendConversationTXT(var4, var5, var6);
} else if ("www6".equals(var5)) {
var7 = this.conversations.getSendConversationAAAA(var4, var5, var6);
}
DNSServer.Response var8 = null;
if (!var7.started() && var2 == 16) {
var8 = DNSServer.TXT(new byte[0]);//返回text=“”
} else if (!var7.started()) {
byte[] var9 = this.controller.dump(var4, 72000, 1048576);
if (var9.length > 0) {
var9 = this.controller.getSymmetricCrypto().encrypt(var4, var9);
var8 = var7.start(var9);
} else if (var2 == 28 && "www6".equals(var5)) {
var8 = DNSServer.AAAA(new byte[16]);//返回::
} else {
var8 = DNSServer.A(0L);//返回0.0.0.0
}
} else {
var8 = var7.next();
}
if (var7.isComplete()) {
this.conversations.removeConversation(var4, var5, var6);
}
this.cache.add(var4, var6, var8);
return var8;
}
}
當域名為cdn,www6, api作為第一個子域的時候,Cobalt Strike服務器會對不同的情況作處理。可以看到,當請求的類型是A記錄的時候,Cobalt Strike服務器會返回固定的IP值為0.0.0.0。

當請求的類型是TXT記錄的收獲,返回的結果中text字段為空。

對于AAAA記錄,Cobalt Strike服務器也會返回固定的地址::,只不過只能抓包看到。

由于返回的值都是固定的,同樣沒有判斷域名后綴,所以完全可以拿來作為檢測Cobalt Strike服務器的方法。以下是以api關鍵字作為檢測的參考代碼:
def checkA(host):
resolver = dns.resolver.Resolver()
resolver.nameservers = [host]
try:
#請求的xxxx.xxx最好是隨機的,并多次嘗試
ip = resolver.resolve("api.xxxx.xxx", 'A')[0].to_text()
except:
return False
if ip == "0.0.0.0":
return True
return False
當第一個子域為www,post的時候,處理情況又不相同,限于篇幅這里就不分析了,有興趣的朋友可以自行研究。
03 檢 測
本地驗證沒問題后,我們將目標轉移到了公網上。為了快速地篩選出潛在的并且開啟了DNS Server的Cobalt Strike服務器,我們可以通過一些關鍵字在網絡空間探測平臺中獲取初定的目標。
通過分析發現Cobalt Strike返回的A記錄中除返回的IP和域名外基本上數據是固定的。從Type字段開始到Data Length字段,Cobalt Strike每次響應都會返回\x00\x01\x00\x01\x00\x00\x00\x01\x00\x04,后面再接4個字節的IP,這里是0.0.0.0,也就是\x00\x00\x00\x00。如下圖:

所以利用這樣的特征,在FOFA或ZoomEye上可以很容易地就能找到潛在的開啟了DNS 服務的Cobalt Strike服務器。因為有不少滲透測試人員喜歡把dns_idle設置為8.8.8.8。所以我們將0.0.0.0的IP地址替換為常用的8.8.8.8也就是\x08\x08\x08\x08作為查詢關鍵字,便可以快速地找到潛在的監聽了DNS服務的Cobalt Strike服務器。

導出了IP地址后,并用腳本進行了探測,探測的部分結果如下:

同時也發現了一些開啟host_stage的IP,直接下載了DNS Beacon的Shellcode,下面是某IP的檢測結果。

04 防 御
針對上面提到的特征,可以通過修改beacon/beaconDns.java中的代碼,改變respond_nosync()處理請求的流程,增加判斷,修改默認的返回值。可參考如下代碼(注:該代碼是4.2版本的代碼,不過筆者本地測過CS最低版本是3.8,最高版本是4.2,代碼可能會有差異,但是可以采取同樣的方式):
public DNSServer.Response respond_nosync(String var1, int var2) {
StringStack var3 = new StringStack(var1.toLowerCase(), ".");
String dname = var1.toLowerCase().trim().substring(0, var1.length() - 1);
if (var3.isEmpty()) {
return this.idlemsg;
} else {
String var4 = var3.shift();
boolean CheckDname = false;
//增加了判斷請求的類型是否為TXT同時驗證了域名后綴是否為Listener配置的字符
if (var4.length() == 3 && var2 == 16 && dname.substring(3).startsWith(this.stager_subhost) && dname.endsWith(this.listener.getStagerHost().toLowerCase())) {
return this.serveStage(var4);
} else {
String var5;
String var6;
String[] dnameArray = dname.split("\\.");
String[] dC2Array = this.listener.getCallbackHosts().split(", ");
for (int i=0; i<dC2Array.length; i++){
if (dC2Array[i].endsWith(dnameArray[dnameArray.length - 2] + "." + dnameArray[dnameArray.length - 1])){
CheckDname = true;
}
}
//判斷請求的域名后綴是否為綁定的域名后綴
if (!CheckDname){
return this.idlemsg;
}
if (!"cdn".equals(var4) && !"api".equals(var4) && !"www6".equals(var4)) {
if (!"www".equals(var4) && !"post".equals(var4)) {
//增加了判斷請求的類型是否為TXT
if (this.stager_subhost != null && var2 == 16&& var1.length() > 4 && var1.toLowerCase().substring(3).startsWith(this.stager_subhost)) {
return this.serveStage(var1.substring(0, 3));
} else if (CommonUtils.isHexNumber(var4) && CommonUtils.isDNSBeacon(var4)) {
var4 = CommonUtils.toNumberFromHex(var4, 0) + "";
...
...
}
}
}else {//當請求域名的第一個子域是cdn、api、www6的時候
var3 = new StringStack(var1.toLowerCase(), ".");
var5 = var3.shift();
var6 = var3.shift();
var4 = CommonUtils.toNumberFromHex(var3.shift(), 0) + "";
if (this.cache.contains(var4, var6)) {
return this.cache.get(var4, var6);
} else {
SendConversation var7 = null;
if ("cdn".equals(var5)) {
var7 = this.conversations.getSendConversationA(var4, var5, var6);
} else if ("api".equals(var5)) {
var7 = this.conversations.getSendConversationTXT(var4, var5, var6);
} else if ("www6".equals(var5)) {
var7 = this.conversations.getSendConversationAAAA(var4, var5, var6);
}
DNSServer.Response var8 = null;
if (!var7.started() && var2 == 16) {
var8 = this.idlemsg;
//var8 = DNSServer.TXT(new byte[0]);返回text=“”
} else if (!var7.started()) {
byte[] var9 = this.controller.dump(var4, 72000, 1048576);
if (var9.length > 0) {
var9 = this.controller.getSymmetricCrypto().encrypt(var4, var9);
var8 = var7.start(var9);
} else if (var2 == 28 && "www6".equals(var5)) {
var8 = this.idlemsg;
//var8 = DNSServer.AAAA(new byte[16]);返回::
} else {
var8 = this.idlemsg;
//var8 = DNSServer.A(0L);返回0.0.0.0
}
} else {
var8 = var7.next();
}
if (var7.isComplete()) {
this.conversations.removeConversation(var4, var5, var6);
}
this.cache.add(var4, var6, var8);
return var8;
}
}
需要注意的是,上面的代碼并沒有修復域名請求返回的A記錄IP固定為dns_idle值的特征。但是我們可以在Cobalt Strike服務器前面再部署一臺正常的DNS服務,如下圖,根據請求的域名進行轉發,并利用Iptable設置白名單來繞過檢測,這里就不詳細介紹了。具體可以參考F-Secure發布的文章末尾提到的方法。

05 總 結
本篇文章簡單分析了Cobalt Strike DNS Beacon與Cobalt Strike 服務之間的通信,并在分析Cobalt Strike DNS 服務的代碼中找到了以下的特征:
-
當Cobalt Strike服務器的profile配置stage_host為true的時候,可以使用帶有stage關鍵字的域名模擬stager下載DNS Beacon的Shellcode。
-
使用api、cdn、www6作為第一個子域的域名如api.ns.dns.com向Cobalt Strike DNS服務查詢A記錄時將返回固定ip地址0.0.0.0,查詢TXT記錄是返回的text字段為空。
-
當查詢時用目標Cobalt Strike的作為名稱解析服務器的時候,上述請求可以忽略域名后綴,比如查詢api.xxx.xxxx和查詢api.ns.dns.com都會返回0.0.0.0。
結合以上特征,可以精確地檢測出監聽了DNS的Cobalt Strike服務器,并在公網上得到了驗證,同時也給出了防御的參考代碼和思路。
參考鏈接:
https://labs.f-secure.com/blog/detecting-exposed-cobalt-strike-dns-redirectors/

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1568/
暫無評論