
.NET Framework を対象とするファイルの自動アンパッカーとロガー
DotNet Framework をターゲットとするファイルのための自動アンパッカー兼ロガーです!このツールは Black Hat USA 2022 で発表されました。Black Hat Asia 2023 では、DotDumperGUI と DotDumperNative、および DotDumper バージョン 1.1-stable が公開されました。これら3つのツールは一緒に使用することを想定しており、DotDumper 1.1-stable は DotDumperNative の DLL が存在する必要があり、DotDumperGUI は DotDumper の実行結果である JSON 出力を開いてフィルタリングするためのグラフィカルユーザーインターフェースとして設計されています。
任意のファイルを信頼性の高い方法で自動的に検出および分類することは、マルウェア解析の聖杯とも言えるでしょう。そこに至るまでの試練は数多く、そのためそのようなシステムの構築は高く評価されています。DotNet をターゲットとするバイナリに関しては、新しいオープンソースツール DotDumper が、その過程におけるいくつかの重要なステップ(インメモリでのアクティビティのログ記録、有用なメモリセグメントのダンプ、サンプルの特性抽出)を支援することを目的としています。
簡単に言えば、手動でのアンパックは退屈なプロセスであり、アナリストにとって不釣り合いな時間を消費します。難読化されたバイナリは、ファイルのアンパックに必要な時間をさらに増加させます。これをスケールする場合、組織は毎日マルウェアを解析する多数のアナリストを必要とし、おそらくスケーラブルなサンドボックスと組み合わせることになります。失われた貴重な時間は、広く蔓延している平凡な汎用マルウェアではなく、新しい脅威を発見するための興味深いキャンペーンやサンプルの調査に使用できるはずです。何と言っても、アナリストは干し草の山の中の数本の針を探すのです。
では、DotDumper はどのような違いをもたらすのでしょうか?DotDumper を介して DotNet ベースのマルウェアサンプルを実行すると、重要な、文脈を提供する、一般的な関数呼び出しのログファイルが3つの形式(人間が読めるプレーンテキスト、JSON、XML)で生成され、有用なメモリセグメントのコピーも提供されます。これにより、アナリストは関数呼び出しのログをざっと見ることができます。さらに、ダンプされたファイルをスキャンして分類することで、マルウェアサンプルとその含まれるデータに関する追加の洞察を得ることができます。これにより、トリアージやインシデントレスポンスのプロセスに不可欠な時間を短縮し、SOCアナリストや研究者の時間をより高度な分析ニーズに割り当てることができます。
文脈を提供する関数呼び出しとその結果をログに記録しダンプするために、DotDumper はリフレクションとマネージドフックの組み合わせを使用しており、すべて純粋な C# で記述されています。以下では、主要な機能を強調し、詳しく説明するとともに、パックされた AgentTesla スティーラサンプル(ハッシュは以下)に対する DotDumper の出力の抜粋を示します。
| ハッシュタイプ | ハッシュ値 |
|---|---|
| SHA-256 | b7512e6b8e9517024afdecc9e97121319e7dad2539eb21a79428257401e5558d |
| SHA-1 | c10e48ee1f802f730f41f3d11ae9d7bcc649080c |
| MD-5 | 23541daadb154f1f59119952e7232d6b |
DotDumper はコマンドラインインターフェースからアクセス可能で、さまざまな引数を提供します。以下の画像はヘルプメニューを示しています。すべての引数について説明するわけではなく、最もよく使われるものを取り上げます。

