リバーシングで読み解くSliver Beaconのシンボル難読化
はじめに
C2フレームワークとして知られるSliverのビーコンについて、そのシンボル難読化手法をリバースエンジニアリングから見ていきます。「コード追えばいいじゃん」はドMリバーサールートから外れるのでNGです。
Sliverとは
SliverはオープンソースのC2フレームワークでペンテストに使われたりしてます。以下の様に実際の攻撃者にも使われてしまってるツールです。
詳しくは以下のドキュメントを参照してください。
シンボル難読化
Sliverのビーコンを作成するときのコマンドは以下の様なものです。適当にHTTPのリスナーを指定して実施しました。
generate beacon --http IP:Port -N mybeacon
この時に生成されるexeファイルがビーコンです。このビーコンはデフォルトでシンボルを難読化する処理が入ります。
このビーコン作成の際に--skip-symbolsのオプションを指定するとシンボル難読化をスキップできます。
この2つのビーコン作成方法で生成されたexeファイルを比較しながら読み解いていこうと思います。
リバースエンジニアリング
ビーコン作成
適当に以下の様にビーコンを作成します。

tak.exeはシンボル難読化がされているビーコン、tak-skip.exeはシンボル難読化がスキップされているビーコンです。

サイズが違いますね。
エントロピー
適当にエントロピーを見てみます。
-
tak.exe

-
tak-skip.exe

data周りのセクションが若干シンボル難読化されているせいかエントロピーが高くなっています。この辺りに難読化データが入ってそうですね、わかんないけど。
静的解析
ビーコンのメイン関数に入るまではtak-skip.exeで解析していこうと思います。
Go language
SliverはGo言語製です。最初の実行処理周りはGo言語のバイナリを意識して解析する必要があります。
以下の様に最初は2回ほどJMPします。
※関数名などシンボルは自身で記載したものです。そのままリバーシングしてもsub_XXXXXXのままなので、TryHarderしましょう。


その後、以下の様な処理に入ります。goroutineの呼出し前の処理です。

ここで注目するのが以下の処理でしょうか。Intelフラグの確認処理ですね。
00464da0 if (temp0 != 0)
00464db8 if (temp1 == 0x756e6547 && temp3 == 0x49656e69 && temp2 == 0x6c65746e)
00464dba isIntel = 1
この0x756e6547, 0x49656e69, 0x6c65746eはGenuineIntelの比較です。
また、0x123の書き込みも気になりますね。これはGo言語のTLSスロットでのメモリ書き込みのテストが行われています。
GenuineIntelの比較、0x123のテスト、最初のJMP命令というのも合わせてGo言語製バイナリの可能性が高いという判断が慣れてる人だとできたりするかなと思います。
以下の公式ドキュメントを参考にすると追いやすいです。
この関数の最後の処理を見てみるとGo言語特有の呼びだしが見えます。newprocからGoルーチンの呼出しが行われ、スタックに積まれているmainPCの指すruntime.mainがnewprocによって実行されます。


この流れは以下のブログが参考になると思われます。
runtime.mainの中にはSliverのメイン関数があります。main.mainがそれです。

Sliverのメイン関数
main.mainに入ると、基本的に--skip-symbolsオプションを指定したものと、そうでないものの違いはまだ特段ありません。
- tak-skip.exe

- tak.exe

違いがあるのは008fe3e0の関数(Init_Kernel32とか付けてるやつ)から呼び出される008fe420(tak-skip.exe)と00e21fe0(tak.exe)の関数です。Load_Kernel32の前にPlaneとかEncodeとか適当に名前つけ分けてます。
シンボル難読化の解析
それぞれの中身を確認します。
-
tak-skip.exe

-
tak.exe

わぁ、全然違うや。
シンボル難読化されてないものは直でkernel32.dllの文字が見えます。おそらくDLLのロードだと思われます。難読化されてるほうはよくわからんバイトが並んでますね(コメントや関数名ですでにデコードや解析が済んでるのは悪しからず)。
ここからは難読化されてるほうの関数を追っていきます。
動的解析
kernel32.dllの難読化
00e22140にある「decode_kernel32.dll」の中身を確認します。

なんかよくわからんバイト列があります。
HUH???

なんやこれ。
アセンブリを読みます。

スタックに積んでガチャガチャしてるのがわかるかと思います。ぱっと見でStack String Obfuscationっぽいですね。
こういうのは動的解析でStackのメモリ追ったほうが早いので動的解析します。
適当にRIPを対象の関数のaddress領域にセットして実行していきます。

ガチャガチャ終わるとこにBP張って実行します。

kernel32.dllの文字列が見えました。難読化解除成功です。
Failed to loadの難読化
似たような難読化がロード関数にもあります。

これもStack積んでガチャガチャしてたので同様に動的解析します。


Failed to loadの文字列がStackメモリ上に見えます。これは難読化されてないものだと以下の様に見えます。

IsDebuggerPresentの難読化
最後に「Encode_Load_Kernel32_DLL」にあったバイト列の難読化も見てみます。
Stackガチャガチャしてたので同様に動的解析します。

IsDebuggerPresentが見えます。アンチデバッグでよく使われるAPIですね。
こんな感じでStack String Obfuscationによって難読化されているバイト列がところどころにあります。
Stack Stringと言ってもロジック自体はバラバラで、Solverコード書くのが面倒そうです。面倒なのでやりません。許せサスケ。
おわりに
Sliverのビーコンにおけるシンボル難読化手法をリバースエンジニアリングから見てきました。
Stack Stringを用いた難読化が主に使われていることがわかりました。
(読み解いてはない、すまぬ。)
明日にはみんな忘れてそうな内容ですが、リバースエンジニアリングの参考になれば幸いです。
Discussion