Cobalt Strike破解思路
本文首发于i春秋社区:https://bbs.ichunqiu.com/thread-61581-1-1.html
未经许可,禁止转载
Cobalt Strike现在已经更新到了4.4版本,学习一下破解思路,以后就不怕各种后门版本了。
环境配置
目录结构以4.3版本(已破解)为例:
cobaltstrike4.3
├─ agscript 扩展脚本
├─ c2lint 检查C2文件配置
├─ cobaltstrike 客户端启动脚本
├─ cobaltstrike.auth 认证密钥文件
├─ cobaltstrike.exe
├─ cobaltstrike.jar 主程序jar包
├─ icon.jpg
├─ peclone
├─ start.sh
├─ teamserver 服务端启动脚本
├─ third-party
│ ├─ README.winvnc.txt
│ ├─ winvnc.x64.dll vnc服务端dll
│ └─ winvnc.x86.dll
├─ update 更新脚本
├─ update.bat
└─ update.jar
主要是针对cobaltstrike.jar
,反编译修改后再打包成jar包,此处的思路主要来自于RedCore@Moriarty师傅的公开课。
反编译
使用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项目环境
IDEA新建项目,将反编译后的所有源码放入decompiled_src
目录,原包放入lib
目录,再在File
-Project Structure
-Modules
-Dependencies
中添加原包:
在File
-Project Structure
-Artifacts
中添加JAR
,主类为aggressor.Aggressor
:
需要修改相应文件时,右键选择Refactor
-Copy file
,To directory
选择src
目录里新建的目录:
修改完成后就可以进行编译,选择Build
-Build Artifacts
,在out
目录下得到jar包:
接下来调试运行,配置选择JAR Application
,VM options
填入-XX:+AggressiveHeap -XX:+UseParallelGC
:
最后将cobaltstrike.auth
放在刚刚打包好的JAR包目录下即可。
认证流程
主类Aggressor
中开始进行认证流程:
License.checkLicenseGUI(new Authorization());
跟入checkLicenseGUI
,这里主要检测.auth
文件的有效性:
调用了三个Authorization
类的方法进行验证,从第一个isValid
开始看,跟入后可以看到isValid
相当于一个flag,默认为false,在Authorization
类的构造方法中进行验证,成功后设置为true.
第二个isPerpetual
则是验证关键字forever
是否存在,不存在就说明你的不是正式发行版,而是试用版或者已经过期的版本。
第三个isAlmostExpired
计算了有效期。
来看Authorization
类的构造方法:
public Authorization() {
String var1 = CommonUtils.canonicalize("cobaltstrike.auth");
if (!(new File(var1)).exists()) {
try {
File var2 = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
if (var2.getName().toLowerCase().endsWith(".jar")) {
var2 = var2.getParentFile();
}
var1 = (new File(var2, "cobaltstrike.auth")).getAbsolutePath();
} catch (Exception var17) {
MudgeSanity.logException("trouble locating auth file", var17, false);
}
}
byte[] var18 = CommonUtils.readFile(var1);
if (var18.length == 0) {
this.error = "Could not read " + var1;
} else {
AuthCrypto var3 = new AuthCrypto();
byte[] var4 = var3.decrypt(var18);
if (var4.length == 0) {
this.error = var3.error();
} else {
try {
DataParser var5 = new DataParser(var4);
var5.big();
int var6 = var5.readInt();
this.watermark = var5.readInt();
byte var7 = var5.readByte();
if (var7 < 43) {
this.error = "Authorization file is not for Cobalt Strike 4.3+";
return;
}
byte var8 = var5.readByte();
var5.readBytes(var8);
byte var10 = var5.readByte();
var5.readBytes(var10);
byte var12 = var5.readByte();
var5.readBytes(var12);
byte var14 = var5.readByte();
byte[] var15 = var5.readBytes(var14);
if (29999999 == var6) {
this.validto = "forever";
MudgeSanity.systemDetail("valid to", "perpetual");
} else {
this.validto = "20" + var6;
MudgeSanity.systemDetail("valid to", CommonUtils.formatDateAny("MMMMM d, YYYY", this.getExpirationDate()));
}
this.valid = true;
MudgeSanity.systemDetail("id", this.watermark + "");
SleevedResource.Setup(var15);
} catch (Exception var16) {
MudgeSanity.logException("auth file parsing", var16, false);
}
}
}
}
前面都是判断文件存在和读取的代码,主要从这里开始看起:
AuthCrypto var3 = new AuthCrypto();
byte[] var4 = var3.decrypt(var18);
初始化了一个AuthCrypto
类,调用decrypt
方法解密,得到一个字节数组。跟入AuthCrypto
类就可以发现它的构造函数中调用了一个load()
方法,继续跟入:
public void load() {
try {
byte[] var1 = CommonUtils.readAll(CommonUtils.class.getClassLoader().getResourceAsStream("resources/authkey.pub"));
byte[] var2 = CommonUtils.MD5(var1);
if (!"8bb4df00c120881a1945a43e2bb2379e".equals(CommonUtils.toHex(var2))) {
CommonUtils.print_error("Invalid authorization file");
System.exit(0);
}
X509EncodedKeySpec var3 = new X509EncodedKeySpec(var1);
KeyFactory var4 = KeyFactory.getInstance("RSA");
this.pubkey = var4.generatePublic(var3);
} catch (Exception var5) {
this.error = "Could not deserialize authpub.key";
MudgeSanity.logException("authpub.key deserialization", var5, false);
}
}
resources/authkey.pub
就是公钥文件,对比文件的hash,确认是否符合要求。
decrypt
方法是用来解密.auth
文件的,并对文件头进行校验:
public byte[] decrypt(byte[] var1) {
byte[] var2 = this._decrypt(var1);
try {
if (var2.length == 0) {
return var2;
} else {
DataParser var3 = new DataParser(var2);
var3.big();
int var4 = var3.readInt();
if (var4 == -889274181) {
this.error = "pre-4.0 authorization file. Run update to get new file";
return new byte[0];
} else if (var4 != -889274157) {
this.error = "bad header";
return new byte[0];
} else {
int var5 = var3.readShort();
byte[] var6 = var3.readBytes(var5);
return var6;
}
}
} catch (Exception var7) {
this.error = var7.getMessage();
return new byte[0];
}
}
真正RSA解密的部分是_decrypt
方法:
protected byte[] _decrypt(byte[] var1) {
byte[] var2 = new byte[0];
try {
if (this.pubkey == null) {
return new byte[0];
} else {
synchronized(this.cipher) {
this.cipher.init(2, this.pubkey);
var2 = this.cipher.doFinal(var1);
}
return var2;
}
} catch (Exception var6) {
this.error = var6.getMessage();
return new byte[0];
}
}
这里要提一下RSA算法的加密和解密,它是一种非对称加密算法,也就是有公钥和私钥,公钥用来加密,私钥用来解密。但并不是说公钥就只能用来加密,就像这里,.auth
文件需要用公钥来解密,它的明文就是用私钥来加密的。
好了,现在我们看完了RSA解密.auth
文件及验证的部分,接着看:
DataParser var5 = new DataParser(var4);
var5.big();
int var6 = var5.readInt();
this.watermark = var5.readInt();
byte var7 = var5.readByte();
if (var7 < 43) {
this.error = "Authorization file is not for Cobalt Strike 4.3+";
return;
}
将解密之后的.auth
文件解析为byte 类型,之后读取四个字节转换为整数值,var6
的值就是用来判断授权有效与否的,与前面说过的isPerpetual
方法相关,如果不为29999999就是20天的试用版本。
再继续读取四个字节,这里this.watermark
值是用来判断是否填充水印特征的,在common/ListenerConfig
中可以看到:
if (this.watermark == 0) {
var3.append("5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\u0000");
} else {
var3.append((char)CommonUtils.rand(255));
}
watermark
值为0就会添加这个字符串,这是EICAR测试字符,扫描到这个字符串的杀软会直接报毒,因为它是被用来测试杀毒软件响应程度的。
继续读取一个字节,var7
是用来判断版本的,高版本不能使用低版本的.auth
文件。
接下来是这段:
byte var8 = var5.readByte();
var5.readBytes(var8);
byte var10 = var5.readByte();
var5.readBytes(var10);
byte var12 = var5.readByte();
var5.readBytes(var12);
byte var14 = var5.readByte();
byte[] var15 = var5.readBytes(var14);
这里4.3版本相比4.0版本多了一些代码,实际上是包含了前面版本的key,也就是说4.3版本的.auth
文件里有4.0、4.1、4.2的key,应该是为了兼容以前的版本。最后得到var15
,用在这里:
SleevedResource.Setup(var15);
这是4.0版本新增的验证步骤,跟入这个类:
public class SleevedResource {
private static SleevedResource singleton;
private SleeveSecurity data = new SleeveSecurity();
public static void Setup(byte[] var0) {
singleton = new SleevedResource(var0);
}
public static byte[] readResource(String var0) {
return singleton._readResource(var0);
}
private SleevedResource(byte[] var1) {
this.data.registerKey(var1);
}
private byte[] _readResource(String var1) {
String var2 = CommonUtils.strrep(var1, "resources/", "sleeve/");
byte[] var3 = CommonUtils.readResource(var2);
if (var3.length > 0) {
long var7 = System.currentTimeMillis();
byte[] var6 = this.data.decrypt(var3);
return var6;
} else {
byte[] var4 = CommonUtils.readResource(var1);
if (var4.length == 0) {
CommonUtils.print_error("Could not find sleeved resource: " + var1 + " [ERROR]");
} else {
CommonUtils.print_stat("Used internal resource: " + var1);
}
return var4;
}
}
}
初始化了SleevedResource
类,其私有构造方法里又调用了SleeveSecurity.registerKey
方法,参数为刚刚最后得到的var15
:
public void registerKey(byte[] var1) {
synchronized(this) {
try {
MessageDigest var3 = MessageDigest.getInstance("SHA-256");
byte[] var4 = var3.digest(var1);
byte[] var5 = Arrays.copyOfRange(var4, 0, 16);
byte[] var6 = Arrays.copyOfRange(var4, 16, 32);
this.key = new SecretKeySpec(var5, "AES");
this.hash_key = new SecretKeySpec(var6, "HmacSHA256");
} catch (Exception var8) {
var8.printStackTrace();
}
}
}
使用传入的值计算一个长度为256的摘要,再取0-16作为AES的密钥,取16-32作为HmacSHA256的密钥。这里就结束了,但是既然取了密钥,那么肯定要进行操作,可以在SleeveSecurity.decrypt
方法中看到:
public byte[] decrypt(byte[] var1) {
try {
byte[] var2 = Arrays.copyOfRange(var1, 0, var1.length - 16);
byte[] var3 = Arrays.copyOfRange(var1, var1.length - 16, var1.length);
Object var4 = null;
byte[] var14;
synchronized(this) {
this.mac.init(this.hash_key);
var14 = this.mac.doFinal(var2);
}
byte[] var5 = Arrays.copyOfRange(var14, 0, 16);
if (!MessageDigest.isEqual(var3, var5)) {
CommonUtils.print_error("[Sleeve] Bad HMAC on " + var1.length + " byte message from resource");
return new byte[0];
} else {
Object var6 = null;
byte[] var15;
synchronized(this) {
var15 = this.do_decrypt(this.key, var2);
}
DataInputStream var7 = new DataInputStream(new ByteArrayInputStream(var15));
int var8 = var7.readInt();
int var9 = var7.readInt();
if (var9 >= 0 && var9 <= var1.length) {
byte[] var10 = new byte[var9];
var7.readFully(var10, 0, var9);
return var10;
} else {
CommonUtils.print_error("[Sleeve] Impossible message length: " + var9);
return new byte[0];
}
}
} catch (Exception var13) {
var13.printStackTrace();
return new byte[0];
}
}
这里校验HMAC,正确后进行AES解密。
寻找调用,SleevedResource._readResource
方法中存在调用:
private byte[] _readResource(String var1) {
String var2 = CommonUtils.strrep(var1, "resources/", "sleeve/");
byte[] var3 = CommonUtils.readResource(var2);
if (var3.length > 0) {
long var7 = System.currentTimeMillis();
byte[] var6 = this.data.decrypt(var3);
return var6;
} else {
byte[] var4 = CommonUtils.readResource(var1);
if (var4.length == 0) {
CommonUtils.print_error("Could not find sleeved resource: " + var1 + " [ERROR]");
} else {
CommonUtils.print_stat("Used internal resource: " + var1);
}
return var4;
}
}
这个方法接受一个字符串作为文件路径,并将路径中的resources/
替换为sleeve/
,之后读取文件内容并进行解密。此处存放的都是重要功能的dll文件,如果不能正常解密,就会出现虽然能正常打开登录,但是使用功能时会出现很大限制。
破解方法
其实从流程上可以看出,最重要的部分就是:
SleevedResource.Setup(var15);
这个key非常关键,拿到了它才能进行之后的解密。
那么有没有可能从末尾反推到这个值?末尾是HMAC校验和AES解密所使用的密钥,了解过密码学之后就会发现这无异于痴人说梦。
官方用这个key加密了sleeve
下的dll,将key放在了.auth
文件中,那么key应该是一个固定值,如果是随机或者根据用户身份计算得到的话,就无法保证官网jar包的hash值全部一样了。
1. 自己生成auth文件
拿到key之后,可以自己生成一份.auth
文件。前面说过,.auth
文件是用RSA公钥解密的,我们没私钥,怎么加密明文呢?答案就是自己生成一对密钥,用自己的公钥替换官方给的公钥即可。
从头梳理一下.auth
文件的要求:
- 6位字节,特定的文件头
- 4位字节,转换为有符号整数后等于29999999
- 4位字节,转换为有符号整数后不等于0
- 1位字节,其值大于43小于128
- 1位字节,其值为16
- 16位字节,值为key,这里注意4.3版本还包含了之前的key和key长度
那么4.3版本的.auth
文件有效长度应该为83位字节,即4.0版本为32位,之后每一个版本都在前面版本的基础上增加17位。
转换一下:
public class authTest {
public byte[] intToByteArray(int num){
return new byte[] {
(byte) ((num >> 24) & 0xFF),
(byte) ((num >> 16) & 0xFF),
(byte) ((num >> 8) & 0xFF),
(byte) (num & 0xFF)
};
}
public static void main(String[] args){
authTest authTest = new authTest();
int header = -889274157;
int num = 29999999;
int watermark = 1;
byte[] bheader = authTest.intToByteArray(header);
byte[] bnum = authTest.intToByteArray(num);
byte[] bwatermark = authTest.intToByteArray(watermark);
}
}

