长亭百川云 - 文章详情

如何正确的 “手撕”Cobalt Strike

d_infinite

66

2021-12-02

0x00 背景

众所周知,Cobalt Strike是一款在渗透测试活动当中,经常使用的C2(Command And Control/远程控制工具)。而Cobalt Strike的对抗是在攻防当中逃不开的话题,近几年来该领域对抗也愈发白热化。而绝大多数厂商的查杀,也是基于内存进行,然而其检测方式的不当,导致非常容易被Bypass,包括但不限于

  • 扫描RWX内存(正常进程中的堆区域一般没有执行权限)
    • DOS头
  • 扫描特征
    • 字符串特征
      • ReflectiveLoader
      • beacon.x64.dll
      • ...
    • Beacon Config(使用前)
  • ...

上面列出的这些方法,实际都能绕过,核心原因是,去掉这些特征可以不影响Cobalt Strike Beacon的正常运行

0x01 BeaconEye核心原理

BeaconEye的核心原理是通过扫描Cobalt Strike中的内存特征,并进行Beacon Config扫描解析出对应的Beacon信息,项目地址是https://github.com/CCob/BeaconEye

0x02 Beacon.dll

Cobalt Strikeshellcode,实际都是通过反射加载的方式加载Beacon.dll,而Beacon.dll中存在Beacon Config配置信息(主要定义通信目标/通信方式等),在Cobalt Strike中对应的Resourcesleeve/beacon.dll

0x03 Beacon Config Generate

Beacon Config的生成在BeaconPayload类的exportBeaconStage函数中

这上面指向的Settings结构体就是Beacon Config,比如var1,它代表实际通信的端口

最终Cobalt Strike会将Settings转化为bytes数组,然后使用固定的密钥进行Xor,并对剩余空白字段填入随机字符

最后将生成的beacon.dll嵌入到最终的PE文件中

0x04 Beacon Struct

SettingsAdd系列函数,如AddShort,并不是简单的将Short类型直接追加到bytes数组中,而是追加了一个结构体

第一个字段是index,第二个是type(short/int/...),第三个是length,第四个则是关键的value值,因此根据这个结构即可解析在内存或在文件中的Beacon Config

0x05 BeaconEye规则

接下来让我们看一下BeaconEyeyara规则

32位的Beacon Config规则长这个样子,如果你认真阅读了前文一定会觉得很疑惑,因为按照Java当中的结构,它应该分为四个部分

[ ID ] [ DATA TYPE ID ] [ LENGTH OF VALUE ] [ VALUE ]

但是实际的yara规则却没有办法对上java中的Beacon Config结构,说明Beacon.dll在装载的过程中,并没有直接将上述数据memcpy分配到堆中,接下来让我们通过对beacon.dll进行逆向

通过dllmain跟进,发现有一个关键函数,里面首先解密了先前Beacon Config的加密数据,然后遍历Beacon Config。首先是在拿到了Type之后,直接往堆中分配的内存写入WORD长度的Type,然后根据Type进行判断,case 1对应Shortcase 2对应Intcase 3对应Data,所以实际上最终的Beacon Config的结构是

DWORD           DWORD
[ DATA TYPE]   [ VALUE ]

因此最终的yara规则可以解读如下

??代表通配符,实际匹配的就是beacon.dll当中真正的config结构体,到这一步,后面的结构体还原就是顺水推舟了

0x06 Bypass BeaconEye

而近日有安全人员提出在执行Cobalt StrikeShellcode之前,通过调用SymInitialize即可实现Bypass,本着好奇的态度,笔者对原理进行了深入的探究

0x07 SymInitialize作用

根据官方文档的描述,SymInitialize的作用是用来初始化进程符号句柄的

它的传参有三个

  • hProcess: 代表进程句柄
  • UserSearchPath: 符号文件的搜索路径
  • fInvadeProcess: 是否对进程中已加载的每个模块调用SymLoadModule64函数
    仅仅从传参来看,并没有办法明确的判定为什么能Bypass,因此我们使用windbg进行对比抓取

0x08 Windbg调试

接下来我们分别对调用了SymInitialize和没有调用SymInitializeCobalt StrikeBeacon进行windbg调试,由于我们知道BeaconEye扫描的是堆内存,因此我们直接对比两者的堆内存


从上面两张图我们可以很清晰的看到,这两个进程在堆内存中的最大的区别是,使用了SymInitialize的第一个heap区域,比没有使用SymInitialize的第一个heap区域,多了几个Segment,那为什么多了几个Segment就导致BeaconEye无法扫描呢?

0x09 Windows中Heap结构

使用windbg,执行如下命令,查看一下具体Heap的结构

dt !_heap


