用户
搜索
  • TA的每日心情
    无聊
    2021-8-17 17:03
  • 签到天数: 17 天

    连续签到: 1 天

    [LV.4]经常看看II

    i春秋-见习白帽

    Rank: 3Rank: 3

    2

    主题

    46

    帖子

    222

    魔法币
    收听
    0
    粉丝
    0
    注册时间
    2017-10-11
    发表于 2021-8-17 16:59:04 07388

    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.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文件读取key的流程。
    前一种方法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师傅的耐心解答。

    发新帖
    您需要登录后才可以回帖 登录 | 立即注册