脅威リサーチ

アンチリバースエンジニアリング技術に対するエミュレーションの使用

投稿者 Gergely Revay | 2022年6月28日

実物のマルウェアをリバースエンジニアリングするのは、必ずしも簡単ではありません。非常に難しいことだと思う方もいることでしょう。しかし、実はビットの組み合わせなのです。セキュリティ業界全体に当てはまることですが、これはマルウェア開発者とのいたちごっこと言えます。私たちが絶えずマルウェアを暴き、攻撃者の詳細を把握して対策を講じていこうとすると、彼らも絶えず新たな方法を実行して、アナリストやリバースエンジニアリングの作業を遅らせようとします。最近のブログでは、Pandoraランサムウェアの分析を行い、アンチリバースエンジニアリング技術に関することと、コードを難読化するレイヤーが複数存在することについて解説しました。

このブログでは、Pandoraランサムウェアで発見したことを紹介するのではなく、どのようにして発見したのかを解説します。具体的には、エミュレーションを使用して、このランサムウェアサンプルにおける関数呼び出しの難読化と文字列の暗号化に挑みます。flare-emuフレームワークでIDAPythonスクリプトを実装し、IDA Proで分解されたものを解読できるようにします。これは、サンプルの静的解析に大きく役立つことでしょう。

影響を受けるプラットフォーム:Windows

Pandoraの問題

Pandoraのリバース時に直面した問題の詳細については、こちらの分析ブログを確認してください。このブログでは、Pandoraで確認された2つのアンチリバースエンジニアリング技術について解説します。

   -    Opaque Predicateによる関数呼び出しの難読化

   -    暗号化された文字列

まず、これらの問題について具体的に解説します。

Opaque Predicateによる関数呼び出しの難読化

図1では、Pandoraランサムウェア展開後、標準的な関数呼び出しがどのようになっているのかが示されています。

図1:Pandoraでの標準的な関数呼び出し

呼び出されている関数のアドレスが実行時に計算されていることがわかります。cs:qword_7FF6B6FF9AB8は、何かの関数アドレステーブルのベースアドレスのようです。次に、ハードコードされた値を使用して、テーブル内の適切な関数ポインタを見つけます。これをraxにロードしてから呼び出しを行います。opaque predicateはプログラムの表示で、結果がプログラマーにわかるようになっていますが、実行時にも判断が必要なのが一般的です。難読化および分析回避技術として、さまざまなところで使用されます。この場合、raxに入る値は固定されますが、実行時にも計算を行う必要があるため、静的解析ツールが中断されます。

図1を例にすると、raxのアドレスは次のように計算されます:

rax = *(*address_table_base + 0x260BB2E4) + 0xFFFFFFFFAAF7CABC)

10進数の場合:

rax = *(*address_table_base + 638300900) - 1426601284)

このような問題に対する自明な解は、デバッガーでマルウェアを実行し、そこからアドレスを取得することです。しかし、この例では、すべての関数呼び出しがこのようになりました(静的にリンクされたライブラリの関数呼び出しを除く)。つまり、デバッガーの関数呼び出しをすべて中断して、マルウェアで何が起こっているのかを考えるべきでした。これには、オートメーションが必要になってきます。

暗号化された文字列

今回のランサムウェアサンプルのもう1つの問題は、奇妙な文字列がすべて暗号化されていることでした。バイナリには多くのプレーンテキスト文字列がありましたが(図2参照)、ほとんどがWindows API関数名や埋め込みライブラリの文字列でした。マルウェアの行動を把握できる文字列は、プレーンテキストにはありません。これは、最新のマルウェアで非常によく見られる傾向なので、この問題に対するソリューションは、さまざまなマルウェアに対して使用できることでしょう。

図2:Pandora検体内の文字列

暗号化された文字列を含むマルウェアに遭遇した場合、アプローチとしては次の2つが一般的です。

-        デバッグやエミュレーションなどの動的なアプローチで、マルウェアが持つ文字列の復号関数を使用して作業を行う。

-        単純なスクリプトで再実装できるように、復号関数を詳細に把握する。これは通常、暗号化がシンプルな1バイトXORの場合のやり方です。

Pandoraの場合、復号関数は1つではなく、14種以上の異なる文字列の復号関数があるため、復号アルゴリズムを再実装することは必ずしも可能ではありません。

エミュレーション

