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中添加原包:
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,看样子增加了更多的暗桩,有机会再详细介绍。

参考链接

  • 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的耐心解答。