サンプルを実行するための最小要件は、「-file」引数にファイル名またはファイルパスを指定することです。フルパスが指定された場合はそれが使用されます。ファイル名のみが指定された場合、カレントワーキングディレクトリと DotDumper の実行可能ファイルが配置されているフォルダがチェックされます。
ディレクトリ名が指定されない限り、「-log」フォルダ名はサンプルのファイル名(拡張子があればそれを除いたもの)と同じに設定されます。フォルダは DotDumper が存在する同じフォルダに配置され、ログとダンプされたファイルはそこに保存されます。
ライブラリの場合、またはバイナリへの代替エントリポイントの場合、エントリポイントを「-overrideEntry true」で上書きする必要があります。さらに、「-fqcn My.NameSpace.MyClass」を使用して完全修飾クラス名(名前空間を含む)を指定する必要があります。これにより DotDumper がどのクラスを選択するかが決まり、そのクラスから指定された関数名(「-functionName MyFunction」を使用)が取得されます。
選択した関数に引数が必要な場合、「-argc」と必要な引数の数を使用して引数の数を指定する必要があります。引数の型と値は「string|myValue int|9」のように指定します。値にスペースが含まれる場合は、コマンドラインインターフェース上で引数を引用符で囲み、単一の引数として渡されるようにする必要があります。
「-raceTime」や「-deprecated」などのあまり使用されないオプションは、デフォルト設定で安全ですが、将来 DotNet Framework の変更により調整が必要になる可能性があります。これらは現在コマンドラインインターフェースで公開されており、必要に応じて(たとえ古いバージョンの DotDumper を使用している場合でも)簡単に変更できるようになっています。
ログ記録とダンプは DotDumper の2つの主要機能です。分析にかかる時間を最小限に抑えるため、ログはアナリストにコンテキストを提供する必要があります。これは、ログに記録される各関数呼び出しについて、以下の情報をアナリストに提供することで実現されます。
ダンプされた各ファイルについて、ファイル名はそのファイルの SHA-256 ハッシュと同じであることに注意してください。
上記を明確にするために、ログの抜粋を以下に示します。この抜粋は、前述の AgentTesla サンプルが DotNet の Assembly.Load 関数を使用して第2ステージをロードする際の詳細を示しています。
![インターセプトされた Assembly.Load(byte[] rawAssembly) 関数呼び出しのログ](images/assembly_load.png)
まず、ローカルシステム時刻が、元の関数の戻り値の型、名前、および引数とともに表示されます。次に、スタックトレースが表示され、サンプルのメイン関数がコンストラクターに至り、コンポーネントを初期化し、2つのカスタム関数を呼び出していることがわかります。Assembly.Load 関数は「NavigationLib.TaskEightBestOil.GGGGGGGGGGGGGGGGGGGG(String str)」内から呼び出されました。これにより、アナリストはこの呼び出しに関心がある場合、その周辺のコードを見つけるためのコンテキストを得られます。
次に、アセンブリの呼び出し順序に関する情報が表示されます。ステージが増えるほど、どのステージを経て呼び出しが行われたかを把握するのが難しくなります。通常は1つのステージが次のステージをロードしますが、場合によっては後のステージが前のステージを非線形な順序で利用することがあります。さらに、発信元のアセンブリに関する情報が表示され、アナリストのデータがさらに充実します。
次に、親ハッシュが表示されます。ステージの親は前のステージであり、この例ではまだ存在していません。新しくロードされたステージは、このステージを親として持ちます。これにより、アナリストはイベントをより簡単に関連付けることができます。
最後に、関数の戻り値の型と値が保存され、フックされた関数に渡された各引数の型、名前、値も保存されます。変数が100バイトを超える場合は、代わりにディスクに保存されます。その後、値の代わりにファイルを参照するための参照がログに挿入されます。しきい値は、一部の配列が数千のインデックスサイズになるため、ログの出力に支障をきたさないように設定されています。
Microsoft のドキュメントによると、リフレクションは「アセンブリ、モジュール、型をカプセル化するオブジェクトを提供する」と最もよく要約されています。簡単に言えば、これによりマルウェアサンプルから DotNet のクラスや関数を動的に作成および呼び出すことができます。DotDumper にはリフレクティブローダーが含まれており、アナリストが実行可能ファイルとライブラリの両方をロードして分析できるようになっています(これらが DotNet Framework ベースである限り)。
ローダーを利用するには、コマンドラインインターフェースでエントリポイントを上書きし、指定されたファイル内のクラス(その名前空間を含む)と関数名を指定する必要があります。オプションで、指定された関数に引数(ネイティブ型とその配列)を提供できます。ネイティブ型の例としては、int、string、char、および int[]、string[]、char[] などの配列があります。すべての引数はコマンドラインインターフェースを介して指定し、型と値の両方を指定する必要があります。
エントリポイントを上書きしない場合は、デフォルトのエントリポイントが使用されます。デフォルトでは、サンプルが引数なしで実行された場合と同様に、空の文字列配列がサンプルのメイン関数に渡されます。
さらに、リフレクションはローダーによって、次のステージの特定のクラスの特定の関数を呼び出すためによく使用されます。時には引数も一緒に渡され、後でリソースを復号するために使用されます。前述の AgentTesla サンプルでは、まさにこのシナリオが発生します。DotDumper の invoke 関連フックはこれらの発生をログに記録します。以下を参照してください。

最初の行の関数名は DotNet Framework の内部関数ではなく、第2ステージ内の特定の関数への呼び出しです。関数シグネチャには3つの引数の型と名前がリストされています。それらの値は関数引数情報セクションにあります。これにより、アナリストは指定された引数値を使用してカスタムローダーで第2ステージをロードしたり、以前にダンプされたステージをロードして引数を提供することで、DotDumper を使用してそれを行うこともできます。
マネージドフックに入る前に、フックがどのように機能するかを理解する必要があります。ここで考慮すべき主な変数は、ターゲット関数とフックと呼ばれる制御された関数の2つです。簡単に言えば、ターゲット関数(Assembly.Load など)のメモリが変更され、代わりにフックにジャンプするようになります。これにより、プログラムの実行フローが迂回されます。フックは任意のアクションを実行でき、オプションで元の関数を呼び出し、その後必要に応じて戻り値とともに実行を呼び出し元に返します。以下の図はこのプロセスを示しています。