エミュレーションを使用すると、コードがCPU上で実行されているかのように見せかけることができます。実際はCPUではなく、エミュレーションソフトウェアで行われています。エミュレーションは実際の実行に比べ非常に遅いのが一般的です。しかし、実行内容を制御したり、エミュレーションされたコードとの極めて高度な相互作用を全面的に制御したりできるようになります。たとえば、エミュレータがあれば、マルウェアの関数を1つだけ(または2行のコードだけ)エミュレーションすると、すべての命令においてプログラムの状態を判断できるようになります。今回は、IDA Proで直接実行できることがエミュレーションの大きなメリットと言えます。

flare-emu

flare-emuは、MandiantのFLAREチームによって作成されたエミュレーションフレームワークです。これは、Unicorn EngineおよびIDAPythonと呼ばれる有名なエミュレーションエンジンをベースにしています。Unicorn Engineを直接使用することもできますが、flare-emuには複雑性も秘められています。基本的に、エミュレーションするもの(コードの一部)を定義したり、エミュレーションがフックに到達したときに呼び出される特定のフックに対するコールバック関数を定義したりすることができます。例としては、callHookパラメータが挙げられます。このパラメータは、CALL命令がエミュレーションされるたびに呼び出されるコールバック関数を受け入れます。このコールバック関数では、レジスタのダンプ、データの変更、コールのスキップなど、あらゆる状況での実行内容を実装できます。flare-emuは非常に簡単で、比較的使いやすいことがわかりました(何かを解明するのにソースコードを調べる必要はありましたが)。

問題の解決

導入部分を詳細に説明したので、次は、IDAPythonスクリプトを使用してこれらの問題を解決するコードを書き込んでみましょう。

関数呼び出しの難読化

図3では、最初に解決を試みる問題が再び示されています。これは、Pandoraのコードの解凍部分にあるmain()関数の最初の関数呼び出しです。この時点で、main()関数をエミュレーションして、呼び出し前にraxの値をチェックすれば、適切な結果が得られるはずです。そうしたら、関数呼び出しの引数を読み出し、その情報をすべてIDA Proのコメントとしてアセンブリコードに追加することもできます。

図3:関数呼び出しの難読化

IDAPythonスクリプトをまとめてみましょう。図4では、エミュレーションの初期化方法が示されています。スクリプトの起動後、IDAの時点(get_screen_ea()で返された時点)でカーソルが置かれている関数をエミュレーションする必要があります。

図4:エミュレーションの初期化

flare-emuを初期化するには、EmuHelperをインスタンス化するだけです。flare-emuでは、さまざまな方法でエミュレーションを実行することができます。今回はemulateRange()関数を使用します。この関数は、エミュレーションするメモリ範囲を指定するときに使用します。関数の先頭に開始アドレスを設定します。終了アドレスは省略可能です(Pythonにはありません)。その場合、戻り値の型の命令に達するまでエミュレーションが実行されることになります。なお、emulateRange()の代わりにiterateAllPaths()も機能するはずなのですが、Pandoraの別の難読化技術により問題が発生してしまいました。この問題については、今回のブログの対象外とします。また、マルウェアが比較的シンプルなものであれば、iterateAllPaths()の方が良い場合もあります。

flare-emuのエミュレーション関数がどれか1つ呼び出されると(今回はemulateRange())、エミュレーションが開始されます。フレームワークでは、レジスタおよびスタックを有するプロセッサの状態やコールバック関数のデータなど、エミュレーションに関する付加的な詳細情報が得られますが、現時点では必要ありません。

emulateRange()では、さまざまなフックに対するコールバック関数を定義できます。

-        命令フック:あらゆる命令がエミュレーションされる前に呼び出されます。これを使用して、IDAでエミュレーションされた命令をすべて色分したことにより、エミュレーションの対象範囲が視覚化されました。

-        コールフック:CALLタイプの命令がエミュレーションされるたびに呼び出されます。なお、デフォルトでは、呼び出された関数はエミュレーションされません。

-        メモリアクセスフック:読み取りや書き込みでメモリアクセスするたびに呼び出されます。

現在のタスクで必要なのは、callHookのみです。図4の9行目のとおり、callHookパラメータとしてcall_hook_function名の引数をすでに渡しています(適切な名前ではなかったかもしれません)。次に、図5に示すcallHook関数を定義する必要があります。

図5:最初のCALL_HOHOOK ()実装

call_hook()関数を作成しました。この関数は、CALL命令がエミュレーションされる前に毎回エミュレータによって呼び出されます。現行の状態で、この関数が実行されたことがログに記録されます。次に、analyisHelperを使用して、現行のCALL命令のオペランドがレジスタであるかどうかを確認します。レジスタでない場合、「return」にして戻ることができます。今必要なのはレジスタの場合のみだからです。次に、レジスタ名(operand_name)とその値(operand_name)を回復させ、これらをログに記録します。main関数に対してスクリプトを実行すると、図6で示すような結果が表示されます。なお、Pandoraのコードには他にも悪意のある難読化が数多くあるため、このような単純なスクリプトでは関数全体をエミュレーションできません。ただし、スクリプトを拡張すれば可能です。

