Cobalt Strike 4.5 二开相关笔记

近期有人已经把beacon源码公布了,所以也把之前自己的一些研究笔记汇总发一下。

另外想说下自己的观点:已经2025年了,所谓二开 Cobalt Strike 对绝大部分人来说都是伪需求,你能有官方团队写的好么?学习一下就行了。

0x1 环境搭建

Java端

可能部分人还没有Java的原生源码,所以介绍下反编译的方法。

先把jar包反编译了,利用IDEA自带的java-decompiler.jar

java -cp java-decompiler.jar org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true cs_original/cobaltstrike.jar cs_src

cs_original/cobaltstrike.jar是原包,cs_src是反编译后的输出目录,得到一个jar后缀文件,解压缩即可得到源码。

IDEA新建项目,将反编译后的所有源码放入decompiled_src目录,原包放入lib目录,再在File-Project Structure-Modules-Dependencies中添加原包:

File-Project Structure-Artifacts中添加JAR,主类为aggressor.Aggressor

需要修改相应文件时,右键选择Refactor-Copy fileTo directory选择src目录里新建的目录:

修改完成后就可以进行编译,选择Build-Build Artifacts,在out目录下得到重新打包好的jar包。

Beacon端

beacon源码是纯C的,为了兼容性,需要使用Visual Studio 2012来开发,并且要安装Update 1补丁,否则无法满足平台工具集Visual Studio 2012 - Windows XP (v110_xp)的设置。

另外还有几个地方需要修复下,不然无法通过编译。

ReflectiveLoader.c中,会出现一个fatal error C1017: 无效的整数常量表达式错误,代码如下:

编译时RefLoadSize没初始化,就会导致错误,这个实际上会在预处理器里初始化,所以这里定义一下就行,我用的100:

还有在parse.c中,bool标识符会报错,因为C99标准里没有bool只有_Bool,我换成了BOOL

另外后期生成事件里,要把命令改下,否则有可能会因为找不到路径导致失败。

如果报tomcrypt.lib是用旧版本编译的,那就在这里把全程序优化设为

另外一个情况是,替换掉原来的sleeve下的dll然后生成exe,client端会报一个错误:

这是因为cs生成exe木马的逻辑是:先把回连地址等信息patchbeacon.dll,然后根据你需要的类型(exe、service exe、dll)再把beacon.dll patchartifact.xxx里,这里的错误就是因为重新生成的beacon.dll太大导致原来的模板放不下了,所以生成出来的exe也会直接无法运行报访问冲突的错误。

解决办法就是用Artifacts Kit重新生成一下即可:

记住,生成出来的东西不能直接用来替换掉resources目录下的exe,得用给的cna脚本去加载,这个b问题折磨了我好几天,问了@evilash才解决。

0x2 去除特征和修复漏洞

这里的内容参考了很多网上的文章,时间久远忘了有哪些,只列举记得的:

  • https://blog.qwqdanchun.com/CobaltStrike_Modify/

去除暗桩

这个版本有一堆暗桩,Java端和beacon里都有,Java里基本就是两种:

  1. 检验某个.class文件是否被篡改(对比哈希),如果被篡改就退出
  1. 检验是否用 Java Agent 方式启动,是则退出

还不少,全局搜索再耐心去掉就行。

beacon端也有一处:

这两个方法的返回值都要改为FALSE,因为会用 teamserver 端发过来的水印值进行校验,不符合就退出,所以全部改掉。

CVE-2022-39197修复

这个漏洞就是 Cobalt Strike 的 XSS 漏洞,可以导致 RCE :

修复方式,在aggressor.MultiFrame中添加:

try{
    ClassPool pool = ClassPool.getDefault();
    CtClass cc = pool.get("javax.swing.plaf.basic.BasicHTML");
    CtMethod method = cc.getDeclaredMethod("isHTMLString");
    method.setBody("return false;");
    cc.toClass();
}
catch (Exception e){
}

这种是完全关闭 HTML 标签解析,所以可能对正常显示会造成影响,遇到了再看怎么修复。

修复foreign派生错误的bug

该版本无法正常使用windows/foreign/reverse_http(s)进行 spawn ,但没有做判断,导致这里的beaconDLL.customDLLbeaconDLL.customFileName会为空:

暂时用直接返回原 DLL 的方式来修复,这样无法使用自定义反射 Loader:

后续再看有没有更好的办法处理。

修改winvnc.dll位置

server\ManageUser.java中修改,我放进了 resources 里,这样就不用单独开一个文件夹了:

虽然 VNC 这个功能几乎用不上。

启用Payload Generator(Stageless)

这个是为了方便做自己想要的 Loader,因为 Stage 的 shellcode 特征更多。

修改scripts\default.cna

修复Windows 7下https问题