可以看到heap结构的字段非常的多,这里重点关心3个字段

  • SegmentListEntry: 存储堆段地址的双向链表
  • BaseAddress: 堆段起始地址
  • NumberOfPages: 页面的数量
    那一个堆段的范围是怎么计算出来的呢?非常简单
BaseAddress ~ BaseAddress + NumberOfPages * PageSize

而每一个BaseAddress以及NumberOfPages,都仅仅只针对当前的堆段

0x0A NtQueryVirtualMemory

BeaconEye中查询内存信息实际调用的是NtQueryVirtualMemory,我们都知道Nt系列函数是WindowsRing3进入Ring0的入口,让我们查看该函数的官方文档

可以看到查询的信息都存到了MemoryInformation中,而MemoryInformation对应的结构体是MEMORY_INFORMATION_CLASSMEMORY_INFORMATION_CLASS实际包含了一个 MEMORY_BASIC_INFORMATIONMEMORY_BASIC_INFORMATION结构如下

查看RegionSize的描述

翻译过来的意思是,RegionSize的计算方式是,从起始地址开始,直到内存页的属性不一致为止,包含的byte数量,就是RegionSize

0x0B 猜想与验证

首先我们可以初步得出结论,BeaconEye当中获取堆的信息时,实际只获取了第一个堆段(因为堆段和堆段之间是不连贯的,导致内存页属性不能保持一致),因此假设Beacon Config没有被释放在第一个堆段中,就会导致BeaconEye检测失败,为了实现这个猜想,笔者将SymInitialize注释掉,转而手动调用HeapAlloc进行堆分配(当堆空间分配的足够多时,就会触发系统自动生成堆段),如果这个猜想是正确的,那么BeaconEye将同样无法扫描

编译运行,再使用BeaconEye进行检测,发现已经无法检测了,猜想bingo

0x0C BeaconEye修复(伪)

现在不能检测的原因已经找到了,修复其实非常简单,前面提到过,heap结构中包含了堆段的双向链表,因此我们只需要在BeaconEye当中,遍历这个双向链表,将所有堆段地址都添加到待扫描列表中即可,以下是修复代码

这个时候我们重新编译,扫描原先使用了SymInitializeCobalt Strike Beacon,发现已经可以扫出来了

0x0D 为什么还是被Bypass了?

但是事情远远没有那么简单,因为我发现先前手动调用HeapAllocCobalt Strike Beacon并没有扫出来,这令我百思不得其解,为了解决问题,我的思路是先确定Beacon Config在内存中哪个位置,这里同样使用yara进行确认(扫描完整内存),得到具体的位置后,调试BeaconEye并判断是否读取到了对应的内存。经过一番调试,发现BeaconEye确实存在于堆段中,但是BeaconEye并没有完整的读取到堆段的所有内存,示意图如下

红色部分是BeaconEye实际读取到的内存,绿色部分是实际Beacon Config存放的位置,为什么会出现这种情况,这个时候就得继续回到Windows的内存设计上

0x0E HeapBlock

Windows当的堆内存当中,除了堆段以外,还有一个概念叫堆块,每一个堆段都是由多个堆块组成的,使用vmmap工具即可查看

不难发现每一个堆段包含了大量的堆块,而在实际的进程当中,堆块对应的结构体是_HEAP_ENTRY

而在_HEAP_SEGMENT当中,只有FirstEntryLastValidEntry,这两个字段的含义是指向第一个以及最后一个堆块

而经过阅读相关资料,发现并没有链表将所有堆块串联起来(无论堆块是何种状态),因此堆块的位置需要手动计算,这里存在一个小插曲,就是windows实际是加密了_HEAP_ENTRY这个结构的,加密方式是Xor,而Xor的密钥则在_HEAP结构的0x88(x860x50),因此在计算堆块大小时,需要手动解密Size

0x0F BeaconEye修复(真)

在之前的修复代码上,我们手动计算所有堆块的地址,并添加到待扫描列表当中,代码如下(方便演示这里只写了x64部分)

重新扫描通过手动调用HeapAllocBypass原版BeaconEyeCobalt Strike Beacon,发现已经可以扫描了

0x10 结语

目前这个加强修复版的代码,可以通杀Cobalt Strike全版本(3.xyara规则需要修改),这对攻击方来说提出了更高的挑战以及要求。目前该检测功能已经集成到 牧云Beta版 当中,也欢迎大家来申请试用体验更强大的主机安全产品。

另外,牧云正在招聘主机安全领域的产品安全研究员,如果你和我一样,喜欢研究红蓝对抗,并希望将它落地到产品当中,欢迎投递简历,投递邮箱为jingyuan.chen@chaitin.com

0x11 参考资料

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

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