BeaconEye绕过

BeaconEye中对cobalt strike内存的检测逻辑如下,检测的是内存中profile的配置信息。

1.png

cs源码分析profile构成方式。在服务端创建一个Listener之后会处理beacon、profile和sleepmask。首先进入到Common.ScListener#export()函数中。

2.png

一直跟到src.Beacon.BeaconPayload#exportBeaconStage()函数中的Settings结构,这个结构体保存了profile处理后的格式。在415行创建了Settings结构体后,读取配置信息并按照一定的格式写入结构体

3.png

跟进addShort(),多次调用了this.patch.addShort()写入keyvalue,写入后的数据先保存在this.patch.out.buf,写入对应的位置如下。addShort()会在写入keyvalue值的中间插入两个字节的12

4.png

第二次调用addShort()函数写入后buf中的结果

5.png

addInt()函数,这一回value的占用大小为4个字节。

6.png

继续往下,添加pivot、killDate、进程注入的配置,接着会填充4096个随机字节在配置的末尾,处理完这些后会对profile配置做一次异或加密。函数中94就是加密密钥。这里已经将默认的修改掉了。

7.png

8.png

将加密前后的profile配置信息导出到文件分析,和BeaconEye的扫描规则并不匹配,BeaconEye中开头有16个字节的0

9.png

在函数最后return的数据是将profile写入到beacon后的字节数据,将其也导出

10.png

逆向分析导出的BeaconDll,在写本文之前已经分析过一遍了,所以后面就用分析后的dll代码进行记录。

DllMain会进入process_profile_config函数中,这个函数是处理profile逻辑函数

11.png

跟进,函数会先创建一块新的内存空间并将句柄保存在profile_mem_addr变量中,之后将空间内容全部重置为6(原来是0,这里已经改成了6),向下进入for循环xor解密profile配置信息,如下图。

12.png

13.png

解密之后完成之后,会保存解密后的内存地址和大小到变量profile_decode_ptr中。然后执行下一个for循环,

在这个for循环中,首先调用readShort函数将结果返回给变量j,接着再执行for循环内的逻辑。函数readShort逻辑如下,首先会做一个if判断,然后调用ntohs函数读取两个字节,对应的正好是服务端代码中的addShort()函数的逻辑,读取完成后当前读取的指针地址+2,未读取内存空间大小-2,返回读取的结果。

14.png

在for循环语句体内,首先判断第一个readShort读取的数据是否为0,是则退出循环,不为0则继续读取两个字节赋值给变量data_type,第三次读取赋值给变量data_size(变量的含义是根据下面代码中switch对数据的使用得出的),之后计算一次地址偏移,第一次的offset根据动态调试发现是8,这也就对应上了BeaconEye为什么前面会有一堆0的检测,只要我将上面的memset重置的内存设置为非0就能绕过BeaconEye的规则了。

15.png

回到IDA,计算完偏移后将变量data_type的值写入到偏移指定的地址,(第一次写入的是数据类型),往下进入switch循环,对data_type的值进行判断如果是1则再次读取两个字节并写入到内存空间,是2则读取4个字节并写入,3则是string类型,sub_10006C81传入了数据大小data_size

16.png

分析到这里就明白了,cobalt strike只会将profile中的数据类型和数据内容写入到内存,并且保存profile处理后的内存空间中前8个数据为0,这时候beaconeye扫描是能扫描到的,绕过方法就是在其memset的时候将其替换为非0

17.png

18.png