0-从零开始手搓C2框架
写完《从 SRDI 原理剖析到 PE2Shellcode 的实现》后,我一直在琢磨:能不能搭一套真正属于自己的C2框架?思路很简单——先把轮子造出来再说。Havoc、Sliver、Merlin、AdaptixC2和Iom等等优秀的开源项目,因为它们已经经过实战检验,值得我们学习它们的设计思路。我打算先全盘“临摹”,把它们的架构与设计吃透,再慢慢内化成自己的肌肉记忆,然后后期再慢慢消化为自己的开发经验。
至于软件工程那一套,先放一边——我既不是科班出身,也没那么多时间画UML,流程图等等:)所以就直接上手撸代码。但这也导致了一个问题,因为大多数C2服务器都是用go,而我的后端golang基础知识薄弱,以至于服务器的代码写的异常艰难,所以就在下班的时候抽出时间恶补一个月的go-gin的基础知识。
服务器代码主要借鉴 AdaptixC2,请多多支持一下这个项目,之后我也会借鉴其他项目的设计思路继续完善!
至于implant语言的选择,我起初是想选C语言的,但考虑到我是第一次开发C2框架最重要的是快速熟悉整个开发流程和实现出一个demo出来,所以我选择了go语言。想必各位师傅有自己擅长的语言,看自己需要来选择吧。后面重构implant我会使用c/c++来实现更高级的防御规避技术以满足高强度的安全对抗场景,而且用c/c++实现的implant是真的小,另外rust实现的也挺小的,但我不会,哈哈哈。
最后是GUI的选择了,为了提升用户的体验感和减轻上手难度,相当一部分的C2会提供GUI版本的客户端,就比如说大名鼎鼎的CobaltStrike。GUI还细分出浏览器和桌面级应用,而且我有跨平台的需要,经过我的深思熟虑,最终选择了QT桌面级应用,理由主要是:桌面应用可以方便的与操作系统交互,而QT的跨平台正是我需要的,当然我也考虑过webview类型的桌面应用,上手写了代码之后感觉写了太难受了所以否掉了(Electron真是害我不浅),不过后面还是要学wails。
我就边抄边写边调式,也算是做出了一个功能齐全的C2框架,但是代码写的太乱了,还需要重构。当然我也会开源,但不是现在。我的想法是先开源部分,等我哪天重回网安了再全部开源,我也不知道自己能不能回来。
好了,废话少数,我就以一个初次开发c2框架的小白提出以下在实现c2过程中的一直在脑中回荡的一些疑问:
服务器怎么启动?怎么实现RPC以供客户端调用?
怎么配置监听器然后让它启动的呢?
如何选择监听器生成相应的implant?
implant是怎么完成上线服务器并完成注册的?
怎么给implant下发任务或者说命令?
implant又是怎么执行任务的?
任务执行的结果怎么回传给服务器的?
C2是一个复杂的软件工程级的项目,涉及方法面面的知识,由 Listener、TeamServer、Implant 与 GUI Client 四大核心组件协同构成。本文作为一篇介绍性质的文章,我会将原先实现C2框架的代码量浓缩简化为5%左右,如果你能通过代码解决上述的问题,那么恭喜你完成了C2框架的核心。
什么数据库,GUI客户端,jwt鉴权等实用技术就在这里展开了,我只是在这篇文章中粗略的告诉各位师傅如何完成C2框架的搭建,距离实际的攻防实战还远远不过,还需要加上“亿”点点细节,比如说实现大文件上传与下载、监听器的完整生命周期(监听器的创建、修改、暂停和删除)、Beacon的完整生命周期(Beacon的创建、上线和删除)、浏览器(文件浏览器和进程浏览器)和隧道/代理等,只有实现这些功能,才能真正的作为红队的基础设施。
本文虽然是从零开始,但我认为对新手小白极其不友好,默认各位师傅有较高的开发水平,虽然我的开发水平也不高(+-+)。本文与传统的文章不一样,是一步一步地构建出C2,而不是直接给出完整的项目,只要能复现出来就算入门c2框架搭建了。
也有看到一些文章说可以搭建一个几百行的c2服务器,只用bof实现大多数后渗透功能,这也是可行的,但这是开发能力有限情况下的妥协产物,并非主流。
一、路由注册和服务器启动
1.1 Go的面向对象与依赖注入
现在,你随便去github翻找C2项目,你就会发现越来越多的开源C2服务器把后端换成Go+Gin或grpc,并不是为了高并发——几条Shell会话根本吃不满CPU——而是被写完直接之后使用交叉编译出一个单文件丢进不同操作系统都能运行的快感征服。
Go的语法简单、性能像C,标准库自带HTTP/JSON/TLS,Gin再把路由、中间件、参数绑定封装成十几行代码,开发者就能把全部精力放在协议细节而不是环境折腾上。只有写过的的人,才知道,这种顺手带来的生产力。本文不会过多介绍Gin的使用方法,请大家自行查阅网上资料。
先在这里介绍一些服务器的项目结构:
controller:服务器启动、路由注册,一般地,路由处理函数接收来自客户端的数据,完成数据检验、参数传递、调用相应的业务方法以及返回响应给客户端。server:New一个服务器和真正的业务的实现。handler:深度处理listener、Beacon和extension的相关业务middlewares:中间件的实现,比如说jwt鉴权(未实现)、日志、错误恢复等utils:与业务无关的小工具profile:一些全局的配置
在实际的开发过程中,我用到了面向对象和接口与组合,区别于传统的以过程式为主的开发思路,这是因为C2服务器中涉及到大量的自定义结构体,比如说listener、beacon、profile,taskqueue、proxy等等结构体。
为了避免使用全局变量在高并发的条件上数据不一致性、竞争性和代码耦合等问题,有必要用一个东西将这些结构体“装起来”。
另一方面,在handler中有大量的场景需要用到依赖注入,比如说需要用到teamserver实例中的方法去操作其属性(成员)。在go中依赖注入的实现方式有很多种,本项目中使用接口来声明需要用到的teamserver方法。还有另一个原因促使我使用依赖注入的是 import cycle not allowed 问题。
举一个简单的例子,当我们通过前端向某一个beacon下发命令执行的任务时,我的调用链是这样的 controller.BeaconCommandExecute -> TeamServer.BeaconCommand -> handler.BeaconCommand -> BeaconHandler.BeaconCommand -> TeamServer.TaskCreate。
可以看到在BeaconHandler的 BeaconCommand 方法里用到TeamServer的 TaskCreate 方法,这就涉及到一个依赖的问题。常规的解决方法(静态注入)是
传递TeamServer的指针:零抽象,一眼能看懂,强耦合。
BeaconHandler里定义TeamServer成员:强耦合
BeaconHandler里定义TeamServer接口:BeaconHandler彻底不知道“对面是谁”,只要相关的实例TeamServer实现TaskCreate方法即可,面向接口,解耦最干净。
比如说在 handler/beacon/types.go 这样定义
BeaconHandler对象中需要用到TeamServer方法,为了避免在定义BeaconHandler对象时添加TeamServer对象,从而暴露TeamServer所有信息,需要用到接口来拿到需要用到的TeamServer的方法(比如说TaskCreate)。
可能有点难懂,我的建议是多看代码去感受。
下文正式进入代码编写!
1.2 服务器配置
创建一个目录,然后输入命令,推荐使用golang ide,智能提示,自动引入包等功能不是vscode能比的。
在Go项目中引入profile.json作为配置文件,能有效实现配置与代码分离,避免硬编码带来的维护成本。
为了在程序运行期间使用到配置信息需要在 profile/types.go 定义相应的结构体,通过go的结构体标签能够在json反向序列时将数据绑定到相应的成员,这也是Go官方的推荐操作。
profile.json
profile/types.go 相应的配置结构体定义
profile/profile.go 需要一个定义一个NewProfile方法返回一个Profile对象,Profile提供一些配置检验的方法,程序启动阶段就能把所有非法配置一次性拦下来,避免运行中再因配置不当panic/Fatal,可靠性显著提高。
Validate()是统一入口,依次调用三个子校验函数,如果遇到一个err直接返回并阻止服务器启动:
validateTeamServer():校验C2服务器核心配置
validateServerResponse():校验伪装响应页面配置
validateZap():校验日志系统(Zap是 Uber的日志库)
profile/profile.go
为了保护客户端和服务器之间的通信安全,大多数C2框架都会采用*.crt和*.key启用流量加密,当然 “crt + key” 只是起点,在高强度的网络对抗中还使用双向TLS(mTLS)、证书校验、防篡改等安全机制。
生成自签名 RSA-2048 服务器证书(server.crt)和对应的私钥(server.key),有效期10 年,并且不带密码保护私钥。
生成*.crt和*.key后需要将这两个文件放置到服务器的static目录。
1.3 路由注册
controller/types.go。TeamServer接口并未定义需要用到的方法,后续会增加。
controller是C2服务端的控制器,负责挂载日志、恢复、伪装(404)等中间件,并给相应的路由绑定路由处理函数,所有client的请求都先通过它再由它分发到具体的业务逻辑,这是传统“MVC”框架中的“Controller”部分,也充当了部分服务器的角色。
controller/controller.go
utils/crypt/hash.go 生成SHA256哈希值
为了下文的1.6的接口测试,需要定义测试的接口以验证服务器是否能正常提供服务。
controller/test.go
1.4 中间件与日志器
gin框架允许我们自定义中间件,以“洋葱模型”灵活地插入任何横切逻辑,而无需改动业务代码本身。当接收到client的路由请求时,Gin会按照使用中间件的顺序依次调用,最后再处理业务逻辑。
controller用到了GinLogger、GinRecovery、404中间件,为了实现方便我并未添加jwt鉴权中间件,有需要的师傅自行添加,或者等我后续开源完整的C2框架。
这是我从别的项目找的生产级Gin中间件,用Zap做日志,功能上完全替代了 gin.Logger() 和 gin.Recovery(),而且更强大、更灵活。
GinLogger:是一个轻量级、结构化、可观测的HTTP请求日志中间件,用Zap输出到控制台(可通profile.json关闭)和日志中,包含 状态码、耗时、IP、UA 等关键信息,方便你快速定位问题、做性能分析。GinRecovery:任何panic 都会被捕获,且不宕机,能打印堆栈。
middlewares/logger.go如果看不懂只管用就完事了
GinLogger 和 GinRecovery 两个中间件都用到Zap日志,Zap 是 Uber 开源的高性能、结构化日志库,专为Go设计,日志字段以 JSON 形式输出。不用搞懂,直接用就完事了。
logs/zapLogger.go
最后一个中间件,用于如果访问到了不存在的接口时直接返回404页面
404.html
middlewares/404.go访问不存在的路由时返回自定义的404hmtl页面
1.5 服务器启动
启动流程:
通过NewTeamServer返回一个TeamServer对象
TeamServer.Start:创建一个goroutine启动服务器,传递
stopped通道的地址,以便StartServer方法可以在接收到停止信号时优雅地关闭服务。Controller.StartServer:用于启动 HTTPS服务并处理服务。
server/types.go:定义一个TeamServer结构体,负责管理配置、启动HTTP服务、管理监听器和Beacon,以及协调业务逻辑
utils/safeType/map.go 加上“锁”的map类型,避免资源竞争而导致的安全问题
handler/types.go 后续再补充需要用到的接口方法
server/teamserver.go:包含NewTeamServer、SetProfile、Start方法
handler/handler.go:定义了一个 NewHandler 函数初始化了一个 Handler 实例,并注册了 Beacon-HTTP 监听器处理器和 Beacon 处理器,为后续的业务逻辑处理做好了准备。
handler/listener/http_type.go。后续再补充需要用到的接口方法
handler/listener/http_main.go。
handler/beacon/types.go。后续再补充需要用到的接口方法
handler/beacon/beacon_main.go 实现NewBeaconHandler方法
controller/controller.go 需要补充一个StartServer方法
main.go 程序入口点
运行服务器

