本帖最后由 zhaorong 于 2021-9-8 15:03 编辑
Cobalt Strike破解思路
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.ConsoleDe
- compiler -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:+Aggre
ssiveHeap -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("MM
- MMM 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().getRe
- sourceAsStream("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.le
- ngth + " 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文件读取key的流程。
前一种方法RedCore@Moriarty师傅和Castiel师傅都提供了工具 贴一个链接
GitHub - ca3tie1/CrackSleeve: 破解CS4.0
3. Hook
Hook方法可以不修改原来的源码 将认证的Authorization类做热替换即可 对Java不够熟悉 就不实践了。
收尾工作
众所周知 Cobalt Strike官方会在代码里埋暗桩,4.3版本有一个在beacon/BeaconData的shouldPad方法中
此处会对beacon产生影响,造成30分钟自动退出的情况,原因在beacon/BeaconC2中 使用isPaddingReq
uired方法对文件进行了校验 防止被篡改:
修改时只需将shouldPad方法的值写死即可:
- public void shouldPad(boolean var1) {
- this.shouldPad = false;
- this.when = System.currentTimeMillis() + 1800000L;
- }
复制代码
关于Cobalt Strike的破解思路和方法已经介绍完了 目前最新版本是4.4 但我还没有拿到
key看样子增加了更多的暗桩 有机会再详细介绍。 |