
軽量な動的インストルメンテーションライブラリ
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
## TinyInstとは?
TinyInstは軽量な動的計装ライブラリであり、プロセス内の選択したモジュールのみを計装し、残りのプロセスはネイティブに実行させることができます。理解しやすく、ハックしやすく、ハックに使いやすいことを目的としています。すべてのターゲットと互換性があるように設計されているわけではありません(詳細は後述)。
### [DynamoRIO](https://dynamorio.org/)や[PIN](https://software.intel.com/en-us/articles/pintool)との比較は?
TinyInstは、DynamoRIOやPINのような複雑な計装フレームワークの代替としてではなく、より軽量なソリューションで十分なシナリオのための代替手段です。TinyInstは、ターゲットがwell-behaved(後述の意味で)であることを前提としていますが、これはより複雑なフレームワークでは当てはまりません。したがって、[以前DynamoRIOで行われたように](https://www.slideshare.net/MaximShudrak/fuzzing-malware-for-fun-profit-applying-coverageguided-fuzzing-to-find-bugs-in-modern-malware)、マルウェアに対してTinyInstを正常に実行できる可能性は低いでしょう。一方、計装する必要のないモジュールが原因で他のフレームワークで動作しないターゲットがあり、その計装対象モジュールがwell-behavedである場合、TinyInstでは動作する可能性があります。TinyInstではプロセスの大部分がネイティブに実行されるため、プロセスの起動時間が短くなり、ターゲットプロセスが計装を必要としないモジュールで多くの時間を費やす場合には、他のソリューションよりも優れたパフォーマンスを発揮する可能性があります。
### [Mesos](https://github.com/gamozolabs/mesos)や[TrapFuzz](https://github.com/googleprojectzero/p0tools/tree/master/TrapFuzz)との比較は?
TinyInstは完全なバイナリ書き換えソリューションであり、ターゲットモジュール内の任意の動作を変更できます。これにより、例えば基本ブロックだけでなくエッジカバレッジを抽出することが可能です。また、TinyInstは基本ブロックを特定するためにIDA Proなどの他のソフトウェアに依存しません。
### TinyInstはどのオペレーティングシステムをサポートしていますか?
TinyInstはWindows(x86およびx64)、macOS(x64およびARM64)、Linux(x64およびARM64)、Android(ARM64)で動作します。各オペレーティングシステムの追加の注意事項と制限については、対応するディレクトリのREADMEを参照してください。
### TinyInstと互換性のあるターゲットは?
TinyInstは、すべての計装対象モジュールが以下の意味でwell-behavedであると仮定しています:
- 自己修正コードがないこと
- スタック上のリターンアドレスがプログラムから直接アクセスされることがないこと
OR/AND(設定に依存)
- スタックの先頭より前(ESP/RSPが指すアドレスより低いアドレス)にデータが格納されることがないこと。この条件は、`-stack_offset`フラグを使用して「(ESP/RSP - arbitrary_offset)より前にデータがない」という条件に緩和できます。
TinyInstはまた、ターゲットプロセスでDEP/NXが有効になっている必要があります。そうでない場合は、`-force_dep`フラグを使用して強制的に有効にできます。ただし、ターゲットが正しく機能するためにDEPをオフにする必要があるという稀なケースでは、強制的に有効にすると誤動作を引き起こす可能性があります。
### パフォーマンスオーバーヘッドは?
画像デコードに関する初期の測定結果によると、デフォルトのTinyInst設定でwell-behavingな64ビットターゲットの場合、パフォーマンスオーバーヘッドはクライアントなしで約15%、カバレッジ収集のサンプルクライアントを使用すると約20%でした。これには、モジュールの初期計装によって生じるタイムアウトは含まれていないことに注意してください。詳細については、以下のパフォーマンスのヒントを参照してください。
## TinyInstのビルド
1. ターミナルを開き、ビルド環境を設定します(例:Windowsではvcvars64.bat / vcvars32.batを実行)
2. ソースを含むディレクトリに移動します
3. 以下のコマンドを実行します(ビルド対象のIDEのバージョンとプラットフォームに応じてジェネレータを変更してください)
#### Windows```
mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Release
mkdir build cd build cmake -G Xcode .. cmake --build . --config Release
#### Linux```
mkdir build
cd build
cmake ..
cmake --build . --config Release
mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE=</path/to/android/ndk>build/cmake/android.toolchain.cmake -DANDROID_NDK=</path/to/android/ndk> -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM= .. cmake --build . --config Release
注意点1: 64ビットビルドは、WindowsおよびLinuxオペレーティングシステム上で32ビットターゲットに対しても動作します。
注意点2: 64ビットWindows上で32ビットビルドを作成する際、環境が適切に設定されておらずライブラリが不足しているために問題が発生していますか? `cmake --build`を実行する代わりに、生成された.slnファイルをVisual Studioで開き、そこからビルドしてください。また、64ビットビルドは32ビットターゲットでも動作するため、32ビットビルドを作成する必要はないかもしれないことに注意してください。
## TinyInstの使用
TinyInstは主に、他のプログラム内でライブラリとして使用されることを意図しています。
TinyInstクライアントは、TinyInstクラスのサブクラスとして記述されます。クライアントは必要なAPIメソッドをオーバーライドできます。APIメソッドは以下で定義されています。
クライアントが作成された後、コマンドラインオプションを指定して初期化する必要があります。呼び出しは以下の通りです。
`void init(int argc, char **argv);`
コマンドラインオプションは以下で定義されており、クライアントは独自のオプションを定義することもできます。その後、インストルメントされたプログラムを実行および制御するために、以下の関数を使用できます。
`DebuggerStatus Run(int argc, char **argv, uint32_t timeout);`
`DebuggerStatus Attach(unsigned int pid, uint32_t timeout);`
これらの関数は、プログラムを実行するか(指定されたコマンドラインを使用)、既に実行中のプログラムにアタッチします。ターゲットメソッドが指定されていない場合、ターゲットはプログラムが終了するか、クラッシュするか、タイムアウト(ミリ秒単位で指定)が経過するまで実行を続けます。ターゲットメソッドが定義されている場合、TinyInstはターゲットメソッドが入力されたとき、およびターゲットメソッドが戻るときに戻り、呼び出し元が追加のタスクを実行できるようにします。
`Run`および`Attach`がターゲットプロセスがまだ生きている間に戻った場合、以下の関数を使用してプロセスを終了するか、実行を継続できます。
`DebuggerStatus Kill();`
`DebuggerStatus Continue(uint32_t timeout);`
TinyInstには、以下のように呼び出すことができるカバレッジバイナリの例が付属しています。
`<options> -- <target command line>`
Windowsでの例:
`litecov.exe -instrument_module notepad.exe -coverage_file coverage.txt -- notepad.exe`
## インストルメンテーションAPI
### デバッガイベントコールバック
これらのコールバックは情報提供のみを目的としており、クライアントはその間にインストルメントされたコードを出力すべきではありません。クライアントは、これらのイベントを自身で処理する前に、スーパークラスで定義された同じハンドラを呼び出す必要があります。
`OnProcessCreated`
ターゲットプロセスが作成されたか、アタッチされたときに呼び出されます。
`OnProcessExit`
ターゲットプロセスが終了したときに呼び出されます。
`OnProcessEntrypoint`
プロセス(メインバイナリ)のエントリポイントに到達したときに呼び出されます。
`OnTargetMethodReached`
ターゲットメソッドが定義されている場合、ターゲットメソッドに初めて到達したときに呼び出されます。
`OnModuleLoaded`
モジュールがロードされたときに呼び出されます。インストルメントされたモジュールだけでなく、すべてのモジュールに対して呼び出されます。
`OnModuleUnloaded`
モジュールがアンロードされたときに呼び出されます。インストルメントされたモジュールだけでなく、すべてのモジュールに対して呼び出されます。
`OnException`
例外が発生したときに呼び出されます。クライアントは、例外が処理された場合はtrueを返すか、親クラスの同じメソッドの結果を返す必要があります。
### インストルメンテーションコールバック
これらのコールバック中、クライアントは`WriteCode()`を呼び出すことでターゲットにコードを追加できます。クライアントは、挿入されたコードで破壊されるコンテキスト(レジスタやフラグなど)を保存および復元する責任があることに注意してください。
`InstrumentBasicBlock`
特定のベーシックブロックで実行されるコードを挿入するために使用できます。
`InstrumentEdge`
特定のエッジで実行されるコードを挿入するために使用できます。注意: パフォーマンス上の理由から、このコールバックは非決定的エッジ(条件付きジャンプなど)と間接ジャンプ/コール(例:`call rax`)でのみ発行されます。次のベーシックブロックが前のベーシックブロックから常に既知であるエッジ(例:`jmp offset`、`call offset`)では、コールバックは発行されません。
`InstrumentInstruction`
命令を変更したり、その前にコードを挿入するために使用できます。戻りコードに応じて、元の命令はコールバック後に出力されるか、されません。
### その他のコールバック
`OnModuleEntered`
別のモジュールからインストルメントされたモジュールに制御フローが移行したときに呼び出されます。
`OnModuleInstrumented`
モジュールがインストルメントされたときに呼び出されます。これは通常、プロセスエントリポイントに到達したとき(ターゲットメソッドが定義されていない場合)、またはターゲットメソッドに到達したとき(定義されている場合)に発生します。クライアントはここでインストルメンテーション関連のデータを初期化できます。
`OnModuleUninstrumented`
インストルメンテーションデータが無効になり、クリアする必要があるときに呼び出されます。これはモジュールがアンロードされたことと同じではないことに注意してください。デフォルトでは、インストルメンテーションはモジュールのアンロード/再ロードをまたいで持続します。このコールバックは、クライアント内のインストルメンテーション関連データをクリアするために使用できます。
### フックAPI
上記の汎用APIに加えて、TinyInstは個々の関数の動作を検査および変更するのに適したフッキングAPIも実装しています。このAPIは[別のページ](https://github.com/googleprojectzero/TinyInst/blob/master/hook.md)に文書化されています。
## コマンドラインオプション
### インストルメンテーション関連
`-instrument_module [module name]` インストルメントするモジュールを指定します。複数の`-instrument_module`オプションを指定して、複数のモジュールをインストルメントできます。
`-instrument_transitive [module name]` `-instrument_module`と似ていますが、他のインストルメントされたモジュールから入力されたコードのみがインストルメントされて実行されます。主にmodule1->module2->module1のような呼び出しの最適化として使用され、module2モジュール全体をインストルメントすることが重要ではなく、module2->module1のエントリが速度低下を引き起こしている場合に使用します。
`-indirect_instrumentation [none|local|global|auto]` 間接ジャンプ/コールに使用するインストルメンテーションを指定します。
`-patch_return_addresses` - リターンアドレスを元の値に置き換えます。指定された`-indirect_instrumentation`メソッドを使用してリターンがインストルメントされるようにします。
`-generate_unwind` - インストルメントされたコード用のスタックアンワインドデータを生成します(C++例外処理を高速化するため)。一部の古いWindowsバージョンでは正しく動作しない可能性があることに注意してください。
`-persist_instrumentation_data` (デフォルト = true) モジュールのアンロード/再ロード時にモジュールを再インストルメントしません。モジュールが以前ロードされたのと同じアドレスにロードされた場合にのみ機能します。
`-instrument_cross_module_calls` (デフォルト=true) 複数の`-instrument_module`モジュールが指定され、一方が他方を呼び出す場合、例外を発生させずに(速度低下の原因となる)他方のモジュールのインストルメントされたコードにジャンプします。
`-stack_offset` (デフォルト=0) スタックにコンテキストを保存するとき、スタックの先頭(スタックポインタの前)にこのバイト数を変更せずに残します。
`-patch_module_entries [off|data|code|all]` 以前に検出されたエントリポイントへのポインタを検索し、それらをインストルメントされた対応物に置き換えることにより、過剰なモジュールエントリによる速度低下を解決しようとします。フラグの値は、これらのポインタを検索する場所を制御します。警告: これを有効にすると、ターゲットに不安定性が生じる可能性があります。
### デバッグ関連
`-trace_debug_events` - デバッガイベント(モジュールのロード、例外など)を表示します。
`-trace_basic_blocks` - 実行されるベーシックブロックを表示します。
`-trace_module_entries` - インストルメントされたコードへのすべてのエントリを表示します。
`-trace_syscalls` - [Linux/Androidのみ] `OnSyscall()` / `OnSyscallEnd()`コールバックを介して、クライアントがシステムコールの開始/終了イベントを受信できるようにします。
`-full_address_map` - インストルメントされたコード内のアドレスから元のコード内のアドレスへの命令レベルのマップを維持します。メモリを多く消費しますが、デバッグに役立ちます。
### ターゲットメソッドと永続性
TinyInstでは、ユーザーがターゲットメソッドを定義できます。ターゲットメソッドが定義されている場合、ターゲットメソッドに初めて到達するまではコードはインストルメントされず(すべてネイティブで実行されます)。さらに、TinyInstはターゲットメソッドのエントリと終了で実行を中断します。
`-target_module` - ターゲットメソッドを含むモジュール
`-target_method` - ターゲットメソッドの名前。これは、ターゲットメソッドがエクスポートされているか、ターゲットモジュールのシンボルがある場合にのみ機能します。
`-target_offset` - ターゲットメソッドを名前で指定できない場合に使用します。モジュールベースからのターゲットメソッドの相対アドレス。
`-loop` - このフラグが指定されると、TinyInstはターゲットメソッドを無限ループで実行します(またはKill()が呼び出されるか、プロセスが別の理由で終了するまで)。関数の引数は反復間で保存および復元されます。これは主にファジングのために永続性を強制するために使用されます。
`-nargs` - 反復間で保存するターゲットメソッドの引数の数。`-loop`と一緒に使用します。
`-callcon [ms64|stdcall|fastcall|thiscall]` - ターゲットメソッドが使用する呼び出し規約。`-loop`と一緒に使用します。
### その他
`-target_env key=value` - [現在はmacOSおよびLinux/Androidのみ] ターゲットプロセスに渡す追加の環境変数を指定します。複数の`-target_env`オプションを指定して、複数の環境変数を渡すことができます。
`-force_dep` - [Windowsのみ] ターゲットプロセスでDEPを強制的に有効にします。
## カバレッジモジュール
TinyInstには(サンプル)カバレッジモジュール`LiteCov`が付属しています。このカバレッジモジュールは、ベーシックブロックまたはエッジカバレッジを収集できます(`-covtype`フラグで制御)。さらに、`-cmp_coverage`フラグを指定することで、「比較」カバレッジ(cmp/sub命令で一致するバイト数をカウント)を抽出することもできます。
カバレッジモジュールの特別な機能として、ターゲットプロセス内のカバレッジバッファは最初は読み取り専用として割り当てられ、新しいカバレッジが最初に検出されたときに例外を引き起こします。カバレッジの特定のサブセットを無視するオプションと組み合わせることで、特定の入力を使用してターゲットを実行した結果、新しいカバレッジが発生したかどうかを迅速に照会できます。
## TinyInstの仕組み
TinyInstはカスタムデバッガの上に構築されています。デバッガは、モジュールのロード、ブレークポイントのヒット、例外の発生などのイベントをターゲットプロセスで監視します。デバッガは、ターゲットメソッドが指定されている場合は、ブレークポイントと永続性も実装します。
インストルメントされるモジュールがロードされると、最初に次のように「インストルメント」されます。
- モジュール内のすべての実行可能領域は実行不可としてマークされ、他の権限(読み取り/書き込み)は元のまま保持されます。これにより、制御フローがインストルメントされたモジュールに到達するたびに例外が発生し、デバッガによってキャッチおよび処理されます。
- 元のモジュールアドレス範囲から2GB以内に実行可能なメモリ領域が割り当てられます。ここにモジュールのインストルメント/リライトされたコードが配置されます。2GBが重要なのは、[rip+offset]形式のアドレッシングを使用するすべての命令を[rip+fixed_offset]に置き換えることができるためです。
インストルメントされたモジュールに入るたびに(最初でもそれ以降でも)、ヒットしたベーシックブロックが、条件分岐や直接呼び出しおよびジャンプ(例:jmp offset、call offset)を再帰的にたどることで確実に発見できるすべてのベーシックブロックとともにインストルメントされます。
これでインストルメントされたコードを実行するのに十分な理由は次のとおりです。
- すべての直接ジャンプ/コールは、正しい場所にあるインストルメントされたコードに着地します。
- すべての間接ジャンプ/コール(例:call rax)は元のコードの場所に着地し、例外が発生します。デバッガは命令ポインタをインストルメントされたコードの対応する場所に置き換えることで解決します。
ただし、これで機能するものの、ターゲットがインストルメントされたモジュール内にある間接コール/ジャンプごとに例外が発生することに注意してください。例外処理は遅いため、間接性の多いターゲット(C++の仮想メソッド、関数ポインタなど)をインストルメントする場合、追加のインストルメンテーションなしでは遅くなります。
### 間接呼び出しとジャンプのインストルメント
TinyInstは間接呼び出しとジャンプをインストルメントして、(既に確認された)間接ターゲットでの例外を回避できます。インストルメントされた呼び出し/ジャンプは、元のターゲットにジャンプする代わりに、スタブのリンクリストの先頭にジャンプします。各スタブには(original_target, translated_target)のペアが含まれています。ジャンプ/コールターゲットがoriginal_targetと一致するかどうかをテストし、一致する場合は制御フローがtranslated_targetに転送されます。それ以外の場合は、次のスタブにジャンプします。リストの最後に達した場合、ジャンプ/コールターゲットがこれまでに見られていないことを意味します。これによりブレークポイントが発生し、デバッガによってキャッチされ、別のスタブを作成してリストに挿入することで解決されます。
このメカニズムは2つの方法で実装できます。
- コールサイトごとの(ローカル)リスト
- すべての間接ジャンプ/コールで使用されるグローバルハッシュテーブル
グローバルハッシュテーブルの方がパフォーマンスが優れています。ローカル(コールサイトごとのリスト)では、間接コール/ジャンプで正しいエッジ(正しいソースアドレス付き)を取得できます。
最新のWindowsでは、CFGのため、すべての間接ジャンプ/コールが同じ場所から発生するため、CFGでコンパイルされたバイナリでは、(何らかの特別な処理なしでは)正確なエッジを取得することは不可能です。これとパフォーマンス上の利点から、TinyInstでは間接呼び出し/ジャンプを処理するためのデフォルトの方法としてグローバルハッシュリストが採用されています。
### リターンアドレスのパッチ適用
デフォルトでは、インストルメントされたコードで呼び出しが行われると、書き込まれるリターンアドレスは*インストルメントされたコード*の次の命令になります。これはほとんどの場合正しく機能しますが、ターゲットプロセスがリターン以外の目的でリターンアドレスにアクセスする場合、問題が発生します。顕著な例として、64ビットオペレーティングシステムでの例外処理中のスタックアンワインドがあります。したがって、例外をキャッチする必要があるターゲットは、デフォルトのTinyInstでは正しく動作しません。
これはほとんどの場合、`-generate_unwind`フラグを追加することで解決できます。このフラグにより、TinyInstはターゲットプロセス用のスタックアンワインド/例外処理メタデータを生成および登録します。`-generate_unwind`は、UNWIND_INFOバージョン2が必要なため、一部の古いWindowsバージョンでは正しく動作しない可能性があることに注意してください。
TinyInstには、呼び出しが発生するたびにリターンアドレスを非インストルメントコード内の対応する値に書き換えるオプション(`-patch_return_addresses`フラグで公開)もあります。ただし、このオプションは、インストルメントされていないモジュールからインストルメントされたモジュールへの各リターン(後方エッジ)でコンテキストスイッチが発生するため、かなり大きなオーバーヘッドを導入することに注意してください。
## パフォーマンスのヒント
TinyInstの最大のオーバーヘッドは、インストルメントされていないモジュールからインストルメントされたモジュールに入るたびに例外がスローされることから生じます。これらの例外がトリガーされる様子は、`-trace_module_entries`フラグを使用して確認できます。間接ジャンプ/コールのインストルメンテーションは可能な限り使用し、リターンインストルメンテーションは可能な限り使用しないでください。TinyInstは、適度に自己完結しているモジュール(またはモジュールグループ)で最もパフォーマンスを発揮します。たとえば、2つのモジュールAとBがあり、AがBを頻繁に呼び出すがBのみがインストルメントされている場合、これにより大きな速度低下が発生します。AとBの両方をインストルメントすることで、より良いパフォーマンスを達成できます。
## デバッグのヒント
`-trace_basic_blocks`を使用して、実行されるベーシックブロックを確認します。インストルメントされたコード内のアドレスと、非インストルメントコード内の対応するアドレスの両方が表示されます。
クラッシュが発生したときに、OnException()コールバックを使用してプログラムの状態を調べます。
## 免責事項
これはGoogleの公式製品ではありません。