2024 年 6 月、セキュリティ研究者が「MuddyRot」(別名「BugSleep」)と呼ばれる新しいインプラントの分析を公開しました。このリモートアクセスツール(RAT)により、攻撃者は、特殊なコマンドアンドコントロール(C2)プロトコルを活用して、被害者のエンドポイントでリバースシェルおよびファイル入出力(I/O)機能を実行できるようになります。このブログ記事では、BugSleep のプロトコルを解読し、機能的な C2 サーバを作成し、Snort でこのトラフィックを検出するプラクティスと手法について紹介します。
主な調査結果
- BugSleep インプラントは、プレーン TCP ソケット上で特殊な C2 プロトコルを実装している。
- BugSleep を運用する攻撃者は、検出を回避するための複数のファイル難読化手法を使用している。
- BugSleep により、ターゲットシステムにリバースシェル、ファイル I/O、永続化の各機能が実装される。
データの送受信
この記事では、サンプル b8703744744555ad841f922995cef5dbca11da22565195d05529f5f9095fbfca を分析に使用します。C2 スタックにおける最も低レベルの 2 つの関数、SendSocket(FUN_1400034c0)と ReadSocket(FUN_140003390)は、送信および受信 API 関数の非常に軽量なラッパーであり、ペイロードの暗号化を処理します。これらの関数にはエラー処理が含まれ、失敗するまでデータの送受信を 10 回試行します。
このプロトコルは、整数と文字列の 2 つの型のみを持つ疑似 TLV(タイプ、長さ、および値)構造を使用しています。整数はリトルエンディアンの 4 バイトまたは 8 バイト値として送信され、文字列の先頭にはその長さを示す 4 バイト値が付加されます。ペイロードはその後、バッファ内の各バイトから静的な値(以下の例では 3)を減算することによって暗号化されます。
タイプ | 値 | プレーンテキスト | 暗号テキスト |
IntegerMsg | 6 | 06 00 00 00 | 03 FD FD FD |
StringMsg | Talos | 05 00 00 00 48 65 6C 6C 6F | 02 FD FD FD 51 5E 69 6C 70 |
図 1:BugSleep が使用するデータ暗号化の例
C2 通信を処理するための主な関数は、C2Loop(FUN_1400012c0)と CommandHandler(FUN_1400028a0)の 2 つです。C2Loop はサーバとのソケット接続の設定とビーコンの送信を行い、CommandHandler はサーバからのコマンドを処理および実行します。
ソケット接続を設定した後、インプラントは C2 サーバにビーコン(FUN_140003d80)を送信し、コマンドを要求します。ビーコンはコンピュータ名/ユーザー名形式の StringMsg です。サーバ応答の IntegerMsg が 0x03 に等しい場合、BugSleep はプロセスを自己終了します。これは、古い kill コマンドの名残か、後で実際の kill コマンドを読み取る手間を省くための緊急 kill であると思われます。
ビーコンへの応答後、各 BugSleep コマンドが IntegerMsg として送信されます。以下は、検出されたすべてのコマンド ID の定義を列挙したものです。
図 2:インプラントによって使用されるコマンド ID
ホームとの通信
インプラントはプレーン TCP ソケットを使用して通信します。これは、Netcat リスナーと Wireshark を使用して確認できます。
図 3:Wireshark を使用して確認された BugSleep ビーコン
図 1 に示したメッセージの暗号化について、このビーコンは Python を使用して復号できます(図 4)。これは、C2 サーバの残りの部分を構築するときに再び使用します。
図 4:ビーコンデータの復号化
Python C2 サーバ
プロトコルの基本を理解したら、C2 サーバの構築を開始します。完全なソースコードはこちらで公開されています。
ビーコン
前述のように、BugSleep のビーコン関数は StringMsg を送信し、サーバからの IntegerMsg 応答を読み取ります。返される IntegerMsg は 0x03 以外の値であればよいため、サーバが受信したコンピュータ名/ユーザー名の文字列長を返しました。
図 5:ビーコンデータを受信した C2 サーバからの出力
Ping コマンド
最も実装が簡単なコマンドは Ping コマンドです。このコマンドの ID は 0x63 です(BugSleep は受信した ID から 1 を減算します)。このコードは、4 バイトを送り返すというシンプルなものです。
図 6:Ping コマンドを処理する Switch-case 文
ビーコンを受信すると、サーバは次の処理を実行します。
- ビーコンへの応答に対して 4 バイトを送信する
- Ping コマンド ID に対して 4 バイトを送信する
- 4 バイトの Ping データを読み取る
Ping コマンドはヒープ上に最近割り当てられた 4 バイトを送り返すことが確認されており、そのデータがどのような形式かは不明な場合があります。実際に動作しているかどうかを確認するには、WinDbg でブレークポイントを設定し、送信前にメモリを手動で設定します。
図 7:メモリに書き込まれた 0xdeadbeef が Ping コマンド内のサーバで受信されたことを確認
File コマンド
次の一連のコマンドは、侵害を受けたシステムにファイルをダウンロード(GetFile)するか、C2 サーバにファイルをアップロード(PutFile)します。これらのコマンドは互いに逆の処理を実行するため、ここでは GetFile コマンドについてのみ詳しく説明します。今回は、SendSocket または ReadSocket への各呼び出しをトレースし、その呼び出しに対する応答を Python で実装するという手法を用いました。CommandHandler により、インプラントはネットワークから長さと値を読み取ります。これが取得するファイルです。
図 8:GetFile がソケットからパスの文字列長とパス文字列を読み取る
CmdGetFile 関数は、ターゲットファイルを開き、一度に 1 ページずつソケットにチャンクします。SendSocket 呼び出しの一覧は次のとおりです。
図 9:CmdGetFile 関数による SendSocket 呼び出し
図 10:GetFile コマンドによる C2 サーバの出力例
PutFile コマンドは、ポインタの計算を使用して受信ページを処理する点が GetFile コマンドと異なります。
図 11:複雑なファイルポインタの計算
これを解読すると、各ページは 4 バイトのページ番号で始まり、その後に 1,020 バイト(0x3fc)のファイルデータが続きます。GetFile コマンドではこの処理は行われず、1,024 バイトのファイルデータのページ全体がページ番号なしで送信されます。
リバースシェル
最後のコマンドはリバースシェルです。これは、ソケット上で多数の読み取りと書き込みを必要とするため最も複雑です。逆アセンブリはかなり長く、ソケット呼び出しを追跡するのが難しいため、ここでは省略しました。実質的には、インプラントは cmd.exe プロセス(FUN_1400016e0)を生成し、ソケットから実行するコマンドを読み取ります。シェルコマンドとその出力は、セッション中にパイプを介してプロセス間でマーシャリングされます。この操作が複雑なのは、BugSleep がパイプ API 呼び出しからの戻り値を段階的に報告すると同時に、シェル出力(FUN_140003840)の読み取りを試行するためです。インプラントは、文字列「terminate\n」を受信するまで、このコマンドの読み取りと出力の送信のループを継続します。
図 12:リバースシェルコマンドを実行している C2 サーバからの出力例
残りのコマンドはそれほど複雑ではありませんが、実装済みでこちらから確認できます。
Snort による検出
このサーバにより、Talos は BugSleep と攻撃者間の任意の数の会話をエミュレートできるようになります。このトラフィックは、実際の検出のパフォーマンスを記述および検証するために不可欠です。
検出の最初の候補になるのはビーコンです。これは通信をシャットダウンする最初の機会であり、BugSleep インスタンスはコマンドを受信できなくなります。各ビーコンは <len><data> の形式であることが確認されました。ここで、data は sub_string(COMPUTER_NAME + “/” + USERNAME, 3) です。この文字列は長くも静的でもないため、fast_pattern の候補としては不適切です。ただし、各ビーコンの先頭には 4 バイトのこの文字列が付加されることを思い出してください。特定の被害者のコンピュータ名/ユーザー名の文字列が、255 文字を超えることはほとんどありません。つまり、長さフィールドの大半は |XX 00 00 00|(エンコード後は |XX FD FD FD|)のようになります。これは、ストリームの最初の方の静的なオフセットですばやく一致する可能性があるため、適切な fast_pattern 候補になります。
図 13:BugSleep から送信されたビーコンのエンコードされた高次 0 バイトを検出
これは機能しますが、実際には誤検出(FP)が発生する可能性があります。BugSleep のすべてのサンプルはポート 443 を使用して確認されました。このインプラントはネットワーク外部の C2 サーバにも到達するため、このルールで検査されるトラフィックは次のヘッダーを使用して削減できます。
図 14:ネットワークからポート 443 に発信されるトラフィックを検査するようにルールを制限
flow:to_server,established オプションを使用すると、Snort による検出対象を、確立された TCP ストリームを介してクライアントから送信されるデータに制限できます。しかし、このルールの FP 率は依然として高い状態です。ネットワークからポート 443 に発信されるすべての TCP トラフィックについて、オフセット 1 に |FD FD FD| がある場合にアラートが表示されます。これは珍しいものに思えるかもしれませんが、そのトラフィックが BugSleep ビーコンであるという確証はありません。
Snort には、ルールにロジックや状態を追加するための強力なツールとして flowbits が備わっています。これにより、ルールの作成者は複数のルールにわたるストリーム内の状態を把握できます。この場合、ビーコンだけではアラートの信頼性は保証されません。しかし、flowbits を使用して、ビーコンと送り返されたコマンドをチェーンするとどうなるでしょうか。コマンド自体にはあまり情報はなく、可変長の非決定的な文字列(get、put など)または非決定的な 4 バイトの整数(heartbeat、increment timeout など)となっています。ただし、それらはすべて 4 バイトのコマンド ID で始まります。ネットワークからのビーコンの発信時に flowbits を設定すると、同じストリーム内でコマンド ID が返されたことが別のルールによって検出された際のアラートの信頼性が高まります。
コマンドルール
pcre ルールオプションを使用すると、11 個のルールを 1 個に減らすことができます。ビーコンのルールと同様に、|03| としてエンコードされた 3 つの 0 バイトは fast_pattern として使用できます。ルールが入力されると、flowbits の bugsleep_beacon チェックが実行され、誤検出の場合にはルールを迅速に終了できます。3 つの |03| バイトがオフセット 5 にあることが確認されると、pcre はいずれかのコマンド ID が存在することを確認できます。
図 15:C2 サーバから送信された BugSleep コマンドを検出するための Snort ルール
問題点
場合によっては、Snort は想定とは異なる方法でデータを処理または解釈することがあります。このサンプルのトラフィックはちょうどその完璧な例であり、Snort の解釈を確認する良い機会となっています。当初のビーコンルールは次のようなもので、コンピュータ名/ユーザー名の文字列に必ず含まれるエンコードされたスラッシュ(カンマとしてエンコード)を捕捉しようとしました。
図 16:コンピュータ名/ユーザー名の文字列内のスラッシュを捕捉しようとするビーコンルール
前述のように、インプラントは次の処理を実行します。
- サーバに接続する
- 文字列長(4 バイト)を送信する
- PC/ユーザー名の文字列(N バイト)を送信する
- 返された 4 バイトを読み取って応答を確認する
- 4 バイトのコマンド ID と N バイトのコマンドデータを読み取る
- コマンド応答の送信を開始する
Snort は、ネットワーク経由でデータを読み取る際に、データを解釈してさまざまなバッファ(pkt_data、file_data、js_data、http_* など)に分類します。この場合、TCP データがネットワーク上でチャンクされるので、Snort はそれらの個々の TCP セグメントを調べます。十分なデータが得られて初めて、Snort はそれをより大きな「TCP ストリーム」バッファにフラッシュして、ルールがクライアントやサーバから送信されたストリーム全体を解析できるようにします。
当初、get コマンドトラフィックはアラートを表示し、put コマンドトラフィックはアラートを表示しませんでした。幸いなことに、Snort 3 ではこれらの問題のデバッグに役立つトレースモジュールを利用できます。buffer オプションは Snort の各種バッファがいっぱいになるとそのバッファを出力し、rule_eval はルールが評価されるときにトレースします。次のスクリーンショットは、各 PCAP に対して Snort を個別に実行した結果の出力です。「snort.raw」は個々のパケットを表し、「snort.stream_tcp」はリアセンブルされた TCP ストリームを表します。
動作中の GetFile コマンドの最初に、ビーコンサイズとビーコンデータが 2 つの別々のパケットとして表示されます(図 17)。
図 17:Snort によって処理される個々のビーコンパケット
さらに下の方では、リアセンブルされた TCP ストリームが検査され、アラートが表示されることを確認できます。図 18 の上から下へと見ていくと、ルールが評価されるにつれてカーソルの位置とバッファの状態が変化しています。最後に、flowbits が設定され、コマンドルールで使用できるようになります。
図 18:BugSleep ビーコンの flowbits を設定する Snort トレースの出力
さらに下の方では、コマンドデータの TCP ストリームが処理されています。コマンドの高次 0 の検出、flowbits のチェック、PCRE の実行が行われ、SID が想定どおりにアラートが表示されます。
図 19:GetFile コマンドルールによって想定どおりにトラフィックのアラートが表示
PutFile コマンドのトラフィックの結果を調べると、異なる動作が観察されます。ビーコンの長さとビーコンデータの個々のパケットが受信されていますが、Snort が検査する最初のリアセンブルされた TCP ストリームは、インプラントに送り返されたコマンドです。図 20 は、コマンド ID が検出され、その後 flowbits チェックが失敗したことを示しています。
図 20:flowbits チェックに失敗した PutFile コマンドトラフィック
ログをさらにスクロールすると、ビーコンデータの TCP ストリームが最終的に入力され、Snort が想定どおりに flowbits を設定していることを確認できます。ただし、コマンド ID のストリームはすでに終了していますが、flowbits が設定されていなかったため解析に失敗し、アラートは表示されませんでした。この問題の原因は、サーバパケットがリアセンブルおよび検査された時点で、クライアントから送信される raw パケットが TCP ストリームにリアセンブルされていないことにあります。この問題が発生するのは、Snort がリアセンブルを行うために必要なデータが 20 バイトではまだ不十分なためです。
修正
残念ながら、ビーコンルールは、TCP のリアセンブルに依存せず、できるだけ早くアラートを表示するように調整する必要があります。前述のように、ビーコン関数は SendSocket を 2 回呼び出します。1 回目は 4 バイト長のデータに対して、2 回目はビーコンデータに対してです。つまり、Snort で最初に確認されるパケットは 4 バイトのみです。そこで、「bufferlen:=4」を追加すると、Snort の検出対象は 4 バイトのパケットのみに制限され、FP 率が大幅に低下します。最終的なソリューションは以下のとおりです。
図 21:4 バイト長のセグメントを検出するように修正されたビーコンルール
これでルールは想定どおりに動作するようになりました。
図 22:すべての BugSleep コマンドからのトラフィックにアラートを表示する Snort の出力
まとめ
BugSleep は新しいインプラントであり、週次リリースの展開が確認されているため、このプロトコルは変更され、これらのルールがバイパスされる可能性があります。ただし、次の 2 つのことは達成できました。
- このバリアントは今後、お客様のネットワーク経由で通信できなくなります。
- 攻撃者が BugSleep を再び使用するためには開発時間と資金を投資しなければなりません。
このトラフィックをカバーする公開済みの Snort SID は 63937 と 63938 です。
侵入の痕跡
ホスト:
- 1[.]235[.]234[.]202
- 146[.]19[.]143[.]14
- 46[.]19[.]143[.]14
- 5[.]239[.]61[.]97
ハッシュ
以下の Windows 実行可能ファイルは、調査中に収集されたものです。これらが改ざんされていないと仮定すると、このバイナリセットのコンパイル時間は、BugSleep の週次リリースを示しています。
SHA256 | コンパイル時間 |
b8703744744555ad841f922995cef5dbca11da22565195d05529f5f9095fbfca | 2024 年 5 月 8 日(水)00:55:53 UTC |
94278fa01900fdbfb58d2e373895c045c69c01915edc5349cd6f3e5b7130c472 | 2024 年 5 月 22 日(水)21:56:39 UTC |
73c677dd3b264e7eb80e26e78ac9df1dba30915b5ce3b1bc1c83db52b9c6b30e | 2024 年 5 月 31 日(金)23:29:21 UTC |
5df724c220aed7b4878a2a557502a5cefee736406e25ca48ca11a70608f3a1c0 | 2024 年 7 月 7 日(日)21:09:49 UTC |
960d4c9e79e751be6cad470e4f8e1d3a2b11f76f47597df8619ae41c96ba5809 | 2079 年 7 月 15 日(土)09:15:20 UTC |
本稿は 2024 年 10 月 30 日にTalos Group のブログに投稿された「Writing a BugSleep C2 server and detecting its traffic with Snort」の抄訳です。