白袍的小行星

Cobalt Strike破解思路

字数统计: 3.3k阅读时长: 15 min
2021/08/30 Share

本文首发于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中添加原包:
2021081610570024rDgv

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

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

修改完成后就可以进行编译,选择Build-Build Artifacts,在out目录下得到jar包:
202108161113089ZMrPK

接下来调试运行,配置选择JAR ApplicationVM options填入-XX:+AggressiveHeap -XX:+UseParallelGC
20210816111632nqJoMf

最后将cobaltstrike.auth放在刚刚打包好的JAR包目录下即可。

认证流程

主类Aggressor中开始进行认证流程:

License.checkLicenseGUI(new Authorization());

跟入checkLicenseGUI,这里主要检测.auth文件的有效性:
20210816112604aGoMh7
调用了三个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文件的要求:

  1. 6位字节,特定的文件头
  2. 4位字节,转换为有符号整数后等于29999999
  3. 4位字节,转换为有符号整数后不等于0
  4. 1位字节,其值大于43小于128
  5. 1位字节,其值为16
  6. 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);
}
}

202108170916525XrY1s

得出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/AuthCryptoload()方法的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/BeaconDatashouldPad方法中,此处会对beacon产生影响,造成30分钟自动退出的情况,原因在beacon/BeaconC2中,使用isPaddingRequired方法对文件进行了校验,防止被篡改:
20210817162913PluI5P
20210817162933xmN67S

修改时只需将shouldPad方法的值写死即可:

public void shouldPad(boolean var1) {
this.shouldPad = false;
this.when = System.currentTimeMillis() + 1800000L;
}

关于Cobalt Strike的破解思路和方法已经介绍完了,目前最新版本是4.4,但我还没有拿到key,看样子增加了更多的暗桩,有机会再详细介绍。

参考链接

感谢RedCore@Moriarty师傅的公开课,还有Twi1ight的耐心解答。

CATALOG
  1. 1. 环境配置
    1. 1.1. 反编译
    2. 1.2. IDEA项目环境
  2. 2. 认证流程
  3. 3. 破解方法
    1. 3.1. 1. 自己生成auth文件
    2. 3.2. 2. 解密dll
    3. 3.3. 3. Hook
  4. 4. 收尾工作
  5. 5. 参考链接