夕阳西下-该回家啦
大多数情况我们都关注着beacon的上线,本文讲的是在POST-EX阶段,在不重写beacon或功能模块的情况下,对自带的功能模块实现一些内存IOC规避
CS经过不断的更新迭代,逐步把功能模块的实现从RDI切换成BOF的形式,这样带来了很多OPSEC方面的提升和减少了载荷的大小,但你仍然可以看到还存在少数通过RDI实现的功能模块。
当你使用诸如hashdump之类的功能时,在CS资源中的这个dll就会被Decrypt然后经过一系列Patch,再经过一系列封装(参数、功能描述、功能号等等)传输给Beacon再通过RDI自加载。
在这个过程中你可以使用Mallable Profile 自定义一些Patch的内容,包括pipename
、obfuscate
、smartinject
、amsi_disable
、thread_hint
等等
我们的思路是可以在dll patch 之后,再次使用一些工具来packer dll。达到一些内存特征的规避效果,这就看大家发挥了,简单点你可以使用一些壳来帮助你提高一些规避的效果,但是相应的
opcode
依然还是之前的那些,所以你还可以使用代码虚拟化的方式将重点函数虚拟化或者混淆。
思路和方法本身没什么实际难度,但选择packer的方法、工具和遇到的问题可能并不一样(不同的packer可能会导致不同的问题,也并不一定都能正常被加载,这是这个方法需要解决的重点问题),这里仅记录我使用的这种packer遇到的问题。
在源码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));
直接把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 这个功能,该功能一样可以使用上述思路来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请保证你的大小可以被正常传递。
回到之前的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();
}
在写死profile中的 post-ex 变量后,你可以一直使用这个被patch之后的dll再做packer,所以如果你的profile之后是固定了,那么你可以通过本地资源替换的方法直接加载。其中invokeassembly.x64/32.dll 就是一个典型的case。
现在主流的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之后的,内存里面的绝大部分可读信息或特征已经被混淆了。