1.6 一个简单的接口测试
这是一个简单的C2框架,为了省事,我并未实现GUI客户端以完成接口测试,而是用API测试工具Postman来完成功能验证,这也是现代前后端分别开发的基本思路。测试的工具还有Apifox和Reqable,你还可以用bp、yakit、hackbar,甚至写个python脚本进行测试,怎么舒服怎么来。

二、监听器配置和监听器启动
2.1 大致流程
大致流程:
controller.InitRouter:注册一条路由"/listener/create",并由controller.ListenerStart处理controller.ListenerStart:负责接收启动监听器的请求并做校验、日志记录、业务调用和返回响应TeamServer.ListenerStart:先判断是否有同名监听器如果有则返回错误,因为我们以监听器的名称作为map的“键”,再委托底层handler实现启动,最后把元数据缓存起来。Handler.ListenerStart:根据configType找到对应的ListenerHandler处理者,先做参数校验,再真正启动监听器。ListenerHTTP.ListenerStart:HandlerListenerDataAndStart真正创建并启动监听器,最后把运行中的实例放进全局列表中以备后续使用,最后返回listenerData。这个结果是用来将创建的listener数据同步到所有client中的,这个功能我并未在教学项目中实现。ListenerHTTP.HandlerListenerDataAndStart:拿配置,填默认值,生成随机密钥,然后创建一个http服务器,最后返回listenerdata数据HTTP.Start:根据SSL选项启动HTTP服务器还是HTTPS服务器
监听器的配置通常采用json描述监听器参数(类型、监听地址、端口、TLS 证书等),下面就是本项目采用的json监听器配置的例子
2.2 代码编写
controller/controller.go 增加一条"/listener/create"路由
controller/types.go controller需要用到TeamServer的ListenerStart方法,所以在接口处定义
utils/request/listener.go 前端传过来json的数据绑定到ListenerConfig结构体中
controller/listener.go:参数绑定与业务调用
utils/response/listener.go。本项目中没有实际的作用,一般通过sync包同步到所有client
server/listener.go:检查防重名,再调用策略层真正启动,成功后把完整元数据写入内存缓存,对外呈现“一键创建监听器”的简洁门面。
handler/types.go 补充ListenerHandler接口的方法
handler/listener.go 按协议类型取出对应handler,先校验再启动,最后原样返回结果
handler/listener/http_type.go 暂时未用到TeamServer的方法,后续定义
handler/listener/http_main.go
handler/listener/http_handler.go实现HandlerListenerDataAndStart方法。根据传入的配置(conf)初始化一个 HTTP 监听器(HTTP 结构体),启动它,并返回监听器的元数据(ListenerData)和实例(* HTTP)。
handler/listener/http_listener.go 暂时未实现processRequest路由处理方法,我的想法是在 四、Beacon上线服务器并完成注册 实现
middlewares/ResponseHeader.go:把自定义响应头一次性注入到所有后续处理函数
2.3 接口测试
ok,这一部分的代码就编写到这里,我们重新启动服务器,测试接口是正常
或者使用IDE运行

