0%

CS DNS beacon二次开发指北

CS DNS beacon二次开发指北

接上一篇CobaltStrike beacon二开指南,这次把DNS beacon的写法也简要的讲一下。感觉写的东西还是有点玩具,所以依照惯例,还是把指南改成指北吧。。。
之前花了大功夫把geacon系列项目整的差不多了,感觉当初这么一个心血来潮的项目能整的这么成功,爽死了捏。当时的TODO上是写了实现DNS beacon的,但是感觉这个功能真的很拉跨加上流量特征极其明显加上不出网环境上马本身就有点玄学,如果有边界机就上代理之后再上马也行,反正就是各种buff叠起来导致感觉鸡肋的很,以及在安全客上看到有人发布了一版实现过的版本,就一直没写,但是最近有点闲的,一起写geacon pro的好兄弟又一直和我提起这个事,就乱写两笔试试算了捏。

写好的代码在geacon_pro的dns分支下,由于只是随手写的玩具,还有很多地方没修好,只测试了基本的命令执行,很多功能并未测试,以及很多功能也没实现,只是写出了一个实现的样例demon

pro转私有了,虽然网上可能能找到fork,但是有师傅提到了这个问题,就顺便缝合了一下也缝进plus了,顺便修了点之前的小问题。具体代码整个都在packet/dns.go下,外部代码没改啥,通信处有区别的地方加一个if判断改一下就行

碎碎念

距离上一次更新博客已经两个月了捏。放假就是让人没法好好的学习吗?感觉在家每天也有学东西,但是好像很少花时间去实践,很多时候就是找点东西看看原理然后把对应的工具star一下,总之就是很散,难得把得到的内容聚合成一篇文章。。。现在开学了就自主坐牢写点破烂,然后水一篇文章,给两个月不更新的博客刷一点活跃度。另:笔记本键盘用久了感觉快坏了,最近老是按一下空格就输入一堆空格,很烦

以及,当初看到安全客上有人发DNS beacon的时候,准备上去学一下的,点开发现直接闭源+知识星球广告,回头看写的文章,协议关键点是一个都没透露,前面分析了半天开源的geacon,没做什么大事安全客连投四篇,鉴定为纯纯的噶韭菜。要分享就开源讲清楚一点大家共同学习,自己写的工具要私藏就别拿出来吹逼嗷。

协议分析

好久没有去逆CS的jar包了,导致一度翻不到协议内容,不过网上有现成的逆向分析文章,配合之前写http beacon时通信数据组成的了解,再自己抓两个包,完全没有问题

首先可以先确定一个点,DNS beacon受影响的只有通信模块,所以代码封装的好可以侵入性很低的扩展出一个DNS通信功能,原先项目的代码基本上不会有太大修改

基本运行原理

首先还是看一下DNS beacon下的通信模式,如果找一个新一点版本的c2profile的话,可以看到如下几个entry

################################################
## DNS beacons
################################################
## Description:
##    Beacon that uses DNS for communication
## Defaults:
##    dns_idle: 0.0.0.0
##    dns_max_txt: 252
##    dns_sleep: 0
##    dns_stager_prepend: N/A
##    dns_stager_subhost: .stage.123456.
##    dns_ttl: 1
##    maxdns: 255
##    beacon: N/A
##    get_A:  cdn.
##    get_AAAA: www6.
##    get_TXT: api.
##    put_metadata: www.
##    put_output: post.
##    ns_reponse: drop

这里面对应的几种beacon通信的模式,在http中,beacon通过get请求查询是否存在下发命令,通过post请求回传执行结果,在dns中,也可以分为A,AAAA,TXT三种查询模式,不过并非使用查询模式来区分获取命令和回传结果,而是通过域名前缀

可以看到,A记录对应的前缀为cdn,AAAA记录对应的前缀为www6,而TXT记录对应的前缀为api,回传结果使用的前缀是post(默认使用A记录回传,大概),对于metadata回传,则使用www前缀

接下来以上线checkin获取命令并回传作为一个完整流程介绍各部分通信协议的实现吧,本地搭建的环境,server的dns域名设置为test.unknown

上线

dns beacon有一个base domain,每次上线时生成一个特定的beaconID,组合成为<beaconID>.<baseDomain>作为请求的基础域名,向teamserver发起DNS A记录查询,若beaconID校验通过,则认为有效beacon上线

beaconID为一个32位int,且要求beaconID&1202==1202才会被识别为合法的DNS beacon。不同版本的CS对这个地方的校验不一样,需要手动去jar包里翻。位于common.CommonUtils类下的isDNSBeacon方法

