文章其实22年就写了,因为一些原因没有发,本来是笔记,删删改改处理了一些东西后发布

之前研究的版本太老了,找个稍微新一点的版本,选用CS4.5进行研究,另外,刚好官方文档描述CS4.5更新了license验证机制,官网链接https://www.cobaltstrike.com/blog/cobalt-strike-4-5-fork-run-youre-history

image-20241215153237060

二开环境配置

下载完先和官网对比一下hash值避免被加料

image-20241211000754709

反编译的方法很多就不赘述了,百度一下能搜到一堆(jd、java-decompiler…)但是由于有些工具无法识别lambda表达式,因此选择的时候建议找个合适的

并在dependencies中加入依赖,直接添加library或从jar包添加都行

image-20241215112215268

CS的启动类为aggressor.Aggressor,将该文件复制到src目录下,配置Artifact选项,以生成jar包,其中Manifest文件复制lib中的Manifest即可,之后编译Artifact包

image-20241215113422438

启动前添加配置,其中JAR地址为刚刚编译的jar包,VM选项为

-XX:ParallelGCThreads=4 -XX:+AggressiveHeap -XX:+UseParallelGC

image-20241215113528425

在代码中加入一个弹窗显示消息,并测试是否能够成功执行

JOptionPane.showMessageDialog(null,"CS4.5 Modified BY:Y4ph3tS");

image-20241215113740524

通过刚刚配置好的模板运行,弹出窗口即成功

image-20241215113856301

认证流程简析与去暗桩

由于是原版没有进行破解,所以之后会报错退出,接下来研究一下验证的逻辑,并且进行暗桩的去除

beacon/BeaconData.java

shouldPad设置为false

image-20241215115314402

beacon/CommandBuilder.java

在文件最后的static方法中,有大量对变量var1的修改,分析后发现是校验逻辑之一,如果var1为false则程序退出,因此只需保证var1永真

image-20241215120926725

接下来分析核心部分common/Authorization.java,其中主要步骤是校验cobaltstrike.auth,其中对auth文件进行了校验,读取到auth文件并经过格式校验后,调用AuthCrypto的decrypt进行解密

image-20241215141215124

跟进AuthCrypto,可见其中有一个RSA Key验证操作,先读取authkey.pub公钥文件的md5值,如果通过验证就获取其中的公钥值,生成的操作,

image-20241215141457879

完整代码如下:

public AuthCrypto() {
      try {
         this.A = Cipher.getInstance("RSA/ECB/PKCS1Padding");
         this.A();
      } catch (Exception var2) {
         this.B = "Could not initialize crypto";
         MudgeSanity.logException("AuthCrypto init", var2, false);
      }

   }

   private void A() {
      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.C = var4.generatePublic(var3);
      } catch (Exception var5) {
         this.B = "Could not deserialize authpub.key";
         MudgeSanity.logException("authpub.key deserialization", var5, false);
      }

   }

   public String error() {
      return this.B;
   }

   protected byte[] decrypt(byte[] var1) {
      byte[] var2 = this.A(var1);

      try {
         if (var2.length == 0) {
            return var2;
         } else {
            DataParser var3 = new DataParser(var2);
            var3.big();
            int var4 = var3.readInt();
            if (var4 == -889274181) {
               this.B = "pre-4.0 authorization file. Run update to get new file";
               return new byte[0];
            } else if (var4 != -889274157) {
               this.B = "bad header";
               return new byte[0];
            } else {
               int var5 = var3.readShort();
               byte[] var6 = var3.readBytes(var5);
               return var6;
            }
         }
      } catch (Exception var7) {
         this.B = var7.getMessage();
         return new byte[0];
      }
   }

   private byte[] A(byte[] var1) {
      byte[] var2 = new byte[0];

      try {
         if (this.C == null) {
            return new byte[0];
         } else {
            synchronized(this.A) {
               this.A.init(2, this.C);
               var2 = this.A.doFinal(var1);
            }

            return var2;
         }
      } catch (Exception var6) {
         this.B = var6.getMessage();
         return new byte[0];
      }
   }
}