三、生成Beacon
3.1 大致流程
大致流程:
controller.InitRouter:注册一条路由"/agent/generate",并由controller.BeaconGenerate处理。controller.BeaconGenerate:绑定参数,查询监听器配置,调用BeaconGenerate生成二进制,把文件名和内容用 Base64 打包回前端。TeamServer.ListenerGetConfig:先判断监听器是否存在,再从map中取出对应的ListenerData并返回其Data字段(即ConfigDetail)。TeamServer.BeaconGenerate:Handler.BeaconGenerate:根据beaconConfig.BeaconType找到对应的beaconHandler,然后把参数透传给它的BeaconGenerate方法并返回结果。BeaconHandler.BeaconGenerate:首先调用BeaconGenerateProfile获得beacon的配置,再调用BeaconBuild生成beacon可执行文件。BeaconHandler.BeaconGenerateProfile:生成beacon的配置信息,改信息是由两个结构体组合而成。BeaconHandler.BeaconBuild:将配置信息patch到用于测试模板文件,并将patch后的文件输出。本项目写到后面其实是硬编码配置到beacon里,这是为了方便调试,因为精力实在有限,所以没测试patch完整的beacon,感兴趣的师傅可以去尝试一下。
据我所知生成Beacon的方式一共有两大类型:
通过编译器编译生成:通过编译器将源代码编译成可执行文件。这种方式的优点是可以生成完全自定义的Beacon,支持多种平台和架构,缺点是需要完整的编译环境和源代码。常见的做法是在编译的时候将配置放到C/C++的宏中。
Patch生成:在模板可执行文件的基础上,通过修改配置文件或直接修改二进制文件来生成Beacon。这种方式的优点是不需要完整的编译环境,生成速度快,缺点是灵活性较低,通常只能修改配置信息,不能修改逻辑。
灵活性
高(可以完全自定义逻辑)
低(只能修改配置信息)
生成速度
慢(需要编译)
快(只需修改配置)
依赖
需要编译环境和源代码
不需要编译环境,只需模板文件
适用场景
开发阶段、需要高度自定义
生产阶段、快速生成
在本项目中,我选择的是Patch生成Beacon,还有我看很少有C2通过patch生成,我的这个patch方式只适用于go写的beacon(理论上只要支持json反序列化的语言都可行),更通用的方式就是通过将配置信息用自己的打包器打包,与下文打包任务包类似,就不再这里介绍了。
beacon的json配置
3.2 代码编写
controller/controller.go 增加一条"/beacon/generate"路由
controller/types.go controller需要用到TeamServer的ListenerGetConfig和BeaconGenerate方法,所以在接口处定义
utils/request/beacon.go:前端传过来json的数据绑定到BeaconConfig结构体中
utils/response/beacon.go:响应给前端的结构体BeaconFile
controller/beacon.go 绑定参数,查询监听器配置,调用 BeaconGenerate 生成二进制,把文件名和内容用 Base64 打包回前端。
server/listener.go 实现TeamServer接口方法ListenerGetConfig
server/beacon.go 实现TeamServer接口方法BeaconGenerate
handler/types.go 定义BeaconHandler接口的方法
handler/beacon.go 找到相应的handler,并返回结果
handler/beacon/beacon_main.go 实现BeaconHandler接口的方法BeaconGenerate
handler/beacon/beacon_handler.go
BeaconGenerateProfile:把GenerateConfig和ConfigDetail两个结构体的字段抄进BeaconGenerateConfig结构体,然后json.Marshal成紧凑JSON序列化数据返回。BeaconBuild:根据协议/架构/格式拼出目录和文件名,然后读取读模板二进制,找到CONFIG_MARKER_2024的起始索引,从起始索引开始原地覆盖成4字节小端profile长度,这也是为了消除CONFIG_MARKER_2024字符串特征。把JSON数据紧接写在长度字段后面,覆盖旧内容。会输出在static/product目录,这只是为了教学方便!
模板文件.go 的loadConfig方法会在模板文件中的全局placeholder字节切片中读取前4个字节作为接下来要反序列化json数据长度,而后面则是json数据。注意长度是小端序,当然也可以大端,与patch操作保持一致即可
为什么要增加4个字节的json数据长度呢?理由:在反序列化阶段只要碰到裸0x00字节,就会直接返回错误,4字节长度字段是为了能够正常地解析json数据,而不是到了末尾遇到00字节直接报错。
patch前后placeholder字节切片如下图

