纯C重构CS Beacon - 原理详解与开发实现

#前言

去年上半年分析过CS 服务端代码,也发过相关的文章 ,之后就一直想根据TeamServer逻辑来重写一个Beacon。

实现重构Beacon 的目的:

  • 去除现有CS Beacon 行为上的、流量上的、内存上的特征,使其对被控端来说就像一个新的C2 一样,没有现有C2的特征,但又能集成CS TeamServer 优秀的协同功能。
  • 更方便做免杀,源码级的免杀处理,能够完全自定义Beacon 行为、流量、按需增强功能。

在开发Beacon 之前,我对Beacon 最基本要求是:稳定、无特征、最低支持Win XP/2003、体积小。在此基础上,我决定采用C 语言编写Beacon,且开发过程中不导入任何第三方外部库,只依赖Win32 API,这样能使Beacon 静态编译仅160kb 左右,并且可在兼容到Win XP/2003。其次,为追求稳定和占用内存小,在编写代码的时候注重内存管理和对失败的逻辑处理。


去年完成了重构 Beacon 的第一版开发,实现了HTTP/HTTPS 协议上线,进行命令执行、文件上传下载、目录浏览、Sleep 抖动等渗透常用的基础功能。后面紧接着实现了内存解析BOF 文件,适配了二十几种常用的CS BOF。后续经过几次实战项目的使用,也证明了重构Beacon 的稳定和动静态与内存免杀效果。在之后根据实战中遇到的各种问题,对重构的Beacon 增加新了的功能。目前在流量方面上已实现的功能有:动态URL、SSL Pinning、域名/IP 防封策略,这些会在后面文章中介绍。

#实现的功能点

  • 实现的命令:

    shell, execute, ls, rm、sleep(抖动), cd, download, upload, pwd, mkdir, cp, exit, drives

  • 下面图片显示的所有功能点均已实现:

  • 内置集成的BOF:

    arp, cat, debug_privilege, env, get_clipboard, ipconfig, listdns, netstat, probe, quser, regsave, routeprint, scquery, screenshot_bof, self_delete, smbinfo, syscalls_dump, syscalls_inject, syscalls_shinject, tasklist, wdtoggle, whoami, wifidump, wifienum, windowlist, wmi_query, uptime

  • charset 命令或图形化设置Beacon控制台输出内容的编码,解决乱码问题。(如果charset参数为空,则恢复到原始编码) 或图形化设置

#CS 请求处理与数据封装

首先介绍CS 的Team Server 是如何解析Beacon 发来的数据以及如何封装数据向Beacon 发送的(基于HTTP)。这也是后续开发Beacon 的基础依据。

#心跳包获取指令

我们通过在CS 客户端下发指令(如执行命令、删除、上传文件等),TeamServer 会保存客户端发来的指令内容。心跳包就是Beacon 按照Sleep 设置的时间,定时向TeamServer 发起HTTP Get 请求,以获取客户端下发的指令,然后根据指令的内容执行对应的代码。

首先来分析一下这个心跳包的组成结构: 如上图所示,Beacon 需要收集以上信息,然后使用公钥加密,根据Profile中的配置进行编码,然后在通过Get请求发送到TeamServer。由于这里需要使用公钥加密,所以在做马的时候就需要TeamServer 下的.cobaltstrike.beacon_keys ,这里面保存了TeamServer 启动后生成的公私钥信息。

其次还需要根据Profile中的配置进行编码,下面是一个Profile Get请求配置的例子: 上图配置下的心跳请求包表现如下图: TeamServer 在beacon.BeaconC2.process_beacon_metadata()方法中使用私钥对加密内容进行解密,获得机器的各种信息,然后在客户端就可以看到机器上线了。

如果客户端输入了指令,例如shell whoami ,那么TeamServer 会使用心跳包里的密钥对此指令内容进行AES 加密,并按照Profile中的编码方式对加密的内容进行编码并返回,如上两张图。

CS 默认的编码方式有:Base64、Mask、Base64Url、NetBIOS、NetBIOSU。后三种是CS 独有的特征,建议不要使用。这四种编码方式可以组合使用。

Beacon 收到请求后,首先进行解码,然后利用密钥进行AES 解密,解密后的结构如下:

[ 指令类型 + A内容长度 + A内容 ]+..+[ 6指令 + 随机数 ] 以执行shell whoami为例:

Beacon 会根据指令类型78,判断操作为执行命令,然后调用代码执行whoami 命令。

#Beacon 回传数据

执行完命令后,Beacon 通过POST 请求将执行结果发送给TeamServer,TeamServer 在传给客户端进行展示。

在发起POST 请求之前,首先会将执行结果进行AES 加密,然后根据Profile 中的配置的编码方式以及数据的放置位置等信息,对POST 请求包进行构造,构造完成后才会进行发送。TeamServer 再按照对应的方式进行解码解密。

如下图,是一份POST 请求的Profile样例: 如下图,是Beacon 回传的POST 请求包的内容: 数据包组合结构为:编码 {AES 加密[(请求次数)+(响应类型)+(执行结果)]}

TeamServer会根据不同响应类型对执行结果进行不同的处理,对应的处理方法为:beacon.BeaconC2.process_beacon_callback_decrypted()

以上即为Beacon 与TeamServer 进行HTTP 双向通信的流程,不仅仅是命令执行,其他功能(文件操作等等)也都是这样的处理逻辑。