authkey.pub在反编译出的resources目录下,可以通过ASN1editor去看看公钥

image-20241215145635309

简单了解了decrypt函数所在的AuthCrypto类后,回到authorization,解密完后,就开始读取auth文件中对应内容,包含水印版本,中间没调用的部分为读取后跳过,未给相关变量赋值参与校验过程,在后面会提到

image-20241215152100003

根据和历史版本的比较,多了一个watermark水印部分,全局搜索字符串推测应该和beacon生成有关

image-20241215151434603

继续往后分析,其中的var6应该是时间信息,如果值为0x1C9C37F则有效时间为永久,否则则对有效期进行格式化

image-20241215152209304

翻了半天逻辑,没有auth文件还是没办法啊,RSA非对称密码算法使用公钥加密,私钥解密,没有auth文件无法进行伪造,只能找大佬的方法硬编码

byte[] var4 = {1, -55, -61, 127, 0, 1, -122, -96, 45, 16, 27, -27, -66, 82, -58, 37, 92, 51, 85, -114, -118, 28, -74, 103, -53, 6, 16, -128, -29, 42, 116, 32, 96, -72, -124, 65, -101, -96, -63, 113, -55, -86, 118, 16, -78, 13, 72, 122, -35, -44, 113, 52, 24, -14, -43, -93, -82, 2, -89, -96, 16, 58, 68, 37, 73, 15, 56, -102, -18, -61, 18, -67, -41, 88, -83, 43, -103, 16, 94, -104, 25, 74, 1, -58, -76, -113, -91, -126, -90, -87, -4, -69, -110, -42, 16, -13, -114, -77, -47, -93, 53, -78, 82, -75, -117, -62, -84, -34, -127, -75, 66, 0, 0, 0, 24, 66, 101, 117, 100, 116, 75, 103, 113, 110, 108, 109, 48, 82, 117, 118, 102, 43, 86, 89, 120, 117, 119, 61, 61};

其实硬编码后就相对好分析了,看一下逻辑是调用了DataParser函数进行解析,该函数在common/DataParser.java中

image-20241215221236289

将byte数组按大端字节序处理后,开始按位取相应的变量进行处理,根据上面已经截图的逻辑,最终将这段字符串格式化如下:

image-20241216004728854

基于此前版本比较,比之前版本新增的主要字段为watermarkhash,解密sleeve的为var19,跟一下SleevedResource类,可以看到一个调用registerkey的操作

private SleevedResource(byte[] var1) {
    this.A.registerKey(var1);
}

registerkey定义在SleeveSecurity类中,其中定义了AES、HmacSHA256解密的密钥,使用传入的值计算一个长度为256的摘要,再取0-16作为AES的密钥,取16-32作为HmacSHA256的密钥

image-20241216004026305

如果没有获取到key,无法进行解密,因为只有在解密sleeve中的dll后才能正常上线,分析整个文件

package dns;