新版本的 Java 会默认禁用 TLS1.2 以下的协议,这样在无补丁的 Windows 7 上无法用 HTTPS 上线,修改server\TeamServer.java

修复CVE-2022-23317

CVE-2022-23317是 Cobalt Strike Webserver 的一个问题,利用不规则的URL访问时会产生特殊回显,导致可以识别该服务器为Cobalt Strike server,所以加个判断就行,在cloudstrike\WebServer.java中:

去除服务端信息特征

  1. 删除服务端响应字符串

cloudstrike/NanoHTTPD.java里修改回显字符串This is the default!为其他的:

这里直接把sendError的第二个参数弃用,所有错误都只返回一个字符串:

  1. 修改socket通信的magic值

ssl\SecureServerSocket.javassl\SecureSocket.java里都把0xBEEF改掉:

修改默认异或密钥

这里不再使用原有的单字节异或密钥,而是改为多字节加密:

修改BeaconEye特征

实际就是调用memset()时使用了0来填充,导致会有一个位置的值固定呈现为0,这个值又根本用不到,所以直接把memset()的参数改为另一个值即可:

Sleep Mask未混淆部分的内存特征

https://codex-7.gitbook.io/codexs-terminal-window/blue-team/detecting-cobalt-strike/sleep-mask-kit-iocs

这个暂时先不修复,可以靠高版本sleepmask解决。

其他

修改默认client配置文件名,在aggressor\Prefs.java中:

这个是为了防止蜜罐读取本地文件,导致接管CS。

修复BeaconFormatToString错误

4.6版本更新日志中提到一个修复: 修复了在 BOF 中调用 BeaconFormatToString 时错误地要求传递字符串长度地址的问题。

BeaconFormatToString的功能为将格式化数据提取为一个字符串,并用该字符串的长度填充传入的 size 变量:

char * BeaconFormatToString (formatp * obj, int * size)

定位一下,发现该函数结果来源于:

char * bformat_tostring(formatp * buffer, int * size) {
	DWORD len = bformat_length(buffer);
	if (size != NULL)
		*size = (int)len;

	if (size == 0)
		return NULL;

	return bformat_string(buffer);
}

问题很明显,传入的是字符串长度的指针,但是却判断了它是否为0,实际上应该判断的是len:

char * bformat_tostring(formatp * buffer, int * size) {
    DWORD len = bformat_length(buffer);    
    
    if (size != NULL) {
        *size = (int)len;
    }

    if (len == 0) {
        return NULL;
    }

    char *str = bformat_string(buffer);
    return str;
}

0x3 普通功能增加

已登录用户显示

用户登录如果使用同一个用户名,会提示该用户已存在,校验点在server/ManageUser.java

而已登录的用户名数据是存在server.Resources里的,所以要从这里面把数据取出来。

这里要理清代码结构还是挺麻烦的,有以下几个问题需要解决:

  1. 找到teamserver向client传递数据的方法,然后把已登录用户数据加进来
  2. 用户登录和用户退出要自动进行增减

server.Resources里我新加了一个成员变量,用来存储用户的用户名、登录IP、登录时间,这个变量得是LinkedList对象,不然后期解析会出问题。

getOnlineUsers()就是获取数据的方法:

用户登录IP在TeamSocket对象的from字段,加了个方法用来获取它。

之后则是在用户登录和退出的时候,自动将LinkedList对象中的hashmap进行增删:

这里最主要把LinkedList对象中的格式搞清楚。

之后就是在用户登录和退出的地方调用这两个方法:

还有一个问题就是传递数据,也是调试了挺久,最后发现用broadcast()方法就可以:

最后就是添加UI,这个也麻烦,最终是照着Credential显示功能做完了:

最后的效果:

Windows错误代码解析

这个功能是新版本官方加了的,虽然不难,但Windows错误代码得有几万个,全弄进来麻烦死了。

beacon/BeaconC2.java这里是 Teamserver 向 Client 发送执行结果回显的代码:

errorNo是 Cobalt Strike 本身的错误代码,可以在beacon/BeaconErrors.java里找到:

arg1就是 Windows 错误代码,去做个判断就行,但是几万个。。。算了吧,手查一下也费不了什么事。

0x4 Beacon细节

通信情况

Cobalt Strike 用 AES 和 RSA 结合的方式进行通信,teamserver会新建一个密钥对并存储在.cobaltstrike.beacon_keys中。

RSA 使用RSA/ECB/PKCS1Padding,AES 使用AES/CBC/NoPadding,并使用了硬编码的iv:abcdefghijklmnop

数据包结构

1. 心跳包

心跳包就是Beacon 按照 Sleep 设置的时间,定时向 TeamServer 发起 HTTP GET 请求,以获取客户端下发的指令,然后根据指令的内容执行对应的代码。