図6:最初のテスト結果

エミュレーションで3つのCALL命令が検知され、オペランドレジスタの値が表示されました。これを見る限り、関数呼び出しの難読化の問題はほとんど解決されていると考えられます。異なるCALL命令に対して呼び出されているアドレスが確認できるからです。今必要なのは、これをIDAで分解されたものに追加することだけです。CALL命令を解析するときに、IDAで行うことは次のとおりです。

-        呼び出されている関数のアドレスを含むコメントを追加する

-        関数呼び出しの引数を含むコメントを追加する

-        呼び出される関数にIDAの相互参照を追加する

図7では、更新されたコードが示されています。

 

図7:コメントと相互参照の追加

コメントを作成するときは、flare-emuの中から適切な機能を使用します。これにより、アーキテクチャに依存しない方法で関数パラメータを取得できるようになります。このマルウェアはx86_64であるため、rcxrdxr8r9、スタックを使用するだけで済みますが、このような方法で対処する必要はありません。コールフックが取得する引数の1つがarguments変数となり、flare-emuが認識する関数呼び出しのパラメータの値がこの変数に含まれるからです。もちろん、呼び出された関数を分析しなければ、パラメータがどれほどあるのかわからないので、これらはすべて表示します。

最後に(23行目)、IDAの相互参照を追加します。これは、分析を詳しく行う上で非常に役立ちます。main関数に対してこのコードを実行すると、図8のような結果が表示されます。

図8:関数呼び出しの結果の解明

暗号化された文字列

これで最初の問題が解決し、エミュレーションフレームワークを使用できるようになりました。続いて2番目の問題、文字列の復号に移ります。文字列を復号するにはどの関数をエミュレーションするのか。それには、どれが復号関数なのかがわかっていなければなりません。常に、リバースエンジニアリングは反復プロセスです。main関数に書き込んだスクリプトを実行すると、呼び出された関数の分析を開始できます。では、関数が復号関数であるかどうかはどのように判断すればよいでしょうか。(結論:0x7ff6b6f971e0が復号関数を指します)。

A) IDAで確認します。0x7ff6b6f971e0の関数をよく調べられなくても、図10のグラフビューを見てみると、非常に単純でループがいくつかあることがわかります。

図9:0x7ff6b6f971e0での関数のグラフビュー

コードをスクロールすると、図10の基本ブロックが表示されます。ここでは、いくつかの値を反復処理してXORを実行することがわかります。これは、XORベースのエンコーディング / 暗号化である可能性があることを示しています。

図10:XORがデコード/復号を示唆

B) デバッガーで確認します。静的分析と並行して、(安全な環境で)マルウェアをデバッグすることも当然できます。デバッガーで、一部のアドレスを入力として取得し、文字列を返す関数が表示された場合、その関数は復号関数である可能性があります。図11では、0x7ff6b6f971e0の関数が返されている様子が示されています。実際には、rcxに文字列「ThisIsMutexa」を返します。

図11:復号された文字列がrcxに表示

関数が復号関数であることがわかったら、名前を適切に変更することができます(mw_decrypt_str()としました)。興味深いことに、コードを深く調べていくうちに、Pandoraでは複数の復号関数が使用されていることも徐々にわかってきました。最終的に14種類の復号関数を特定しましたが、ほとんどの復号関数は図9と非常に類似していたため、別の復号関数もすぐにわかりました。

復号関数がいくつかわかれば、IDAPythonスクリプトを改良して、復号関数が呼び出されたときにいつでも関数呼び出しをエミュレーションすることができるようになります。これは、flare-emuのドキュメント例の1つと非常に類似しています。このことから、このようなコードが頻繁に再利用されていることがわかります。

図12では、更新されたcall_hook()関数が示されています。23行目からは、まず、呼び出しているアドレスの関数がmw_decrypt_strの文字列を含む名前かどうかを確認します。これは、呼び出された関数が復号関数であるかどうかを判断する方法です。あまり科学的な方法ではありませんが、必要なのはPhDの取得ではなく、マルウェアのリバースです。

図12:call_hook()に復号を追加

復号関数の場合、スクリプト内でdecrypt()関数を呼び出します。これにより、復号されたプレーンテキスト文字列が返されます。次に、復号された文字列を含むコメントも作成します。