我承认这种patch方式很简陋,会浪费很多空间,会有意想不到的错误,而且 BeaconGenerateConfig 有多余的字段,还是那句话:“这很方便”
模板文件.go
生成模板文件 go build <模板文件名>.go 并将其放置到服务器的 static/http/x64/stage64.exe
utils/crypt/crypt.go:按道理来说我们需要将内嵌在json数据用RC4加密,然后beacon先进行一次简单的解密获取内嵌的RC4密钥,然后再用解密后的密钥解密所有的配置信息,避免被静态检测出特征。
3.3 接口测试
重启服务器
先启动监听器,然后在生成beacon


可以看到postman确实接收到服务器传来的base64编码后的beacon文件名和文件内容,正常情况下需要client自己解码并输出到 *.exe、*.dll、*elf、*.bin 文件中。本项目为了实现方便,将beacon输出到服务器的 static/product 目录中
用文件比较工具对比一下patch前后的文件,左边是模板文件,右边是patch后的文件。可以很明显的看到前4个字节是json序列化的字节长度,后面则是未加密的json序列化数据。

静态分析过后,我们运行生成后的产物,我在模板文件中是有两段输出的,第一段是直接将json序列化后的字节数据转换为string类型输出;第二段是json反序列化后输出(证明能反序列化)