#Beacon 开发实现

#Profile 适配与字符串加密

上面分析中发现,Profile中配置了各种编码方式、自定义的请求头、对加密数据的前后字符串拼接等等,在实现Beacon 时需要考虑到这些内容,并且针对不同Profile 都能进行适配。

如下图是部分请求头的处理逻辑。 此外,域名,端口、以及Profile 配置中的各种请求头信息、公钥等敏感信息都是加密保存。加密效果如下图所示: 如果在不同的Profile下手动生成加密字符串,在一个一个替换太麻烦了。这里推荐一个python 库:dissect.cobaltstrike.c2profile ,它可以按照格式读取Profile 中的配置信息,然后写成键值对的形式,这样就可以用python脚本处理这些字符串了,简便了许多。

#动态URL 实现

主要针对可能某些监控场景下会记录对同一个URL 请求的频次或是记录内容、大小始终相同的HTTP/HTTPS包(例如前面说的心跳包)。通过动态URL 能使心跳包每次都变得不同。

实现原理:

Beacon 端生成一个随机字符串,长度自定义,然后拼接到URL 中。并在Header 里指定这个随机字符串的长度,以便TeamServer 能够通过长度,截取该字符串,还原最原始的URL。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//Beacon生成随机字符串
if (IsDynamicUrl) {
		DWORD RandomLen = RandInt(5, 10); //随机字符串5-10位
		CHAR* tmp = (CHAR*)calloc(RandomLen + 1, 1);
		if (tmp == NULL) return NULL;
		RandStr(tmp, RandomLen);
		RandomStr = (CHAR*)calloc(RandomLen + 2, 1);
		if (RandomStr == NULL) return NULL;
		memcpy(RandomStr, "/", 1);
		memcpy(RandomStr + 1, tmp, strlen(tmp));
		free(tmp);
	}

随后,需要修改TeamServer的cloudstrike.NanoHTTPD.run() 方法,获取对应的Header 头参数中的随机字符串长度,然后截取URL 中对应长度。 实现效果如下,每次心跳请求和POST 请求的URL 都不相同,避免心跳的数据报始终不变。

#防封域名/IP 策略与SSL Pinning 实现

实现效果:使防守方封锁域名无效,且通过一定的IP轮询策略减缓封锁IP带来的影响。

原理说明:

这里利用的是CDN的特性,CDN 的转发HTTP/HTTPS 请求的策略是检测Host头里的域名,根据域名转发到CDN 后面的真实服务器。所以可以通过直接请求CDN 的IP,然后在Host里面指定配置的域名,一样可以转发到CDN 后面的C2上线。这样做的好处是,不会发起DNS请求,对抗一部分对域名的封锁。

但是有的流量检测设备,也会检测HTTP 请求包里的Host 字段,根据Host 字段进行恶意域名的匹配。这样HTTP 的请求就会被检测到,所以进一步,需要通过HTTPS 上线,因为数据包都是加密的。

在企业环境下,主机联网前都会安装企业的证书到本机的根证书区,就像我们通过BurpSuite 抓取HTTPS数据包的逻辑一样,这种环境下能够解密HTTPS 数据包。所以某些设备又能看到Host 头里的域名,进行封锁。但是可以通过SSL Pinning 来解决这个问题,通过固定CDN 的证书公钥指纹,以此判断是否有人尝试解密HTTPS 数据包,如果有就不往外发正常包,这样就避免被解密。

CDN 的IP都是很多的,就腾讯云的来说,通过多地ping以及不同的DNS 服务器查询,获取到的有效CDN IP 就有60来个。

以上的利用需要一定的策略来实现,如CDN IP池的轮询策略,需要根据不同的侧重点考虑情况。


IP的轮询策略:

首先,维护一个被Ban的IP池,将请求三次都失败的IP(被封锁)移到被Ban的IP池中,并将其字符串置为"",这个函数就从CDN_IP 里读取那些不为""的IP,如果所有的IP都进了被ban的IP池中,就重置CDN_IP,再一次进行轮询。 站在手动分析者视角(从流量分析角度,尽量避免暴露我们在使用很多个IP)

  1. 默认情况下,始终请求一个IP,只有这个IP被Ban了,才会使用下一个。
  2. 分析者Ban了第一个IP,并在观察木马还有没有请求其他IP,然后继续Ban
    1. 如果之前没有一次请求成功,且轮询了1个IP(失败5次)也没有成功,那就睡眠加2分钟(考虑到第一次上线时某些ip已经被Ban的情况,策略不能太严格)
    2. 如果之前有一次请求成功了,且轮询了1个IP也没有成功,那就睡眠加20分钟(考虑到可能当前CDN IP不可用或是目标断网的情况,而不仅是被ban的情况。 但一般情况下,请求成功过,又开始轮询ip一般都是被Ban或断网的情况,所以时间可以大一些,因为一般情况下是不会进行轮询的)

SSL Pinning 实现代码: 这样就限制防守人员的反制策略为以下两点:

  1. 机器关机或断网
  2. 防守人员动态调试上线马,从内存中dump出IP池里的IP,然后通过防火墙等设备对这些IP进行封堵。(耗费时间,也可能对正常流量有影响)

Tips: 通过CDN IP,并指定Host 方式上线。在腾讯云CDN下,要注意一些Header头的大小写问题。腾讯云会将某些Header头转换为小写。

end.

加载评论