心跳包结构:

  • Magic Number(固定值:0x0000beef即48879),4字节
  • Size of Data,4字节(值要小于等于117字节)
  • Raw key,16字节
  • Code Page Charset,这里是ANSI,2字节,小端序
  • OEM Charset,2字节,小端序
  • Beacon ID,4字节,大端序,每个Beacon唯一,随机生成
  • Beacon Process ID,4字节,大端序
  • Port,2字节
  • Flag,1字节,用来标识是否为高权限、x86/x64
  • Beacon Version,2字节
  • Beacon Build Version,2字节
  • 64 bit Pointer Prefix,4字节,大端序,目标系统为64位则添加函数指针前缀,32位则丢弃
  • GetModuleHandleA ptr,4字节,用来帮助shellcode在没有显式导入的情况下解析其他函数。如果beacon是64位的,则前两个字节带有前缀并将值存储在变量中
  • GetProcAddress ptr,4字节,同上
  • Target IP,4字节,按C0A8C882的方式解析
  • Target Info,剩余字节,ASCII编码,不超过58字节,包括了主机名、用户名、Beacon进程名,中间以\t分割

心跳包会用RSA公钥加密,再根据 C2Profile 的配置进行编码,然后通过GET请求发送到teamserver

之后通信时,会使用AES加密。

2. 任务下发通信包

当在teamserver端执行命令后,命令会被放入任务队列,等待下一次beacon回连请求时,再通过响应包的方式传递给beacon端。

心跳包中的Raw key将用来生成AES KeyHMAC Key,生成方法是计算Raw key的SHA256值,之后前16字节为AES Key,后16字节为HMAC Key.

raw key:

ef5ed760fba60e1973c8c3dbe001d86b

获得AESkey和HMAC key:

import hashlib

hex_string = "e7b6c3dcbbaa7d6f8d6c7a5b4c3d2e1"
byte_string = bytes.fromhex(hex_string)
hash_object = hashlib.sha256(byte_string)
hex_dig = hash_object.hexdigest()
print(hex_dig)
 aeskey:  97bc13f770f8dc2547a530536f08c2f1
 hmackey: 876d31a333a9fdb6e46043b434f41246

指令下发数据,64字节:

39aa6deab09da5f37125d732d559aba7465d1c648bfd3696cc0486dcb0ff88235ca309d67cf911da37e6e03d6628b13b1ac60df7a597517038d939cf1ab2880b71f343e7599a704c815099837e258598

其中最后16个字节为HMAC签名,用来校验数据包是否完整且未篡改。

进行AES解密,IV为 abcdefghijklmnop,模式为AES/CBC/NoPadding,解得hex为:

6530a91d0000002b0000004e000000230000000925434f4d535045432500000010202f432077686f616d69202f70726976000041414141414141414141414141c2b4a44fad005096e1c2b19e1ddeca67

这里就是下发的命令:

6530a91d 前4个字节丢弃,实际是系统时间/1000
0000002b 数据大小
0000004e 任务类型 0x4e = 78 即SHELL类型任务
00000023 4字节,标识有效命令字节长度
00000009 4字节,标识环境变量的长度
25434f4d5350454325 9字节,环境变量,标识了cmd.exe的位置
00000010 命令执行参数的长度
202f432077686f616d69202f707269760000 命令执行参数的内容
41414141414141414141414141 长度不足16倍数的情况下会补A字符

3. 完成后回传结果通信包

执行完成后,beacon用POST方式请求teamserver,将结果回传:

这里的结果也是AES加密的:

000001f008fb3bd328ed1c66c1539f3613a2848bd1e9ec3dfca9dba55351bbd3eae2e335ce338cc12ce848275713ec035eaeac838460244409d67fb63c44312fa703ec858d4bf6b365584acf487b35ade23af66be28a32b29dba65b216bd6d1ff96ab418c8ec919b7bc5a83b486f99d66193653137b47d6d25fef61ab87517fa7293f8b7b9c9d75d5ced6d139251d1695ca4589a56a8a81464d474a54adce068f9150406c97d58f69a49ed3b997d65e1e1764595d37e8254442226d00e92c1a8a4f2ac43e317d905ad66a77466533b720cad9bcbedb4d676df90f38ec442f06cd7e9392e8ab00e6c2f950b1c6c9814079fcdf6dd378e4f61e5eeb2a8ff86e1de8d9d5de8d95a84e61221f76b278e56197c3d23de3fb9b032e961a2ea5cc1e6f7d601ed4a5880465a28edd8a95f1a6ee8e1cccf0fd31a737e2fe139c3c40dd481a5255962fb84c02016535a803a257eff851ab496831d6788650f2644b250fbafb9723a8fed195f0d85633465d1553abe27a71e95047a2ddc68245b2107dfc723e3d761185b4fb6f82fbf9915775a008bab39fe98c1ac17ea1327071deb91d68e64de3108435864dde5f36423a0488d9ffc5b9a0a6e83e6b65918a84f1476c2904d8236f6161fbaec21d2868344e08241885aabbfaf1e63f46791a96ce75921e5a8dc89d853c36b3f65b606328a8e27a05d02f242

