# cs 的工作流程
1、被控机先向 TeamServer 发送心跳包,包中包含了主机信息和协商密钥等信息,而这些信息都被使用 RSA 公钥进行了加密放在了 Cookie 中
2、server 端第一次心跳之后进入睡眠,并用私钥将数据包解开获得主机信息和协商密钥,基于协商密钥会生成新的 AES
key 和 HMAC key
3、睡眠时间过后,会再一次发送心跳包询问是否有新的命令,当有新的命令出现时会将其数据包加密后发送,而命令会作为该回应包的 body 发送
4、被控端在接收到数据包之后进行解密获取命令,再将命令执行的结果加密后返回给 TeamServer,该次传输使用 POST 请求发送回 TeamServer
5、TeamServer 解密之后能看到明文的回显
6(不同情况)、当 3 中所说的睡眠时间过了之后,再次发送心跳包却没有收到新的指令信息,这时候 teamserver 就会返回空包
ps:还需要知道的是,Private Key 和 Public
Key 是特定的,并且保存在.cobaltstrike.beacon_keys 中,也就是说.cobaltstrike.beacon_keys 是特定的,对于一个固定的 CS,经过首次使用 CS 软件,就会生成一组固定的私钥和公钥,而这也是通信的关键。现在很多 CS 的私钥已经披露了,也就意味着如果抓到攻击者的 CS 流量,能够精确找到对应的私钥,那么解密流量包也是有可能的,但是前提不是魔改的 CS
用一副流程图来解释下 CS 的通信流程(来自 Chris 师傅文章流程图的魔改):
![]()
# 信标
接下来来说说信标,信标就是 cs 中的 beacon,对于 windwos 操作系统来说,就是 cs 生成需要执行的 beacon.exe 文件,同时因为信标由 teamserver 端进行生成,因此其中包含很多 teamserver 端的信息,更是可以通过静态和动态的信标去解密 cs 的通信流量,这些后文中都会提到
# 心跳包
接下来浅浅的解释一下心跳包的作用与含义。利用心跳包的这种方式,定时向 TeamServer 进行发包询问,无命令就返回空包,有命令加密后进行回包,有效避免了进行长时间不间断的数据传输和通信而被防御设备检测到异常流量采取措施,sleep 的时间一般是 60s。从正向来看,当 sleep 的时间默认为 60s 时,我们发送命令获得回显的时间有长有短,这就是原因,越接近睡眠时间结束发送的命令,获得的回显时间也越快,只有当心跳包发送进行询问,发现有新的指令,这个时候才会进行传输通信。这里顺便提一嘴,指令是指 TeamServer 端发送的任意动作指令,不是单单指命令行的操作指令
当然,个人看来,从防守方来讲,既然知道了心跳包的的存在,一样可以通过改写规则来实现检测和拦截,一般 CS 的 sleep 时间设定在 6-9s。前段时间和某大厂做流量检测产品的大手子师傅聊了天,知道了其实做流量检测这种安全产品,主要就是想方法让它越做越精准,而不是只会存在一大堆不准确的误报,费时费力不说,客户看了也头疼。例如某一个渗透工具,它可主要分为三次通信,第一次连接,第二次测试,第三次进行执行,这时候没有必要第一次在尝试连接的时候便进行告警,很多的业务流量可能也是相似的,如果这就直接告警那会出现铺天盖地的误报。一般第一次和第二次通信都抓他们的特征包并且做好记录,当第三次出现时,再发出告警并且做一些拦截防护措施,这样既避免了误报,也实现了成功告警拦截
# 抓包
接下来抓取流量进行分析
攻击机:192.168.40.144
靶机:192.168.40.131
首先在攻击机上开启 cs 服务,设置好监听后生成 windows 木马,将生成的 beacon.exe 放入 windows 靶机上,开始抓取流量
# 第一次心跳包
双击 beacon.exe 之后暂停进行分析
![]()
这时候我们的 TeamServer 显示靶机已经受害,被上线了
按照 CS 的工作流程,在 TeamServer 显示出主机之前,需要经过:被控机子的第一次心跳包;TeamServer 接收进入睡眠,并且解密数据包获取主机信息,生成新的 AES
key 和 HMAC key 两个大致的步骤,接下来看看流量包
过滤一下 http
![]()
可以看到一个心跳包一个回包,追踪一下看看
![]()
可以看到 get 请求发送的第一个心跳包,cookie 记录了公钥加密后的主机信息和协商密钥,向 TeamServer 发包
![]()
可以看到 TeamServer 返回了一个空包,符合 TeamServer 第一次接收到心跳包的流程。
对第一次心跳包抓包的发现做一个总结:根据前面所说,Teamserver 出现被上线机子的信息之前,需要经历第一次心跳包,而我们在此之前所拥有的行为只有点击 exe,也就是说双击 exe 的行为使我们向 TeamServer 进行了第一次的发包,并且让 TeamServer 成功解密获取到了主机信息并显示在了 TeamServer 上,成功上线
# 无回显指令
接下来分析一下第一个心跳包之后再次发送心跳包只无回显指令的情况,抓包,开始上线,将上线机子的 sleep 时间调整为 6-9s,静静等待几个睡眠时间
过滤一下 http
![]()
发现睡眠时间调整后短时间内出现了很多心跳包的发送与回包
![]()
根据上图所示,第一个框中就是上线前接收到的第一个心跳包,解析完之后进入睡眠,第二个框是第二次心跳包,回包发现 body 中带有指令信息,这就是我们修改 sleep 时间的指令,以回包的形式加密后发送给被控端,心跳包 1 和心跳包 2 之间间隔时间很长,因为原先设定的时间为默认的 60s,所以第一个心跳包之后进入的睡眠时间为 60s,结束之后第二个心跳包使被控端接收到指令修改 sleep 为 6s 之后,接下来抓包能明显感觉到心跳包发送的速度更快了。因为接下来没有进行任何的操作,因此第三个框中的回包全是空包
# 同一次睡眠时间执行 > 1 指令
接着分析心跳包接收到有回显命令的情况。写到这里的时候,我有一个疑问,当 sleep 时间够长(可以为默认的 60s)时,执行两个指令,那么两个指令会作为一个心跳包的回包还是两个分别作为两个心跳包的回包呢?于是就想着把修改 sleep 时间的指令和一个命令行指令在第一次睡眠时间同时执行看看会是什么效果
抓包过滤 http
![]()
同样分三个框来分析,在抓包过程中,在第一次睡眠时间修改了 sleep 并且输入了 ls 命令。第一个框便是第一次心跳包,第二个框便是 60s 睡眠时间过后的第二个心跳包,发现被控端 POST 传回了一个包,而这个包便是加密过后的命令执行的回显,根据上面无回显指令的分析,sleep 指令被传到被控端之后被执行是没有包被发送回来的,同时在抓取的过程中发现,第二个心跳包之后的心跳包发送间隔变短,也就是说两条指令被作为一个心跳包的回包发送给了被控端解密执行,sleep 指令没有发送回包,而命令执行的回显被 POST 传回 TeamServer,疑问成功解决。那么问题又来了,如果说有两条有回显的命令同一次睡眠时间执行,会不会在同一个包中 POST 发送回来呢?继续抓包研究
这次在第一次心跳包之后的睡眠时间执行了 ls 和 shell more
flag.txt 两条指令,并空了两个心跳包
![]()
还是三个框来分析,第一个框为第一次心跳没问题,第三个框为空余的两次心跳包,第二个框可以确定的是心跳包的回包包含了两条指令,而我们可以明显的看到,只有一条 POST 请求包发送回 TeamServer 端,而我们的 TeamServer 端分别出现了两条命令的回显,证明两条命令被放在同一个包作为 body,被加密后 POST 发送回 TeamServer
** 结论:** 在一个睡眠时间中同时执行两条指令,两条指令会在同一个心跳包询问时(也就是睡眠时间过后的下一个心跳包)被一起返回执行;同时,如果执行两条带有回显的命令,两次的回显也会被放在同一个包 POST 发回。也就是说一次心跳可以执行多条指令,并且同样可以返回多条指令的回显
(PS:当然啦这是为了研究下流量所以一点点慢慢分析,如果好奇想知道能否一次心跳执行多条指令的直接 TeamServer 执行试试,看看回显和功能的改变就好)
##
# 解密 CS 通信流量
# 方法一:
https://github.com/Slzdude/cs-scripts
https://github.com/WBGlIl/CS_Decrypt
根据 CS 的通信流程,我们可以知道最后的通信流量是通过生成的 AES Key 和 HMAC
Key 进行加密的,也就是说需要得到这两个 key。而这两个 key 是根据 TeamServer 端接收到第一个心跳包时用私钥去解密心跳包中公钥加密的 cookie,根据解密出的协商公钥生成的。而公钥加密的 cookie 我们自然能在心跳包中看到,私钥就需要去.cobaltstrike.beacon_keys 文件中拿,使用 github 上的 cs-scripts 项目能进行提取。这就具备了解密 CS 通信流量的条件了
对于上一个标题所得的后半段结论:"多条回显会被同一个包发送回来" 的结论,如果没有看 TeamServer 只看流量包并不能确定。因此,既然知道了解密方法,那就拿上一标题中的流量包来进行解密说明
![]()
首先先拿到私钥,并使用 CS_Decrypt 项目中的脚本利用私钥解密心跳包中的 cookie
![]()
成功拿到被控主机信息和两个需要用到的 key
接着就可以拿这两个 key 去解密通讯流量了
![]()
首先是第二个心跳包的回包,也就是带有 TeamServer 指令的加密回包,它的 body 中带有指令,查看它的 data 数据,根据脚本要求将其进行 16 进制转换再转换为 base64 编码
![]()
利用已知的两个 key 和加密的 base64 编码
![]()
可以看到执行了 more
flag.txt 而没有看到 ls 命令,这是为什么呢?先继续往下看,看看命令执行的回显
![]()
也就是被控机子 POST 发回 TeamServer 的包的 data 数据块,同样的方法放入解密脚本中
![]()
但是发现也是能解密出一条命令执行的回显,这就很奇怪了,按照上面三个框的分析,第一个框为第一次心跳包,没有操作,第三个框也同样是返回空包,能返回的只有第二个框中的 POST 请求包,并且在抓取流量的时候也确切的看到 TeamServer 端回显了第二个命令执行的内容,所以只可能是脚本的问题
# 方法二:
重新出发
https://github.com/minhangxiaohui/CSthing
先说说该脚本,并且简单对比一下和方法一之间使用的不同
首先能看到 1768.py 该脚本是用来解密信标的,信标中包含了很多 TeamServer 端的信息,包括公钥、sleep、抖动都能从信标中解密出来,也就是说,不一定要找到本地的 key 文件,同样可以通过解密信标,使用公钥进行指纹匹配来获取私钥(1768.py 中就已有现披露的 CSkey 的库),但是如果是未披露的 CSkey,那么拿到信标依然没有作用,还是得拿到本地的 key 文件才能解密流量。这里的私钥同样用方法一当中的脚本来获取
![]()
先将 key 进行 base64 解码之后再转为 16 进制
使用命令:
1 | python3 cs-decrypt-metadata.py -p 私钥 cookie |
得到 Raw key、AES key 和 HMAC key
https://nnnpc-1311441040.cos.ap-shanghai.myqcloud.com/edP45N5pP2hM7f90.png!thumbnail
并解密出主机信息,和方法一之中的脚本不同的是,接下来解密通信流量可以使用原始 key 也就是 Raw
key 进行解密
1 | python3 cs-parse-http-traffic.py -r [Raw key] 1.pcapng |
在不规定其它参数的情况下解密出流量包中的通信流量
![]()
在这个脚本中就很清楚能看到了,第一个框显示了数据包 318 作为第二个心跳包的回包,回了两条指令,对于 % comspec% 的解释就是
不管命令行外壳是 cmd.exe 还是 command.exe,% comspec%
会自动选择正确的一个
而数据包 328 中,同样装载了两条指令的回显,证明上面的猜测是正确的
记录一个过滤规则:
引用自:https://blog.nviso.eu/2021/10/27/cobalt-strike-using-known-private-keys-to-decrypt-traffic-part-2/
由于数据已加密,我们需要提供原始密钥(选项 -r
caeab4f452fe41182d504aa24966fbd0),并且由于数据包捕获包含除纯 Cobalt
Strike C2 流量之外的其他流量,因此最好提供显示过滤器(选项 -Y http 和
ip. addr == 192.254.79.71 并且 frame.number >
6703),以便该工具可以忽略所有非 C2 流量的 HTTP 流量。
![]()
再次进行一个总结,同一个心跳包的回包可以带回多条指令,而被同一个回包带回的命令如有多个执行的回显,那也会在同一个包 POST 发送回去。如一次心跳有多个命令执行的回显,方法一的脚本只能解密每次心跳第一次命令执行的回显,方法二解密的通信流量更全
# 方法三:
利用动态信标的进程转储直接获得 AES key、HMAC key 以及 RAW
key,不需要去破解私钥解密元数组了,当然这个条件肯定需求更多,接下进行说明
# 3.x
首先说明
为什么能在动态信标的内存转储中直接提取出三个 key,这是适用于 CS3.x 版本的方法,当对动态信标进行内存转储后,文件中能找到未加密的元数组,并且是以 0x0000BEEF
开头的字节序列。同时在进程的生命周期中越早获取进程转储,它就越有可能包含未加密的元数据。
这里先将提取脚本放出来,就不继续看 3.x 了,用的比较少
1 | 进程转储 |
https://github.com/DidierStevens/Beta/blob/master/cs-extract-key.py
利用脚本
1 | python3 cs-extract-key.py 转储文件.dmp |
可以获得 key
# 4.x
接下来具体讲解和演示一下 4.x。4.x 和 3.x 的区别就在于:4.x 基本不能从动态信标的进程转储文件中恢复未加密的元数组,也就是说直接对 4.x 版本 cs 的动态信标的进程转储文件是提取不出通信所需要的 key。4.x 有自己解密的方法:
AES 和 HMAC
密钥可以在可写进程内存中找到,但没有明确标识这些密钥的标头。它们只是
16
字节长的序列,没有任何可区分的特征。为了提取这些密钥,该方法包括执行一种字典攻击。在进程内存中找到的所有可能的
16 字节长、非空序列将用于尝试解密一段加密的 C2
通信。如果解密成功,则已找到有效密钥。
该解密方法除了需要动态信标的进程转储文件之外还需要加密数据
首先先用上文提到的 cs-parse-http-traffic.py 脚本对抓取的流量包进行提取加密数据的操作
1 | python3 cs-parse-http-traffic.py -k unknown 1015.pcapng |
![]()
得到加密数据之后,再用动态信标的进程转储文件进行爆破解密,得到 key
1 | python3 ./cs-extract-key.py -t 加密data 转储文件.dmp |
![]()
接下来就可以利用 RAWkey 使用 cs-parse-http-traffic.py 对通信流量进行解密了,不同的是这里使用的是 SHA256
Raw Key,参数为 - k
1 | python3 ./cs-parse-http-traffic.py -k [SHA256 Raw key] 1.pcapng |
接下来总结一下方法三,该方法简单来说就是需要利用动态信标的进程内存转储文件,从里面提取出未加密的元数组。3.x 在进程的生命周期越早时越能直接转储到未加密的元数组并直接进行提取。而 4.x 不行,它无法检测到标识开头,但是不影响转储文件中拥有 key,因此,通过截取所有可能的
16 字节长、非空序列去尝试解密加密 data,如果成功,则匹配到正确密钥