import common.CommonUtils;
import common.MudgeSanity;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class SleeveSecurity {
   private IvParameterSpec B;
   private Cipher A;
   private Cipher C;
   private Mac F;
   private SecretKeySpec E;
   private SecretKeySpec D;

   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.E = new SecretKeySpec(var5, "AES");
            this.D = new SecretKeySpec(var6, "HmacSHA256");
         } catch (Exception var8) {
            var8.printStackTrace();
         }

      }
   }

   public SleeveSecurity() {
      try {
         byte[] var1 = "abcdefghijklmnop".getBytes();
         this.B = new IvParameterSpec(var1);
         this.A = Cipher.getInstance("AES/CBC/NoPadding");
         this.C = Cipher.getInstance("AES/CBC/NoPadding");
         this.F = Mac.getInstance("HmacSHA256");
      } catch (Exception var2) {
         throw new RuntimeException(var2);
      }
   }

   protected byte[] do_encrypt(SecretKey var1, byte[] var2) throws Exception {
      this.A.init(1, var1, this.B);
      return this.A.doFinal(var2);
   }

   protected byte[] do_decrypt(SecretKey var1, byte[] var2) throws Exception {
      this.C.init(2, var1, this.B);
      return this.C.doFinal(var2, 0, var2.length);
   }

   protected void pad(ByteArrayOutputStream var1) {
      for(int var2 = var1.size() % 16; var2 < 16; ++var2) {
         var1.write(65);
      }

   }

   public byte[] encrypt(byte[] var1) {
      try {
         ByteArrayOutputStream var2 = new ByteArrayOutputStream(var1.length + 1024);
         DataOutputStream var3 = new DataOutputStream(var2);
         var2.reset();
         var3.writeInt(CommonUtils.rand(Integer.MAX_VALUE));
         var3.writeInt(var1.length);
         var3.write(var1, 0, var1.length);
         this.pad(var2);
         Object var4 = null;
         byte[] var12;
         synchronized(this) {
            var12 = this.do_encrypt(this.E, var2.toByteArray());
         }

         Object var5 = null;
         byte[] var13;
         synchronized(this) {
            this.F.init(this.D);
            var13 = this.F.doFinal(var12);
         }

         ByteArrayOutputStream var6 = new ByteArrayOutputStream();
         var6.write(var12);
         var6.write(var13, 0, 16);
         byte[] var7 = var6.toByteArray();
         return var7;
      } catch (InvalidKeyException var10) {
         MudgeSanity.logException("[Sleeve] encrypt failure", var10, false);
         CommonUtils.print_error_file("resources/crypto.txt");
         MudgeSanity.debugJava();
         if (this.E != null) {
            CommonUtils.print_info("[Sleeve] Key's algorithm is: '" + this.E.getAlgorithm() + "' ivspec is: " + this.B);
         }
      } catch (Exception var11) {
         MudgeSanity.logException("[Sleeve] encrypt failure", var11, false);
      }

      return new byte[0];
   }

   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.F.init(this.D);
            var14 = this.F.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.E, 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];
      }
   }
}

以前写的流程有点复杂,参考大佬写的流程图,原文链接:https://xz.aliyun.com/t/8557

CS认证流程

将auth硬编码写入后发现仍报错无法找到,说明其中还有其他校验逻辑

image-20241215165717827

在common/Helper.java中还有校验逻辑,注释代码保证var2永真,其中给到了其他的一些依赖的提示(怎么有点像CTF不是),刚好再去跟一下其他几个类

image-20241215165925363

common/starter.java和common/starter2.java

其中有很多暗桩,会导致退出程序和校验,把exit方法注释,下面的函数也是,保证var2永真

image-20241215170400684

如果找不到问题在哪退出的就下断点调试,但是调试可能会有个坑,如果用IDEA进行调试的话,下了断点以后启动会报异常退出

image-20241215204305120

全局搜索一下字符串定位异常代码,发现就在Aggressor和Teamserver里,找到对应代码注释就完了,就可以解决无法调试的问题

image-20241215204411790

插入一点分析的Tips:首先是找暗桩,重点关注System.exit()函数

对Teamserver进行配置,启动命令为

java -Dfile.encoding=UTF-8 -XX:ParallelGCThreads=4 -Dcobaltstrike.server_port=[Teamserver端口] -Dcobaltstrike.server_bindto=0.0.0.0 -Djavax.net.ssl.keyStore=./cobaltstrike.store -Djavax.net.ssl.keyStorePassword=[证书密码] -server -XX:+AggressiveHeap -XX:+UseParallelGC -classpath ./Reborn.jar -Duser.language=en server.TeamServer [ip地址] [连接密码]

重新编译后启动即可成功加载,由于机器上有之前连过的机器,因此在Profile处会显示

image-20241215172435130

流量特征checksum8算法修改

先改大家都知道的最常见的checksum8算法,全局搜索定位checksum8算法,修改原算法,将下面的92L和93L修改为其他值并修改原算法

image-20241229154135295

修改完成对应算法后修改下面isStager和isStagerX64中的92和93两个值,修改为经过新算法算出来的特定值