復号のエミュレーション方法は図13のとおりです。EmulateRangeのインスタンスを新規作成します。emulateRangeを起動するときは、関数名(fname)を使用して、関数のアドレスを開始アドレスとして取得します。また、argv配列が持つ最初の4つの要素を引数レジスタとして渡します。最後に、argv[0]の値を返します。復号された文字列のアドレスを含める必要があります。

図13:復号のエミュレーション

IDAでスクリプトを実行すると、図12のように結果が示されます。復号された文字列はThisIsMutexaでした。これが、コメントに追加され、出力に記録されました。

図14:文字列の復号に成功

これで、文字列を自動的に復号できるようになりました。コードの分析を進めていき、さらに多くの復号関数が検出されると、これらの復号関数を呼び出す関数でスクリプトを再実行するだけで、プレーンテキスト文字列を取得することができるようになります。

結論

Pandoraランサムウェアには、難読化とアンチリバースエンジニアリング技術が多数組み込まれています。今回のブログでは、関数呼び出しの難読化と文字列の暗号化の2つについて解説しました。flare-emuのエミュレーションフレームワークを使用してIDAPythonスクリプトを書き込み、関数呼び出しのアドレスと引数を解析し、さらに、復号関数をエミュレーションして文字列をプレーンテキストとして回復させました。最後のスクリプトでは、さらに発展させて、Pandoraランサムウェアの詳細な分析で解説したアンチリバースエンジニアリングの他の問題に対処することができました。

フォーティネットのソリューション

分析が行われたPandoraランサムウェアのサンプルは、次の(AV)シグネチャで検知することができるようになっています。

W64/Filecoder.EGYTYFD!tr.ransom

また、FortiEDRでは、振る舞い分析を組み合わせたり、機械学習と脅威インテリジェンスフィードを統合したりすることで、Pandoraランサムウェアの実行を検知したり減災したりすることができます。今回のブログの一環で分析したPandoraサンプルの実行により、7つのルールがトリガーされ、9つのセキュリティイベントが生成されています。トリガーされたルールは、実行前の分析と実行後の振る舞いにより生成されたものです。これらのセキュリティイベントは、図15で確認できます。

 

図15:Pandoraランサムウェアサンプルの実行後に生成されたFortiEDRセキュリティイベント。実行時においては、実行後の検知を適切に実証するために、FortiEDRでは、減災ではなくイベントのログ記録のみが行われるように設定されています。

実行前の検知には、悪意のあるファイル(ハッシュベース)の識別、不審なパッカーの検知、書き込み可能なコードの存在が含まれます。実行後の検知には、各ファイルの暗号化(試行)の検知、暗号化されたファイル名の変更(試行)の検知、ランサムノートのドロップ、SMB共有へのアクセス(試行)が含まれます。

保護モードでは、FortiEDRが振る舞いを検知しこれを減災します。Pandoraの場合、ランサムウェアの実行を防止し、悪意のあるアクティビティが発生する前にこれを減災します。また、攻撃者がサンプルを実行する可能性がある場合は、後のファイル暗号化の試行を阻止します。また、攻撃後の検知ではシグネチャに依存しません。サンプルに関する予備知識がなくても、新しいPandoraの亜種に対しアクティビティを効果的に減災できるようになっています。

IOC(Indicators of Compromise:侵害指標)

Mutex:ThisIsMutexa
ランサムノート:Restore_My_Files.txt
ハードコードされた公開キーのSHA256ハッシュ:7b2c21ea03a370737d2fe7c108a3ed822be848cce07da2ddc66a30bc558af6b
 SHA256ハッシュサンプル:5b56c5d86347e164c6e571c86dbf5b1535eae6b979fede6ed66b01e79ea33b7b

ATT&CK TTPs

TTP名称

TTP ID

説明

難読化されたファイルや情報 : ソフトウェア圧縮

T1027.002

UPXパッカーが変更される
防御を低下させる方法:Windowsイベントログを無効化

T1562.002

イベントログを無効化
防御を低下させる方法:ツールを無効化または変更

T1562.001

AMSIをバイパス
ローカルシステムからのデータ

T1005

マウントされていないドライブとパーティションを検索
レジストリの変更

T1112

暗号化キーをレジストリに保存
影響用にデータを暗号化

T1486

ランサムウェアとしてファイルを暗号化
コマンドおよびスクリプトインタープリター

T1059

cmd.exeでシャドウコピーを削除

システム情報の発見

T1082

GetSystemInfo()でシステム情報を収集

ファイルとディレクトリの検知

T1083

ドライブを検知しファイルシステムをエミュレーション
システムリカバリを妨害

T1490

シャドウコピーを削除
サービス停止

T1489

ファイルがロックされると、プロセスを終了する