得出4.0版本的byte[]
为:
byte[] decrypt = {
-54, -2, -64, -45, 0, 0, //文件头
1, -55, -61, 127, //时间
0, 0, 0, 1, //水印
50, //版本
16, //key长度
27, -27, -66, 82, -58, 37, 92, 51, 85, -114, -118, 28, -74, 103, -53, 6 //key
};
4.1的key为:
byte[] key41 = {-128, -29, 42, 116, 32, 96, -72, -124, 65, -101, -96, -63, 113, -55, -86, 118 };
4.2的key为:
byte[] key42 = {-78, 13, 72, 122, -35, -44, 113, 52, 24, -14, -43, -93, -82, 2, -89, -96};
4.3的key为:
byte[] key43 = {58, 68, 37, 73, 15, 56, -102, -18, -61, 18, -67, -41, 88, -83, 43, -103};
已经明确了.auth
文件的内容,剩下就只需要生成RSA公私钥,然后使用私钥加密.auth
文件,并把公钥文件authkey.pub
替换到resources
目录下,最后记得修改common/AuthCrypto
中load()
方法的MD5值。
2. 解密dll
还有一种思路是先将sleeve
目录下的dll解密,自定义key,再使用新的私钥加密dll,或者直接把key硬编码在代码中,注释掉.auth
文件验证的流程。
前一种方法RedCore@Moriarty师傅和Castiel师傅都提供了工具,贴一个链接GitHub - ca3tie1/CrackSleeve: 破解CS4.0
3. Hook
Hook方法可以不修改原来的源码,将认证的Authorization
类做热替换即可,对Java不够熟悉,就不实践了。
收尾工作
众所周知,Cobalt Strike官方会在代码里埋暗桩,4.3版本有一个在beacon/BeaconData
的shouldPad
方法中,此处会对beacon产生影响,造成30分钟自动退出的情况,原因在beacon/BeaconC2
中,使用isPaddingRequired
方法对文件进行了校验,防止被篡改:
修改时只需将shouldPad
方法的值写死即可:
public void shouldPad(boolean var1) {
this.shouldPad = false;
this.when = System.currentTimeMillis() + 1800000L;
}
关于Cobalt Strike的破解思路和方法已经介绍完了,目前最新版本是4.4,但我还没有拿到key,看样子增加了更多的暗桩,有机会再详细介绍。
参考链接
- https://rcoil.me/2020/11/%E3%80%90%E7%9F%A5%E8%AF%86%E5%9B%9E%E9%A1%BE%E3%80%91Cobalt%20Strike%204.0%E8%AE%A4%E8%AF%81%E5%8F%8A%E4%BF%AE%E8%A1%A5%E8%BF%87%E7%A8%8B/
- https://ca3tie1.github.io/post/cobaltstrike40-wu-hook-man-li-cracked-license-si-lu/
感谢RedCore@Moriarty师傅的公开课,还有Twi1ight的耐心解答。