四、Beacon上线服务器并完成注册
4.1 解密配置-Beacon端
配置中包含了beacon运行的必要信息,比如sessionkey、callback_address、uri、sleep,是否开启ssl等关键信息。为了调试方便,可以约定硬编码上述字段的值。
正常来说是要先解密存储在placeholder字节切片中的数据再进行反序列化还原配置,为了方便,我们直接反序列化吧,方法是之前说过的 LoadConfig,还有为了调式方便,需要硬编码配置信息,具体看代码!
profile/profile.go 包含BeaconGenerateConfig结构体和LoadConfig方法
4.2 收集系统信息生成心跳包-Beacon端
在典型C2框架的实现中,Beacon在解密并解析其运行时配置后,会立即执行一次全面的主机枚举,收集系统的上的一些信息提供给操作人员使用,收集的信息通常包含beaconid、进程名、进程id、线程id、系统架构、主机名、当前用户名、ACP等等基础信息。
值得说明的是全面的信息收集只会执行一次,上述信息经序列化后,被封装为初始心跳包(heartbeat)的数据,此后,Beacon进入周期性心跳阶段。心跳并非重新拉取完整载荷,而是以固定格式的轻量请求维持长轮询通道,用于:
保活(keep-alive);
轮询C2服务器是否存在待执行任务(task queue)。
心跳包通常用监听器中生成的密钥进行加密,其会话密钥由监听器(listener)在生成阶段随机派生并嵌入 Beacon 配置,加密完成后再进行base64编码然后放置到请求头中,其键名可由操作员自定义,默认为“X-Session-ID”,然后还有前缀“SESSIONID=”,能够有效的迷惑防御者。
在构造心跳包时有必要说一下TLV协议,TLV(Tag-Length-Value)是一种通用的二进制数据编码格式,听名字很高大上的样子,其实说白了就是自定义通信结构体,其中T是Tag,表示数据的类型或业务含义;L是Length,表示可变数据的长度(比如说byteArray,string),V是Value,可为原始数据(如字符串、整数)。
在本项目中,心跳包、任务包和结果包属于TLV协议的范畴,但是又与它的定义有些区别,主要是
Tag字段用不到:因为C2框架主要是在一个小圈子中使用,server和client,server和beacon,约定好什么功能,哪个时间接收到相应的包,这样也就不需要标识这个包的类型了
[]byte类型需要自己打包长度,因为有些数据会进行二次打包,比如说下文五、任务创建时task任务数据会被先打包一次,然后这个打包后的数据还会和长度、taskid、commandid再打包一次形成真正的任务包,为了避免字段冗余,需要自己确定那个[]byte要打包长度字段,不知道各位能否get到我的意思呢?Length只有byteArray和string类型用到:server和client,server和beacon字段事先约定好打包顺序和解包顺序,对于不变字长的数据,直接解读(如byte、int16、int32),对于可变字长的数据,需要用长度来表示接下来要解读的数据长度。
BeaconID
uint32 / 4 B
随机生成的Beacon会话ID(网络字节序/小端均可,需与C2约定)。
BeaconName
string
变长字段;先写 4 B Length,再些数据
Sleep
int32 / 4 B
心跳间隔秒数(>0)。
Jitter
int32 / 4 B
抖动百分比(0-100),目前未使用,可填0。
KillDate
int32 / 4 B
到期自毁;未使用时填0。
WorkingTime
int32 / 4 B
每日可工作时段(分钟数);未使用时填0。
ACP
int16 / 2 B
ANSI 代码页(如936)。
OemCP
int16 / 2 B
OEM 代码页(如936)。
GmtOffset
int8 / 1 B
本机相对 UTC 的分钟偏移
Pid
int16 / 2 B
当前进程PID。
Tid
int16 / 2 B
执行线程TID。
BuildNumber
int32 / 4 B
Windows Build Number(如19045)。
MajorVer
int8 / 1 B
主版本号(如10 →10)。
MinorVer
int8 / 1 B
次版本号(如0)。
InternalIP
uint32 / 4 B
本地 IPv4 转uint32(小端)。
Flag
int8 / 1 B
位标志,未使用
SessionKey
[]byte
变长字段;对称会话密钥;先写 4 B Length,再写密钥字节。
Domain
[]byte
变长字段;域名字符串;先写 4 B Length,再写字节串。
Computer
[]byte
变长字段;主机名字符串;同上。
Username
[]byte
变长字段;当前用户名字符串;同上。
Process
[]byte
变长字段;进程名字符串;同上。
⚠注意:一共有两把密钥:
第一把密钥在配置里,server用来加密配置和解密心跳包的数据,beacon用来解密配置和加密心跳包的数据
第二把密钥在心跳包里,server用来加密任务包和解密结果包,beacon用来解密任务包和加密结果包
sysinfo/sysinfo.go:收集系统信息的工具函数
收集完系统信息后就要打包成心跳包,这里就有必要说一下打包器了,还有除了byteArray和String类型的数据部分之外,其余数据用大端序打包,什么是大端就不用我多少说了!
因为我们的Beacon是用Go语言写,所以就用一下它的一些特性,比如说类型断言,可以动态的获取数据类型。
首先我们需要将所有需要打包的数据放入到
[]interface{}切片中,这个数组可以装入任意类型的数据,interface{}可以换成any,效果是一样的,“协议长什么样”完全交给了顺序和值本身⚠特别注意:
[]byte类型需要自己打包长度,因为有些数据需要进行二次打包,如果在PackArray里打包长度会多次一个长度字段,且这个字段是多余的,不知道各位能否get到我的意思,一个实际的例子在下文的5.1 任务创建中会有体现从切片中取出数据,然后用go的类型断言,获取数据类型,根据类型选择相应的打包方式
比如说我要打包如下的数据
打包后的数据如下

