BeaconEye绕过
BeaconEye中对cobalt strike内存的检测逻辑如下,检测的是内存中profile的配置信息。
cs源码分析profile构成方式。在服务端创建一个Listener
之后会处理beacon、profile和sleepmask。首先进入到Common.ScListener#export()
函数中。
一直跟到src.Beacon.BeaconPayload#exportBeaconStage()
函数中的Settings
结构,这个结构体保存了profile处理后的格式。在415行创建了Settings
结构体后,读取配置信息并按照一定的格式写入结构体
跟进addShort()
,多次调用了this.patch.addShort()
写入key
和value
,写入后的数据先保存在this.patch.out.buf
,写入对应的位置如下。addShort()
会在写入key
和value
值的中间插入两个字节的1
、2
第二次调用addShort()
函数写入后buf
中的结果
addInt()
函数,这一回value
的占用大小为4个字节。
继续往下,添加pivot、killDate、进程注入的配置,接着会填充4096个随机字节在配置的末尾,处理完这些后会对profile
配置做一次异或加密。函数中94
就是加密密钥。这里已经将默认的修改掉了。
将加密前后的profile配置信息导出到文件分析,和BeaconEye的扫描规则并不匹配,BeaconEye中开头有16个字节的0
在函数最后return的数据是将profile写入到beacon后的字节数据,将其也导出
逆向分析导出的BeaconDll,在写本文之前已经分析过一遍了,所以后面就用分析后的dll代码进行记录。
DllMain会进入process_profile_config
函数中,这个函数是处理profile逻辑函数
跟进,函数会先创建一块新的内存空间并将句柄保存在profile_mem_addr
变量中,之后将空间内容全部重置为6(原来是0,这里已经改成了6),向下进入for循环xor解密profile配置信息,如下图。
解密之后完成之后,会保存解密后的内存地址和大小到变量profile_decode_ptr
中。然后执行下一个for循环,
在这个for循环中,首先调用readShort
函数将结果返回给变量j
,接着再执行for循环内的逻辑。函数readShort
逻辑如下,首先会做一个if判断,然后调用ntohs
函数读取两个字节,对应的正好是服务端代码中的addShort()
函数的逻辑,读取完成后当前读取的指针地址+2,未读取内存空间大小-2,返回读取的结果。
在for循环语句体内,首先判断第一个readShort
读取的数据是否为0,是则退出循环,不为0则继续读取两个字节赋值给变量data_type
,第三次读取赋值给变量data_size
(变量的含义是根据下面代码中switch对数据的使用得出的),之后计算一次地址偏移,第一次的offset
根据动态调试发现是8,这也就对应上了BeaconEye为什么前面会有一堆0的检测,只要我将上面的memset
重置的内存设置为非0就能绕过BeaconEye的规则了。
回到IDA,计算完偏移后将变量data_type
的值写入到偏移指定的地址,(第一次写入的是数据类型),往下进入switch循环,对data_type
的值进行判断如果是1则再次读取两个字节并写入到内存空间,是2则读取4个字节并写入,3则是string类型,sub_10006C81
传入了数据大小data_size
。
分析到这里就明白了,cobalt strike只会将profile中的数据类型和数据内容写入到内存,并且保存profile处理后的内存空间中前8个数据为0,这时候beaconeye扫描是能扫描到的,绕过方法就是在其memset
的时候将其替换为非0