好像除了这个校验应该还有别的校验的。。。但是我没找着,也懒得翻了,结果就是这个马得平均跑个六七次才会生成一个 teamserver认为是合法的session
原因是beacon id的范围,beacon id的有效范围是0x00000000到0x7fffffff,之前设置的是0xffffffff,导致运气不好就没法上线,改到这个范围就可以了

beacon会持续发送上线请求等待server下发命令,该请求为A记录查询,server的可能返回值有多种,若为0.0.0.0则无命令下发,0.0.0.240-0.0.0.245表示存在下发命令并指定拉取命令的形式(A/TXT/AAAA),偶数表示无需checkin,奇数表示需要checkin

image-20230227151825201

如图生成的beaconID为6a2897ba0.0.0.243checkin并以TXT记录形式下发命令。

需注意c2profile中的dns_idle项,任何情况下得到的A记录需与其进行异或得到结果(包括使用A记录获取命令等操作),此处为0.0.0.0故直接使用

checkin

dns beacon和http beacon有所不同的一点,是dns beacon上线时,teamserver处只会出现一个没有任何属性的beacon条目,需要手动执行checkin命令或是下发其他命令,要求beacon回传metadata,才会变成http beacon中有用户名pid内网ip等属性的beacon。也就是说,http情况下,beacon每次get请求都会回传metadata,所以上线即获取全部信息,而dns情况下,只有server要求checkin时,才使用www前缀回传metadata。

metadata的格式与http的完全一致,但是需要注意的是metadata中的beaconID需要与请求的域名中的beaconID一致,否则会被认为是另一个beacon上线。又因为DNS请求中查询的域名是有长度限制的,一个DNS查询并不能回传所有数据,故需要分段传输

分段传输方法

此节同样适用于后续对执行结果的回传。唯一区别在于metadata回传与执行结果回传的域名前缀不同,样例如下图

image-20230227153416189

首先,beacon需要再次生成一个32位整数,我暂且称之为transferID,用于标记本次传输的数据,此处为5c2b1359,以及生成一个从0开始的counter

然后传输需要分为两部分,数据长度和数据内容,数据长度和数据内容均以16进制数的形式表示,由于DNS域名请求一次似乎能传两百多个字符(没确认过 ),而一级域名最多只能有64个字符(也没确认过),所以数据需要通过多级域名拼到一起传输,需要一个标记表示当前域名中有几级域名被用于传输数据。
第一个请求回传数据长度,RSA公钥加密后的metadata长度为128,转换为16进制0x80,显然两个字符远小于64个字符,故此处使用1表示只有一级域名被用于传输数据。接着拼接counter,transferID,beaconID和baseDomain,组成请求
<prefix>.<token><data>.<counter><transferID>.<beaconID>.<baseDomain>

后续的两个请求被用于传递真正的metadata,可以观察到counter每次请求后加一,而第二个请求由于数据过长,使用了4级域名传输数据,即www.后的token为4

请求命令

由于下发指令为243,意为checkin后以TXT请求拉取命令,A和AAAA流程类似,只是传输效率远不如TXT

请求命令时应答的数据格式和http时也有点出入

4bytes data length
base64 aes encode data{
        (4bytes cmd type
        4bytes cmdlen
        len bytes cmd)
        ...
}

由于DNS请求传输带宽的问题,DNS这边就不支持http中data prefix,data suffix等冗余操作了,也就是返回一个length和剩下的数据

同数据回传部分,需要生成一个transferID和一个counter,以<prefix>.<counter><transferID>.<beaconID>.<baseDomain>的形式发起请求,但无论请求的格式是什么,第一个请求永远是A请求,得到的ipv4响应即为接下来命令数据的长度。接下来持续发起TXT请求,直到接收到对应长度的数据(此处的数据是被先AES加密再base64编码过的),传输的长度指代的是base64前的数据长度,注意处理。

image-20230227160620700

结果回传

与checkin时的 分段传输流程一致,但checkin时是直接把RSA加密的metadata传了回去,而此处显然不能直接将AES的执行结果回传,格式实际上还是和http时的较为一致

packet len
aes encode data{
    4bytes counter
    4bytes result len
    4bytes callback type
    reply content
}
16bytes hash

同样的先一个单独的请求预示后面packet的长度,接下来将aes加密的数据和数据的hash通过上述分段传输方式回传

总的来说,整体的实现并不是很复杂,只需要认真观察一下流量并且结合之前的知识就能搞定捏 。代码的侵入程度较低,对项目源代码只是稍微修改了一下beaconID的生成,以及回传时将packetlen单独提出来发送,剩下的全部放在一个全新的dns.go中实现

end

就这样8,其实实在没有什么太多的东西,但是感觉搜了一圈也没看到啥详细的解释,刚好好久没更新博客了,水一篇

这篇翻译文章对实现这个功能给出了很大的帮助
Cobalt Strike: 解密DNS流量 – Part 5