更通用的方式:就是自己根据值选择打包方式,比如说addByte、addShort、addInt、addString,addByteArray等,这也是大多数C2使用的方法。比如说havoc就是用这种方式打包:Havoc/teamserver/pkg/common/packer/packer.go at main · HavocFramework/Havoc

utils/packet/packer.go:打包器的代码封装在这里了
sysinfo/heartbeat.go:包含心跳包HeartBeat结构体、InitHeartBeat和PackHeartBeat方法
utils/common/http.go 接下来就是把心跳数据RC4加密(第一把密钥)→Base64URL编码→塞进自定义HTTP头→发GET请求→把返回体用 sessionKey 密钥(第二把密钥)RC4 解密
utils/common/crypt.go:目前只有RC4加密/解密
main.go ok终于到main函数了,先不着急运行,等到 4.4 测试 的时候启动!
4.3 注册-server端
还记得我们在前面留下的TODO吗?我们processRequest和processResponse还没实现呢,在这里要实现processRequest,然后补充validate和parseBeat方法,这两个方法processRequest和processResponse都会用到,且听我解释
processRequest:是用来处理Beacon通过GET方式来拉取任务的请求,首先调用validate请求鉴权,然后调用parseBeat获取心跳数据为注册Beacon做准备,根据BeaconId去查找是Beacon是否存在,如果不存在则注册。具体细节看代码吧。validate:轻量的请求鉴权,防止非预期的访问者(如蓝队、爬虫、扫描器)误打误撞访问到我们的C2 ListenerparseBeat:从自定义请求头中获取BeaconId和剩余心跳包数据
当然为了防止数据被修改,还可以加上HMAC验证数据的完整性,这里不过多介绍。
handler/listener/http_listener.go:补充processRequest(不完整的)、validate、parseBeat方法
handler/listener/http_type.go:在TeamServer处定义BeaconIsExists和BeaconCreate方法
server/beacon.go:实现BeaconIsExists和BeaconCreate方法
handler/beacon.go 补充BeaconCreate方法,找到相应的beaconHandler
handler/types.go:定义BeaconHandler接口的BeaconCreate方法
utils/response/beacon.go:补充BeaconData结构体,这个结构体本来是要json序列化发送给前端的,教学并未实现。
handler/beacon/beacon_main.go:实现BeaconCreate
既然都说到了打包了,那肯定是有相应的解包操作,解包器的方法如下
ParseInt8
1字节 → uint8
缓冲向前滑1B
返回 0
ParseInt16
2字节 → uint16
缓冲向前滑2B
返回 0
ParseInt32
4字节 → uint
缓冲向前滑4B
返回 0
ParseInt64
8字节 → uint64
缓冲向前滑8B
返回 0
ParseBytes
先 ParseInt32 取长度N,再读N字节
缓冲向前滑4B+N
空切片
ParseString
同上,但去掉末尾0x00
缓冲向前滑4B+N
空串
ParseString64
用8字节长度字段
缓冲向前滑8B+N
空串
解析器除了解包相关的方法之外,还有一个Check方法,在不真正消费(不移动读指针)的情况下,验证后续字节流能否按给定类型序列完整解析一遍,比如说给定检验序列:{"int8","int64", "array"},做预处理:
handler/beacon/beacon_packet.go:包含打包和解包器的相关方法
handler/beacon/beacon_handler.go:CreateBeacon方法,具体创建(注册)Beacon的相关代码
server/types.go:补充Beacon结构体,包含Beacon的具体数据,是否存活,任务队列
utils/safeType/slice.go 加上“锁”的slice类型,避免资源竞争而导致的安全问题
handler/beacon/types.go:补充代码页和一些常量
handler/beacon/beacon_utils.go 包含三个工具函数分别解决了编码转换、系统识别、IP 转换
4.4 测试
①以调式的方式启动服务器,请在需要的地方下断点,比如说 handler/listener/http_listener.go 的processRequest!

②postman发送监听器创建请求,不需要生成Beacon操作,为了调式分析方便将配置硬编码到Beacon里了

③Beacon启动

④我们忽略一些过程,直接看TeamServer是否有Beacon的数据

⑤如果创建成功,服务器会发送状态码200,下图是Beacon的控制台输出

五、任务下发
Beacon完成上线后,就可以向其下发任务了,任务不会立即的发送给Beacon端,而是存储在Server的beacon对象的任务队列中。当Beacon通过Get方式获取任务时,服务器根据beaconid从指定beacon的任务队列中取出任务并打包成任务包发送给beacon让其执行。
任务包长什么样子呢?一个任务包=任务包的长度(不包含长度字段,4B)+任务ID(4B)+任务数据(任务类型4B+Args)