フックが何であるかを理解することは、マネージドフックが何であるかを理解するために不可欠です。マネージドコードは、DotNet ランタイムや Java の仮想マシンなどの仮想化されたマネージド環境で実行されます。マネージド関数が存在するメモリアドレスを取得する方法は、C などのアンマネージド言語とは異なります。両方の関数の正しいメモリアドレスが取得されたら、アンセーフ C# と DotNet の相互運用サービスを使用してネイティブ Windows API 機能を呼び出すことにより、メモリに直接アクセスしてフックを設定できます。
DotDumper v1.1-stable 以降、DotDumper はアンマネージド(ネイティブとも呼ばれる)関数もフックできるようになりました。関数のリダイレクトは、マネージド関数とアンマネージド関数の間の任意の組み合わせが可能ですが、1つの重要な注意点があります。マネージドフックを使用するアンマネージド関数は、スタックの値を正しく読み取ることができません。そのため、ネイティブコンポーネント(DotDumperNative と呼ばれる)が必要です。このコンポーネントは名前付きパイプを介して DotDumper 自体と通信し、集中ログシステムを使用しながら、スタック上にある関数引数にアクセスしてアンマネージド呼び出しをインターセプトします。
DotDumper は外部依存関係のない純粋な C# で記述されているため、Visual Studio を使用してフレームワークを簡単に拡張できます。コードはこのブログ、GitHub、クラス内、関数内、ソースコード内のインラインで文書化されています。これにより、明確な命名規則と相まって、誰でもツールを好きなように変更でき、ツールを理解するために必要な時間と労力を最小限に抑えられます。代わりに、開発者とアナリストがツールの改善に専念できるようになります。
サンドボックスタイムアウトサポート機能は変更も修正もされていませんが、これまで文書化されていませんでした。DotDumper は指定されたファイルを実行するため、サンプルが終了するまで実行が継続されます。マルウェアはしばしば「待機」状態に入り、特定の条件が満たされるまで再アクティブ化しないか、または別のプロセスで実行を継続します(プロセスインジェクションを使用)。たとえば、プロセス置換で新しく作成されたスレッドは、そのスレッドが終了するまで戻りません。
停止を回避するために、DotDumper には停滞ハンドラがあります。フックの結果がログに記録されるたびに、カウンターが増加します。3回連続して20秒間隔でこのカウンターが変更されない場合、DotDumper は停滞状態が発生したと見なします。その場合、システム時刻を 30-12-2200 12:00 に設定して強制的にサンドボックスをタイムアウトさせ、その後ロガーを介してアナリストに通知し、自身を終了します。サンドボックスタイムアウトは、現在の日付から新しく設定された日付までの経過時間が通常の5分または10分を超えた場合に発生します。たとえサンドボックスの実行時間が数時間または数日に設定されていても、それを超えます。
分析を途中で打ち切る理由は、時間とリソースを節約するためです。タイムアウトが10分のサブミッションでも、実際の実行に2分しかかからない場合があります。そのため、停滞ハンドラを使用することで、同じ時間内に複数のサンプルを実行できます。停滞ハンドラのロジックの概要を以下に示します。

DotDumper の目的と機能が明確になったところで、既存の公開ツール(ILSpy、dnSpyEx、de4dot、pe-sieve など)との重複があるように思われるかもしれません。あるツールが別のツールより優れていると主張する意図はなく、むしろツールがどのように異なるかを説明します。
DotDumper の目的は、DotNet をターゲットとするサンプルからの重要な、文脈を提供する、一般的な関数呼び出しをログに記録しダンプすることです。ILSpy は DotNet の逆アセンブラ兼逆コンパイラですが、ファイルの実行はできません。dnSpyEx(およびその前身 dnSpy)は ILSpy を逆アセンブラ兼逆コンパイラコンポーネントとして使用し、デバッガを追加しています。これにより、手動でメモリを検査および操作できます。de4dot は DotNet バイナリの難読化解除のみを目的としており、コードの人間の目での可読性を向上させます。この比較の最後のツール pe-sieve は、実行中のプロセスからマルウェアを検出およびダンプすることを目的としており、使用されているプログラミング言語は無視します。以下の表は、上記のツールの概要をグラフィカルに示しています。

DotDumper は絶えずレビューと開発が行われており、バグ修正と新機能の追加という2つの主要な関心領域に焦点を当てています。開発中にコードはテストされましたが、変更される可能性のある DotNet Framework の関数へのフックの注入により、コードにバグが存在する可能性は十分にあります。バグに遭遇した方は、GitHub リポジトリで Issue を開くことをお勧めします。その後、確認が行われます。新機能の提案も GitHub リポジトリを介して可能です。GitHub アカウントをお持ちの方、または公の場でやり取りしたくない方は、私の Twitter までプライベートメッセージをお送りください。
言うまでもなく、分析中に DotDumper を使用した場合、または創造的な方法で使用した場合は、公的または私的にご連絡ください!自作ツールの使用例を聞くほど嬉しいことはありません!
DotDumper にはまだまだ多くの計画があり、利用可能になり次第コミュニティにアップデートをお知らせします!