去掉前面4个字节和最后的16个字节,AES解密得到:

00000002000001c70000001e0d0accd8c8a8d0c5cfa20d0a2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0d0a0d0accd8c8a8c3fb202020202020202020202020202020202020202020202020c3e8caf62020202020202020202020202020202020d7b4ccac20200d0a3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d203d3d3d3d3d3d0d0a536553687574646f776e50726976696c6567652020202020202020202020b9d8b1d5cfb5cdb320202020202020202020202020d2d1bdfbd3c30d0a53654368616e67654e6f7469667950726976696c65676520202020202020c8c6b9fdb1e9c0fabcecb2e9202020202020202020d2d1c6f4d3c30d0a5365556e646f636b50726976696c65676520202020202020202020202020b4d3c0a9d5b9ceebc9cfc8a1cfc2bcc6cbe3bbfa20d2d1bdfbd3c30d0a5365496e637265617365576f726b696e6753657450726976696c65676520d4f6bcd3bdf8b3ccb9a4d7f7bcaf20202020202020d2d1bdfbd3c30d0a536554696d655a6f6e6550726976696c6567652020202020202020202020b8fcb8c4cab1c7f820202020202020202020202020d2d1bdfbd3c30d0a0000000000000000000000000000000000

回传消息包结构为:

结果长度 4字节 大端序 000001f0 即496(4+4+4+451+17+16)
加密内容 480字节
  	计数器 4字节 大端序 00000002 
  	后续结果长度(类型+结果) 4字节 大端序 000001c7 即455
  	callback类型 4字节 大端序  0000001e 即30 
  	执行结果 451字节
  	padding 补充了17字节的0
HMAC签名 16字节

自实现beacon要点

1. 配置信息与编译

Cobalt Strike 的生成exe,是把c2profile的信息异或加密后再patch进去的,beacon在初始化时会解密然后解析。

为了实现云编译以及多样化,c2profile的信息最好不要直接写死在代码中,从外部读取解析最好。

两种方案,第一种是参照WBGII的ReBeacon_src项目,在CS上生成raw,提取相应信息再patch进代码,这种就还需要异或解密一次;第二种则是完全的自实现,只依赖配置好的c2profile内容和部分信息。

针对自实现方案,为实现正常上线和控制,需要外部的相关信息有(针对http/https监听器):

  • RSA公钥:用来加密心跳包的元数据,存储在teamserver生成的.cobaltstrike.beacon_keys
  • teamserver地址:IP+端口或者域名+端口
  • GET请求信息:用来传输心跳包
  • POST请求和响应信息:用来传输任务结果
  • 代理
  • 睡眠时间及抖动

可以看出,对于自实现,我们只关注网络通信部分是否能与teamserver保持一致,其他部分都可以不参照原本的beacon实现,具体项目可以参考geacon之类的。

2. 命令执行

CS的命令执行有3种,shellexecuterun

shell是利用cmd.exe执行,命名管道读取回显的;excute只执行不回显;而run执行且回显。

执行命令要考虑令牌的问题,runas命令本身就考虑了这个。

3. beacon运行时的初始化

初始化需要完成以下几个操作:

  1. 申请内存
  2. 异或解密 beacon 配置信息
  3. 解析配置信息

重点在于这个解析环节,在 teamserver 端,每一项都是用Type-Length-Value的方式来存储的:

到了beacon端,Type有3种值:1 - Short2 - Int 3 - Data,但实际上只有DataLength用得上,因为ShortInt的长度是已知的。

这个问题也导致了beacon的配置文件内存区域存在特征,可以被BeaconEye扫描到。

以及在beacon端,配置信息的结构是这样:

另外,beacon还有个主机轮换策略,这个我平时很少用,分为四种:

  • round-robin:选择按主机名提供的顺序循环遍历主机名列表。每台主机用于一个连接。
  • random:选择此项可在每次尝试连接时从列表中随机选择一个主机名。
  • failover-xx:选择尽可能长时间地使用工作主机。使用列表中的每个主机,直到它们达到连续故障转移计数 (x) 或持续时间段 (m,h,d),然后使用下一个主机。
  • rotate-xx:选择使用每个主机一段时间。使用列表中的每个主机指定的持续时间 (m,h,d),然后使用下一个主机。

0x5 总结

可能你会说:怎么就戛然而止了?因为以上就是之前基本所有的笔记了,从结构上来说肯定是不全的,后续也没继续研究了,凑合看吧。