任务创建代码编写顺序:Controller.InitRouter->Controller.BeaconCommandExecute->TeamServer.BeaconCommand->Handler.BeaconCommand->beaconHandler.BeaconCommand->beaconHandler.CreateTask->TeamServer.TaskCreate
任务获取代码编写顺序:HTTP.processRequest->TeamServer.BeaconGetAllTasks->TeamServer.TaskGetAll->Handler.BeaconPackData->beaconHandler.BeaconPackData->beaconHandler.BeaconHandler->beaconHandler.EncryptData
上面的编写顺序只包含主逻辑相关函数,一些工具函数就没列举出来了,具体看下面的代码编写
5.1 任务创建
controller/controller.go 增加一条"/beacon/command/execute"路由
controller/beacon.go:新增BeaconCommandExecute方法
utils/request/beacon.go 接收前端CommandData的json数据
controller/types.go 在TeamServer接口定义BeaconCommand方法
为了测试方便可以在Beacon项目中硬编码BeaconId

server/beacon.go:实现TeamServer.BeaconCommand方法
handler/beacon.go BeaconCommand,找到相应的beaconHandle处理者
handler/types.go 在BeaconHandler处理中定义BeaconCommand方法
handler/beacon/beacon_main.go 实现BeaconHandler.BeaconCommand方法
handler/beacon/types.go 在TeamServer接口处定义TaskCreate方法
utils/response/task.go 定义TaskData结构体
handler/beacon/beacon_handler.go 补充CreateTask方法(不完整),主要就是根据命令类型将commandId和一些参数打包在一起。回答上文的一个疑问:taskData是原始命令包,是 []byte 类型的,接下来这个taskData会与其他数据(任务包长度、taskID)再一次打包,形成真正的任务包,如果在 PackArray方法 里打包长度会增加一个长度字段,这个字段是多余的。
handler/beacon/types.go:CommandId常量
handler/beacon/beacon_utils.go 补充ConvertUTF8toCp方法,方法功能是将UTF-8编码的字符串转换成指定 Windows代码页(code page)的编码。
server/task.go 实现TeamServer接口的TaskCreate方法,该方法先校验目标是否在线、补充缺失元数据、生成唯一任务ID,最后把任务塞进对应Beacon的任务队列里。
utils/crypt/rand.go 根据指定长度生成唯一ID的GenerateUID方法
server/types.go 补充任务类型常量,本项目中只有 TYPE_TASK 类型的任务
5.2 任务获取
本项目中通过HTTP POST 不断的轮询向服务器请求任务,轮询就是一个死循环,然后在循环体里不断发送任务请求,等待服务器响应,解析响应走入不同的处理分支。任务还可以从DNS、SMB、TCP socket、WebSocket,grpc等协议通信获取,甚至github,百度网盘,评论区等可以留存文本信息且能读取的网站中获取,它们的优劣就看各位师傅使用的场景,我就不在这里长篇大论了。
这一小节只编写Server端的相关代码
handler/listener/http_listener.go processRequest补充“获取任务”和“替换payload存放的位置”的相关代码
handler/listener/http_type.go TeamServer接口定义BeaconGetAllTasks方法
server/beacon.go 实现BeaconGetAllTasks方法
server/task.go:TaskGetAll方法根据指定beaconId从Beacon任务队列中取出多个任务(有长度限制)
handler/beacon.go 补充BeaconPackData方法
handler/types.go 在BeaconHandler接口定义BeaconPackData方法
handler/beacon/beacon_main.go 实现BeaconHandler.BeaconPackData方法
handler/beacon/beacon_handler.go 补充PackTasks方法,这个方法就是打包成最后的任务包,任务的数据结构见本章开头。解释一下 array = append(array, int32(4+len(taskData.Data))) ,4表示taskid的长度。
handler/beacon/beacon_handler.go 补充EncryptData方法
handler/beacon/beacon_utils.go 补充RC4Crypt方法
5.3 测试
5.3.1 任务创建
①服务器启动:在 server/task.go 的TaskCreate方法下一个断点,我只想看到最后任务数据是否进入到任务队列里

②Beacon启动

③创建监听器

④创建任务:一定要Beacon完成上线才能创建任务
用postman发送创建任务的请求

这时会在刚刚下的断点处停下

步过,看到ts对象的相应Beacon的任务队列中存放了任务数据

5.3.2 任务获取
①启动服务器
②用postman发送创建监听器请求
③用postman发送创建任务的请求(beacon一定要先注册,才能下发任务)
④beacon调式分析
无任务:

错误:

有任务: 03:除长度字段外任务包长度 47:TaskId 811:CommandId(或者说是CommandType) 12n:Args,一些任务参数


六、任务执行与结果回显
6.1 任务包解读-Beacon端
还记得任务包长什么样子吗?这里再给出它的结构图,因为我们要根据按照服务器任务包打包顺序解读数据包的里内容。一个任务包=任务包的长度(不包含长度字段,4B)+任务ID(4B)+任务数据(任务类型4B+Args)