然后再修改CommonUtils中的MSFURI和MSFURI_X64,保证其返回值传入上文中isStager中后计算得到的值相同,否则将无法连接

image-20241229155748511

修改完编译完成后记得测试一下是否能成功上线

Beacon解析与Sleeve DLL修改

因为这部分涉及到Beacon,就顺便写一下Beacon的分析,Beacon信标是Cobalt Strike 运行在目标主机上的 Payload,配合各种方式来实现持久化控制,其中较为出名的发源是从vault7中泄露的蜂巢计划,参考链接https://wikileaks.org/vault7/#Hive

首先从BeaconPayload.java进行分析,其中有一个配置解析部分,对var21进行解析,其实就是Beacon的解析,其中有各种分离出的字段

image-20250104145510736

解析完成后将string转换为byte数组,然后进行异或,并将剩余字符串填入随机字符串

image-20250104145931451

如果直接使用文本编辑器加载Beacon时无法进行解析

image-20250104150542359

使用beacon_obfuscate方法进行异或,代码如下,也就是按位异或

image-20250104151240109

所以对Beacon解析首先需要进行自解密,自解密使用以下脚本进行

import sys
import struct

filename = sys.argv[1]
with open(filename, 'rb') as f:
    data = f.read()
    
# 从偏移 0x45 处开始处理数据
t = bytearray(data[0x45:])
# 从偏移 0 处解析两个整数(小端格式 '<II')
(a, b) = struct.unpack_from('<II', t)
# 使用第一个解析出的整数作为初始密钥
key = a
# 从偏移 8 处开始,获取需要解码的实际数据
t2 = t[8:]
out = bytearray()
for i in range(len(t2) // 4):  
    temp = struct.unpack_from('<I', t2[i * 4:])[0]
    # 对数据与当前密钥进行异或解码
    temp ^= key
    out += struct.pack('<I', temp)
    # 更新密钥为当前解码出的值
    key ^= temp

output_filename = filename + '.decoded'
with open(output_filename, 'wb') as f:
    f.write(out)

print(f"解码完成,结果已保存到:{output_filename}")

自解密完成后,再对文件进行2E异或,以下为从两个文件中提取出字符串进行对比,左图为从Beacon中直接提取出的字符串,没有可识读的部分,右图是经过自解密和2E异或后中提取的字符串,Beacon中的函数,字符串等都变成了明文,IP地址等信息也包含在内

image-20250104155231543

如果通过wireshark抓包,可见Beacon的通信为TLSv1.2版本

image-20250104164652494

原Beacon使用公开分析工具解析后能获取大量信息

image-20250111152129462

sleeve解密过程其实上面讲也都提到了,使用逆过程即可,脚本github上有很多,随便找一个就能解密了,记得找到对应的版本,因为不同版本的

key不同

image-20241228140416978

解密完成后将DLL使用IDA进行分析,具体要分析的DLL可以全局搜索.dll来搜索调用情况

image-20250104111825040

上线有关的dll在BeaconPayload.java中,在文件内筛选后涉及的主要dll如下:

beacon.dll
beacon.x64.dll
dnsb.dll
dnsb.x64.dll
pivot.dll
pivot.x64.dll
extc2.dll
extc2.x64.dll

image-20250104112029935

使用别人公开的Sleeve脚本进行解密

编译( javac -encoding UTF-8 -classpath cobaltstrike.jar CrackSleeve.java)
解密文件( java -classpath cobaltstrike.jar;./ CrackSleeve decode)
无需输入Key,加密文件( java -classpath cobaltstrike.jar;./ CrackSleeve encode)

使用IDA搜索原DLL中的2E(十进制为46),即beacon_obfuscate中的异或值,主要关注异或的2e

image-20250111120536269

patch相关字节2e,修改为想要异或的新值,上述所有DLL按此操作

image-20250111120723874

修改完成后重新加密,替换原DLL文件,网上很多师傅写的文章都是加密后替换jar包中的DLL,其实这样非常不方便(尤其是在二开的时候,我的IDEA运行配置是启动前先编译Artifact,如果不创建Resources目录每次重新替换工作量太大了…)

重新编译,需要先在文件中加入resource资源目录,为保证生成的目录和原一致,需要在sleeve目录下再建一层sleeve,否则编译成功后所有内容只能在jar包根目录下,会导致无法上线

image-20250111143716036

打包完成后重新测试连接性(很重要),此时对Beacon重新分析时原分析工具已经失效

image-20250111155723752

Beacon内存特征分析与绕过

其实光改这些东西还是有些问题,因为CS在内存中还是暴露的状态,因此有很多查内存的杀软还是能够检测出来,来分析一下具体负责上线的DLL,也就是上面修改过的Beacon.dll,在程序入口点处,调用了sub_1000A63E

image-20250112150154114

跟进sub_1000A63E,该函数的主要结构如下,其实这个函数就是我们之前修改的涉及到异或部分的函数,也就是修改xor key的部分

image-20250112150252219

用IDA反编译成伪代码来具体分析

image-20250112151057558

联系到源码中生成可执行PE文件中,就是将beacon.dll嵌入了PE中

image-20250112151738973

而在解析Beacon的时候,上文也已经写了Settings()函数用于读取配置,结合伪代码中的switch函数,分析一下就不难对应到Settings里的操作,对应的四个patch操作为index,type,length、value

image-20250112153214391

此时再来对应伪代码中的switch语句就能知道对应Settings里的部分,type有三个值:1 short;2 int;3 data。首先分配内存空间,然后根据类型判断写入数据value,然后根据不同类型写入Value值

image-20250112160100722

对内存中的CSBeacon扫描时的一个很好用的工具为BeaconEye,下载地址:https://github.com/CCob/BeaconEye

实际使用时发现即使经过上述修改,仍无法躲过Beaconeye的扫描

image-20250112141056629

来深入分析BeaconEye的代码,主要有以下功能:

  1. YARA 规则
  • 代码中定义了两个 YARA 规则(cobaltStrikeRule64和cobaltStrikeRule32),用于匹配 64 位和 32 位进程中的 Beacon 配置。
  • 这些规则通过 libyaraNET库编译并应用于进程内存的扫描。
  1. 内存扫描
  • ProcessHasConfig方法负责扫描进程的堆内存,查找 Beacon 的配置信息。
  • 使用 ProcessReader 类读取进程内存,并通过 YARA 规则匹配 Beacon 的配置。
  • 如果找到匹配的配置,返回 Configuration对象,其中包含 Beacon 的配置地址和内容。
  1. 密钥和 IV 地址提取
  • GetKeyIVAddress方法通过反汇编技术从内存中提取 Beacon 的加密密钥和初始化向量(IV)。
  • 对于 64 位进程,查找特定的指令模式(如 movupsmovdqu)来定位密钥和 IV 的地址。
  • 对于 32 位进程,查找特定的字节序列(如 0xa5, 0xa5, 0xa5, 0xa5, 0xe8)来定位密钥和 IV 的地址。
  • 如果成功找到密钥和 IV 的地址,返回它们的地址值。
  1. Beacon 监控
  • 如果启用了监控模式(monitor 参数),代码会为每个找到的 Beacon 启动一个监控线程。
  • MonitorThread 方法负责监控 Beacon 的网络流量。
  • 使用 ManualResetEvent来控制监控线程的启动和停止。

分析其中主要部分yara规则,对应的部分与上图搜索到的部分如下:

image-20250112162337953

绕过也不难,此处用了个小trick,思路是只要让这个规则匹配不到就行了,patch一下字节码中对应部分(具体不说了,也就涉及到一个push和xor的操作,只要前面的文章认真看了自己发掘一下就行),修改完成后不命中规则即无法扫描到

image-20250112173815872

CS的东西有点多,第一篇先写到这里吧…之前写了一篇C2Profile的还没发

后面计划对CS的其他部分进行分析,比如内存特征、ArsenalKit、CNA脚本…