
スクリプト可能なバイナリエミュレーションフレームワークで、IDA Pro/Radare2とUnicornエンジンを統合し、x86、ARM、ARM64アーキテクチャにおける自動マルウェア解析、文字列復号化、コードパス探索を実現します。
flare-emuは、IDA ProやRadare2などのサポートされているバイナリ解析フレームワークと、Unicornのエミュレーションフレームワークを組み合わせ、ユーザーがエミュレーションタスクをスクリプト化するための使いやすく柔軟なインターフェースを提供します。このツールは、サポートするアーキテクチャ向けに柔軟で堅牢なエミュレータをセットアップするための面倒な処理をすべて引き受け、ユーザーがコード解析の問題に集中できるように設計されています。現在、flare-emuはx86、x86_64、ARM、ARM64のアーキテクチャをサポートしています。
現在、5つの異なるインターフェースと、それに関連する多数のヘルパー関数およびユーティリティ関数を提供しています。
emulateRange – このAPIは、ユーザー指定のコンテキスト内で命令の範囲または関数をエミュレートするために使用します。個々の命令に対するフックと、"call"命令が出現したときのフックについて、ユーザー定義フックのオプションを提供します。ユーザーは、エミュレータが関数呼び出しをスキップするか、またはその中に入るかを選択できます。このインターフェースにより、ユーザーは特定のレジスタやスタック引数に値を簡単に指定できます。バイト文字列が指定された場合、それはエミュレータのメモリに書き込まれ、ポインタがレジスタまたはスタック変数に書き込まれます。エミュレーション後、ユーザーはflare-emuのユーティリティ関数を使用して、エミュレートされたメモリやレジスタからデータを読み取るか、返されたUnicornエミュレーションオブジェクトを直接調査に使用できます。emulateRangeの小さなラッパー関数であるemulateSelectionを使用すると、IDA Proで現在強調表示されている命令の範囲をエミュレートできます。
iterate - このAPIは、特定の分岐に沿って強制的にエミュレーションを行い、指定されたターゲットに到達するために使用します。ユーザーはターゲットアドレスのリスト、または関数のアドレス(その関数へのクロスリファレンスのリストがターゲットとして使用される)と、ターゲットに到達したときのコールバックを指定できます。エミュレーション中に別の分岐が取られる原因となった条件に関係なく、ターゲットに到達します。emulateRange APIと同様に、個々の命令に対するフックと、"call"命令が出現したときのフックについて、ユーザー定義フックのオプションが提供されます。iterate APIの使用例としては、argtrackerツールと同様のことを実現することが挙げられます。
iterateAllPaths - このAPIはiterateと非常によく似ていますが、ターゲットアドレスを指定する代わりに、すべてのパスを見つけてエミュレートしようとするターゲット関数を指定する点が異なります。これは、関数のすべての基本ブロックに到達するコード解析を行う場合に便利です。
emulateBytes – このAPIは、外部のシェルコードのブロブを単純にエミュレートする方法を提供します。指定されたバイトはIDBに追加されず、そのままエミュレートされます。これはエミュレーション環境の準備に役立ちます。例えば、flare-emu自体がこのAPIを使用して、Unicornでは公開されていないARM64 CPUのモデル固有レジスタ(MSR)を操作し、ベクトル浮動小数点(VFP)命令とレジスタアクセスを有効にします。Unicornエミュレーションオブジェクトが返され、ユーザーがさらに調査できます。
emulateFrom - このAPIは、難読化されたバイナリやシェルコードでよく見られるように、関数の境界が明確に定義されていない場合に便利です。開始アドレスを指定すると、エミュレートするものがなくなるか、フックのいずれかでエミュレーションを停止するまでエミュレートします。IDA Proでは、strictパラメータをFalseに設定して呼び出すことで動的なコード発見を有効にできます。flare-emuは、エミュレーション中に遭遇した命令をIDA Proに作成させます。
flare-emuをIDA Proにインストールするには、flare_emu.py、flare_emu_ida.py、flare_emu_hooks.pyをIDA Proのpythonディレクトリにコピーし、IDAPythonスクリプトでモジュールとしてインポートするだけです。
flare-emuをRizinにインストールするには、flare_emu.py、flare_emu_rizin.py、flare_emu_hooks.pyがPythonのモジュール検索パスに存在することを確認してください。Rizinをflare-emuのバイナリ解析コンポーネントとして使用する場合、rzpipeが必要です。
flare-emuをRadare2にインストールするには、flare_emu.py、flare_emu_radare.py、flare_emu_hooks.pyがPythonのモジュール検索パスに存在することを確認してください。Radare2をflare-emuのバイナリ解析コンポーネントとして使用する場合、r2pipeが必要です。
いずれの場合でも、flare-emuはUnicornとそのPythonバインディングに依存しています。
重要なお知らせ
flare-emuは新しいIDA Pro 7x APIを使用して記述されており、以前のバージョンのIDA Proとの下位互換性はありません。
flare-emuは多くのコード解析問題を解決するために使用できますが、一般的な用途の1つは、マルウェアバイナリ内の文字列を復号化することです。FLOSSは、文字列復号化関数を特定し、それへのすべてのクロスリファレンスで渡された文字列をエミュレーションによって復号化することで、これを自動的に実行できる優れたツールです。しかし、FLOSSが常にこれらの関数を識別し、汎用的なアプローチで適切にエミュレートできるとは限りません。時にはもう少し作業が必要になる場合があり、flare-emuに慣れれば、多くの時間を節約できます。暗号化された文字列を扱う際にマルウェア解析者が遭遇する一般的なシナリオを見てみましょう。
x86_64バイナリ内のすべての文字列を復号化する関数を特定したとします。この関数はいたるところで呼び出され、多くの異なる文字列を復号化します。IDA Proで、この関数にdecryptStringと名前を付けます。以下は、すべての文字列を復号化し、各関数呼び出しの場所に復号化された文字列をコメントとして追加し、復号化された各文字列とそのアドレスをログに記録するためのflare-emuスクリプトです。```
from future import print_function
import flare_emu
def decrypt(argv): myEH = flare_emu.EmuHelper() myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), registers = {"arg1":argv[0], "arg2":argv[1], "arg3":argv[2], "arg4":argv[3]}) return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData): s = decrypt(argv) print("%s: %s" % (eh.hexString(address), s)) eh.analysisHelper.setComment(address, s, False)
if name == 'main':
eh = flare_emu.EmuHelper()
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
`__main__`では、まず`flare-emu`の`EmuHelper`クラスのインスタンスを作成します。このクラスを使って`flare-emu`のすべての操作を行います。次に、`iterate` APIを使用して、`decryptString`関数のアドレスと、`EmuHelper`がエミュレートする各クロスリファレンスごとに呼び出すコールバック関数の名前を指定します。
`iterateCallback`関数は、`EmuHelper`のインスタンス(ここでは`eh`)、クロスリファレンスのアドレス、この特定の呼び出しに渡された引数、そしてここでは`userData`という特別な辞書を受け取ります。この簡単な例では`userData`は使用しませんが、エミュレータの永続的なコンテキストとして、独自のカスタムデータを保存できるものと考えてください。ただし注意が必要です。なぜなら、`flare-emu`自体もこの辞書を使用して、タスクを実行するために必要な重要な情報を保存しているからです。そのようなデータの1つは`EmuHelper`インスタンスそのもので、`"EmuHelper"`キーに保存されています。興味があれば、ソースコードを検索してこの辞書について詳しく調べてください。このコールバック関数は単に`decrypt`関数を呼び出し、復号された文字列を出力し、その`decryptString`への呼び出しのアドレスにコメントを作成します。
`decrypt`は`EmuHelper`の2番目のインスタンスを作成し、それを使用して`decryptString`関数自体をエミュレートし、文字列を復号します。この`decryptString`関数のプロトタイプは次のとおりです。`char * decryptString(char *text, int textLength, char *key, int keyLength)`。これは文字列をその場で復号するだけです。`decrypt`関数は、`iterateCallback`関数が受け取った引数をそのまま`EmuHelper`の`emulateRange` API呼び出しに渡します。これは`x86_64`バイナリであるため、呼び出し規約では引数をスタックではなくレジスタで渡します。`flare-emu`はIDA Proが判断したアーキテクチャとファイル形式に基づいて、どのレジスタがどの引数を表すかを自動的に判断するため、少なくともある程度アーキテクチャにとらわれないコードを書くことができます。これが32ビット`x86`の場合は、代わりに`stack`引数を使用して引数を渡すことになります。例:`myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), stack = [0, argv[0], argv[1], argv[2], argv[3]])`。`x86`では最初のスタック値が戻りアドレスになるため、ここではプレースホルダとして`0`を使用しています。エミュレーションが完了すると、`getEmuString` APIを呼び出して、関数に渡された最初の引数が指すメモリ位置に格納されているNULL終端文字列を取得します。
### flare-emu と idalib
* IDA Proをインストール
* Hex-Raysユーザーガイドに従ってidalibをインストール
* (仮想環境をアクティブにする)
* pip install /path/to/IDA/installation/idalib/python
* python /path/to/IDA/installation/idalib/python/py-activate-idalib.py [-d /path/to/active/IDA/installation]
* idaproをインポートしてスクリプトを作成
* 例については tests/test_flare_emu_idalib.py を参照
### Rizinを使った簡単な文字列復号シナリオ
上記と同じ例を使用する場合、Rizinで作業する際に大きく変わることはありません。違いの1つは、`flare-emu`は現在、Rizinで作業する場合、コマンドラインスクリプトまたはPythonシェル内で実行するように設計されていることです。Pythonシェルはアドホックな問題解決に最適ですが、コマンドラインスクリプトはバッチ処理に最適です。上記のスクリプトのRizin版は次のようになります(rizin内で実行するためにサンプルパスを省略することもできます):```
from __future__ import print_function
import sys
import flare_emu
def decrypt(argv, eh):
myEH = flare_emu.EmuHelper(samplePath=sys.argv[1], emuHelper=eh, isRizin=True)
myEH.emulateRange(
myEH.analysisHelper.getNameAddr("decryptString"),
registers={
"arg1": argv[0],
"arg2": argv[1],
"arg3": argv[2],
"arg4": argv[3],
},
)
return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData):
s = decrypt(argv, eh)
print("%s: %s" % (eh.hexString(address), s))
eh.analysisHelper.setComment(address, s, False)
if __name__ == "__main__":
eh = flare_emu.EmuHelper(samplePath=sys.argv[1], isRizin=True)
rz = eh.analysisHelper.r
eh.analysisHelper.setName(0x100000D60, "decryptString")
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
上記と同じ例を使用する場合、IDA ProではなくRadare2で作業してもほとんど変わりません。違いの一つは、flare-emuが現在、Radare2で作業する際にコマンドラインスクリプトまたはPythonシェル内で実行するように設計されていることです。Pythonシェルはアドホックな問題解決に最適ですが、コマンドラインスクリプトはバッチ処理に最適です。上記のスクリプトのRadare2版は次のようになります:```
from future import print_function
import flare_emu
def decrypt(argv, eh): myEH = flare_emu.EmuHelper(samplePath=sys.argv[1], emuHelper=eh) myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), registers = {"arg1":argv[0], "arg2":argv[1], "arg3":argv[2], "arg4":argv[3]}) return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData): s = decrypt(argv, eh) print("%s: %s" % (eh.hexString(address), s)) eh.analysisHelper.setComment(address, s, False)
if name == 'main':
eh = flare_emu.EmuHelper(samplePath=sys.argv[1])
eh.analysisHelper.setName(, "decryptString")
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
このスクリプトには2つの違いがあります。まず、`EmuHelper`コンストラクタはここでパラメータ `samplePath=sys.argv[1]` を受け取ります。`samplePath`パラメータが指定されると、`flare-emu` はバイナリ解析エンジンとして `r2pipe` を用いた Radare2 を使用します。また、`decrypt` 関数内で作成される2番目の `EmuHelper` インスタンスに2番目のパラメータが渡されていることがわかります。`emuHelper` パラメータは既存の `EmuHelper` オブジェクトを受け取り、新しいオブジェクトを作成する際にそのメモリを複製します。さらに、Radare2 を使用している場合、新しいインスタンスは既存の Radare2 セッションを再利用するため、新たに作成するよりもオーバーヘッドが少なくなります。2つ目の違いは、`flare-emu` が `r2pipe.open` を使用して新しい Radare2 インスタンスを作成するため、目的の関数に対して `decryptString` という名前が付けられていない可能性が高いことです。`EmuHelper` の `analysisHelper` オブジェクトを使用して自分で名前を設定することもできます(例: `eh.analysisHelper.setName(<some address>, "decryptString")`)。あるいは、`iterate` および `emulateRange` の呼び出しにアドレスを直接入力することもできます。
## [Emulation Functions](#emulationfuncs)
`emulateRange(startAddr, endAddr=None, registers=None, stack=None, instructionHook=None, callHook=None, memAccessHook=None, hookData=None, skipCalls=True, hookApis=True, strict=True, count=0)` - `startAddress` から `endAddress` まで(`endAddress` の命令は含まない)の命令範囲をエミュレートします。`endAddress` が `None` の場合、エミュレーションが開始された関数内で "return" タイプの命令に遭遇したときにエミュレーションを停止します。
* `registers` は、キーがレジスタ名、値がレジスタ値の辞書です。`flare-emu` によって作成された特別なレジスタ名(例: `arg1`, `arg2` など、`ret`, `pc`)を使用できます。
* `stack` は、`x86` の関数の引数のように、逆順でスタックにプッシュされる値の配列です。`x86` では、この配列の最初の値が関数呼び出しの戻りアドレスとして使用され、関数の最初の引数ではないことに注意してください。`flare-emu` は、`registers` および `stack` 引数で指定された値に従って、エミュレートされたスレッドのコンテキストとメモリを初期化します。これらの値のいずれかに文字列が指定された場合、その文字列はメモリ内の場所に書き込まれ、そのメモリへのポインタが指定されたレジスタまたはスタック位置に書き込まれます。
* `instructionHook` は、各命令がエミュレートされる前に呼び出されるように定義できる関数です。プロトタイプは次のとおりです: `instructionHook(unicornObject, address, instructionSize, userData)`。
* `callHook` は、エミュレーション中に "call" タイプの命令が検出されたときに呼び出されるように定義できる関数です。プロトタイプは次のとおりです: `callHook(address, arguments, functionName, userData)`。
* `hookData` は、フック関数で使用できるユーザー定義データを含む辞書です。エミュレーション全体でデータを永続化する手段です。`flare-emu` も独自の目的でこの辞書を使用するため、既に定義されているキーを定義しないように注意する必要があります。この変数は、Unicorn での命名に起因して、ユーザー定義のフック関数ではしばしば `userData` と名付けられます。
* `skipCalls` は、エミュレータが "call" タイプの命令をスキップし、それに応じてスタックを調整するようにします。デフォルトは `True` です。
* `hookApis` は、`flare-emu` がエミュレーション中に遭遇するより一般的なランタイムおよび OS ライブラリ関数の単純な実装を実行するようにします。これにより、`memcpy`、`strcat`、`malloc` などの関数の呼び出しを気にする必要がなくなり、デフォルトは `True` です。
* `memAccessHook` は、メモリが読み取りまたは書き込みのためにアクセスされたときに呼び出されるように定義できる関数です。プロトタイプは次のとおりです: `memAccessHook(unicornObject, accessType, memAccessAddress, memAccessSize, memValue, userData)`。
* `strict` は、`True`(デフォルト)に設定すると、分岐先をチェックして逆アセンブラが命令を期待することを確認します。そうでない場合は、分岐命令をスキップします。IDA Pro を使用している場合に `False` に設定すると、`flare-emu` はエミュレートする際に IDA Pro 内で命令を作成します(**注意して無効にしてください**)。
* `count` はエミュレートする命令の最大数で、デフォルトは `0`(無制限)です。
`iterate(target, targetCallback, preEmuCallback=None, callHook=None, instructionHook=None, hookData=None, resetEmuMem=False, hookApis=True, memAccessHook=None)` - `target` で指定された各ターゲットについて、含まれている関数の先頭からターゲットアドレスまで個別のエミュレーションが実行されます。エミュレーションは、各ターゲットに到達するために必要な分岐を強制的に実行します。`target` は関数のアドレスにすることができ、その場合、ターゲットリストは指定された関数へのすべてのクロスリファレンスで埋められます。または、`target` はターゲットの明示的なリストにすることもできます。
* `targetCallback` は、エミュレーション中に各ターゲットに到達したときに `flare-emu` によって呼び出される関数を作成します。プロトタイプは次のとおりです: `targetHook(emuHelper, address, arguments, userData)`。
* `preEmuCallback` は、各ターゲットのエミュレーションが開始される前に呼び出される関数を作成します。必要に応じて、ここでセットアップコードを実装できます。
* `resetEmuMem` は、各ターゲットのエミュレーションが開始される前に `flare-emu` がエミュレーションメモリをリセットするようにします。デフォルトは `False` です。
`iterateAllPaths(target, targetCallback, preEmuCallback=None, callHook=None, instructionHook=None, hookData=None, resetEmuMem=False, hookApis=True, memAccessHook=None, maxPaths=MAXCODEPATHS, maxNodes=MAXNODESEARCH)` - `target` のアドレスを含む関数について、発見された各パスに対して個別のエミュレーションが実行されます(最大 `maxPaths` まで)。
* `maxPaths` - 関数を通るパスの最大数で、検索およびエミュレートされます。より複雑な関数では、グラフ検索関数が非常に長い時間がかかるか、終了しない可能性があります。このパラメータを調整して、合理的な時間内にニーズを満たすようにしてください。
* `maxNodes` - ターゲット関数を通るパスを検索するときに検索される基本ブロックの最大数です。これは、不合理な検索時間やハングを防ぐための安全対策であり、おそらく変更する必要はありません。
`emulateBytes(bytes, registers=None, stack=None, baseAddress=0x400000, instructionHook=None, hookData=None)` - `bytes` に含まれるコードを可能であれば `baseAddress` のエミュレーションメモリに書き込み、`bytes` の先頭から末尾まで命令をエミュレートします。
`emulateFrom(startAddr, registers=None, stack=None, instructionHook=None, callHook=None, memAccessHook=None, hookData=None, skipCalls=True, hookApis=True, strict=True, count=0)` - この API は、関数の境界が明確に定義されていない場合(難読化されたバイナリやシェルコードでよくある)に便利です。`startAddr` として開始アドレスを指定すると、エミュレートするものがないか、フックの1つでエミュレーションを停止するまでエミュレートします。この API は `strict` パラメータを `False` に設定して呼び出すことで、動的なコード発見を有効にできます。`flare-emu` は、エミュレーション中に遭遇した命令を IDA Pro に作成させます。
## [Utility Functions](#utility)
以下は、`EmuHelper` クラスが提供する便利なユーティリティ関数の不完全なリストです。
* `hexString(value)` - 値の16進数形式の文字列を返します。ログ出力や print ステートメントに便利です。
* `skipInstruction(userData, useAnalysisHelper=False)` - エミュレーションフックからこれを呼び出して、現在の命令をスキップし、プログラムカウンタを次の命令に移動します。`useAnalysisHelper` オプションは、バイナリ解析フレームワークが複数の命令を1つの疑似命令に折りたたみ、それらすべてをスキップしたい場合に対処するために追加されました。この関数は単一の命令フックから複数回呼び出して複数の命令をスキップすることはできません。複数の命令をスキップするには、ARM コードをエミュレートしている場合、プログラムカウンタに直接書き込むことは避けることをお勧めします(サムモードで問題が発生する可能性があります)。代わりに、`EmuHelper` の `changeProgramCounter` API(後述)を試してください。
* `changeProgramCounter(userData, newAddress)` - エミュレーションフックからこれを呼び出して、プログラムカウンタレジスタの値を変更します。この API は ARM アーキテクチャのサムモード追跡を処理します。
* `getRegVal(registerName)` - 指定されたレジスタの値を取得し、サブレジスタアドレッシングに敏感です。例えば、`x86` の "ax" は EAX/RAX レジスタの下位16ビットを返します。
* `stopEmulation(userData)` - エミュレーションフックからこれを呼び出してエミュレーションを停止します。`emu_stop` Unicorn API を呼び出す代わりにこれを使用して、`EmuHelper` オブジェクトが `iterate` 機能に関連するブックキーピングを処理できるようにします。
* `getEmuString(address)` - エミュレートされたメモリ内のアドレスにある文字列(ヌル終端まで)を返します。文字は必ずしも印刷可能ではありません。
* `getEmuWideString(address)` - エミュレートされたメモリ内のアドレスにある「ワイド文字」の文字列(ヌル終端まで)を返します。ここでの「ワイド文字」は、UTF-16 LE でエンコードされた ASCII 文字列の場合のように、1バイトおきにヌルバイトを含む一連のバイトを大まかに指します。文字は必ずしも印刷可能ではありません。
* `getEmuBytes(address, length)` - エミュレートされたメモリ内のアドレスにあるバイト列を返します。
* `getEmuPtr(address)` - 指定されたアドレスにあるポインタ値を返します。
* `writeEmuPtr(address, value)` - 指定されたアドレスにポインタ値をエミュレートされたメモリに書き込みます。
* `loadBytes(bytes, address=None)` - エミュレータ内にメモリを割り当て、バイトを書き込みます。
* `isValidEmuPtr(address)` - 指定されたアドレスが有効なエミュレートされたメモリを指している場合、`True` を返します。
* `getEmuMemRegion(address)` - 指定されたアドレスを含むメモリ領域の開始アドレスと終了アドレスを含むタプルを返します。アドレスが有効でない場合は `None` を返します。
* `getArgv()` - "call" タイプの命令のエミュレーションフックからこれを呼び出して、関数の引数の配列を受け取ります。
* `addApiHook(apiName, hook)` - この `EmuHelper` インスタンスに新しい API フックを追加します。エミュレーション中に `apiName` への呼び出し命令が検出されると、`EmuHelper` は `hook` で指定された関数を呼び出します。`hook` が文字列の場合、それは `EmuHelper` によって既にフックされている API の名前であることが期待され、その場合、既存のフック関数を呼び出します。`hook` が関数の場合、その関数を呼び出します。
* `allocEmuMem(size, addr=None)` - `size` バイトを格納するのに十分なエミュレータメモリを割り当てます。要求された `アドレス` を尊重しようとしますが、既存のメモリ領域と重複する場合は、未使用のメモリ領域に割り当てて新しいアドレスを返します。`address` がページアラインされていない場合、新しい領域内で同じページアラインオフセットを維持するアドレスを返します。例えば、`0x1000` が既に割り当てられている場合にアドレス `0x1234` を要求すると、`0x2000` に割り当てられ、代わりに `0x2234` が返される可能性があります。
# [Learn More](#learn)
**flare-emu** の詳細については、以下の入門ブログをご覧ください: https://www.fireeye.com/blog/threat-research/2018/12/automating-objective-c-code-analysis-with-emulation.html.