utils/packet/unpacker.go 包含解包器的相关方法
profile/profile.go:补充命令常量
main.go 这不是最终代码,后面还会改很多次,这里主要是写了任务包解读的相关代码
因为可以发送多个任务包,所以Beacon要用循环处理多个数据包
先确保有4字节可读,才进行下一步
根据0~3字节读取任务包数据,并用这个数据初始taskParser解析器
taskParser.buffer的0
3是taskId,47是commandId,8~n是命令参数紧接着根据commandId进入不同的处理分支,这一步没有写,后面会完成
到这里是可以进行测试的,我就不测了。
6.2 任务执行并打包结果
来到师傅们最熟悉的环节:任务执行。 网上已有大量公开资料与开源项目可供参考,覆盖范围从基础命令(ls、cd、mv、pwd、cat、whoami …)到进阶玩法(execute-BOF、execute-shellcode、execute-assembly、inline-execute等)五花八门。
为了教学方便,我只实现cd和cat命令,后面的各种进阶玩法就由各位师傅自己去拓展了。
beacon端
utils/common/changeCp.go
command/cat.go
Server端
因为server还没写cd的任务包,在这里补充
handler/beacon/beacon_handler.go
handler/beacon/types.go
Beacon端
command/cd.go
profile/profile.go
main.go 这并未包含结果返回的相关代码,而是直接将错误和结构输出
用postman发送cat命令请求,Beacon的执行结果如下

用postman发送cd命令请求
Beacon的执行结果如下

6.3 结果返回
接下来就是将错误和结果打包成结果包,每完成一个任务就通过POST的方式发送给Server,所以结果包长什么样子呢?一个结果包 = 除长度字段外结果包长度 4B + taskId 4B + commandId 4B + 结果数据。其实长度字段记录的长度都不包括其本身,请各位师傅知悉!

接下来是Beacon端的代码
profile/profile.go:增加 COMMAND_ERROR_REPORT 表示命令执行出错
utils/packet/packer.go:增加MakeFinalPacket放用于打包最后的结果包
utils/common/http.go 增加HttpPost方法,用于将结果包发送给服务器
main.go 在代码的末尾调用MakeFinalPacket制作结果包,然后调用HttpPost发送结果包给服务器
接下来是Server的代码
handler/listener/http_listener.go:补充processResponse方法专门用于处理Beacon的结果回显。本小节先不实现结果处理,直接打印结果数据,只是为了验证Beacon是否成功POST到服务器。
Beacon控制台输出:

server控制台输出:

因为我们用的是RC4加密,所以数据长度是一样的,还没解密故server控制台输出乱码。
至此,Beacon端的代码全部编写完成!
6.4 结果处理
上一小节我们只打印了结果包而没做其他处理,这一小节就是专门用于处理结果包的,按照结果包的顺序来解读结果包,解读之前需要进行解密。

handler/listener/http_listener.go 补充调用BeaconProcessData方法进行结果包处理
handler/listener/http_type.go 定义BeaconProcessData方法
server/beacon.go 实现BeaconProcessData方法
handler/beacon.go 补充BeaconProcessData
handler/types.go 定义BeaconProcessData方法
handler/beacon/beacon_main.go 实现BeaconProcessData方法
handler/beacon/beacon_handler.go 补充DecryptData方法
handler/beacon/types.go 补充COMMAND_ERROR_REPORT常量
handler/beacon/beacon_handler.go创建解析器解析然后包,并将结果信息输出到服务器的控制台,正常来说说要将taskdata的信息通过sync包同步到指定用户的控制台,这一块我不在这本项目实现。
至此,所有代码编写完成,难掩兴奋与激动,开始见证奇迹的一刻!

下一步计划
写到这里真的有点神志不清,可以看到后面我真的写的不耐烦了,到这里终于是写完了这篇又臭又长的文章了,每当写到文章的末尾总是头昏脑涨,写文章真是一件特别痛苦的事,尤其是长文。
可以看到我在文章中贴出完整的源码,看起来很繁琐实际也很繁琐但这是有必要的,因为这样就可以根据每一章节一步步构建出C2,这种方式是我学习一个陌生项目的基本做法。还有文章中并没有对代码做详细的解释,各位师傅可以用AI来辅助阅读。
考虑到文章呈现内容的局限性不能完整的体现开发过程,所以想录几个视频玩一玩ヾ(≧▽≦*)o
我承诺不开班,不收费,不搞圈子,纯粹的技术分享,可能是我不在这个圈子才敢这么做,所以求个点赞、收藏加关注不过分吧?(◍•ᴗ•◍)。
由于是社畜且不在安全领域,所以几个月或者几年开始录视频?反正这段时间好好休息一下,一个人的经历总是有限的,我又能拼搏几次呢?做出c2后我还想将webshell融入其中,正好web安全是我的薄弱项,借此机会学习一下,但这也是遥远未来之后的事了。
相信看过这篇文章之后,想必各位师傅对C2框架搭建是很熟悉了,这样就可以去github上找开源的C2的源码,最好是可以调式运行的,然后慢慢熟悉工作流程,最后积累自己的开发C2框架的经验。
下一步计划就是没有计划٩( ╹▿╹ )۶,随缘更新文章,内容随机,还有Convert2Shellcode项目开始修复我老早就提出的问题,然后编写支持x86架构的代码,也请各位师傅多多支持一下。
最后的最后要说的一点就是最近在弄一个新博客,也算是新的开始,期待与各位师傅交互友链!
Last updated