AndroidアプリでJNI APIをトレースするためのFRIDAベースのツール。
Androidアプリに含まれるネイティブライブラリは、多くの場合、JNI APIを使用してAndroidランタイムを利用します。手動のリバースエンジニアリングを介してこれらの呼び出しを追跡することは、ゆっくりと痛みを伴うプロセスになる可能性があります。 jnitrace 、Frida-TraceやStraceに似た動的分析トレースツールとして機能しますが、JNIには機能します。
jnitraceで実行する最も簡単な方法は、PIPを使用してインストールすることです。
pip install jnitrace
PIPインストール後、 jnitrace簡単に実行できます。
jnitrace -l libnative-lib.so com.example.myapplication
jnitraceは、トレースを実行するには、最低2つのパラメーターが必要です。
-l libnative-lib.so -lib.so-は、ライブラリを指定するために使用されます。この引数は複数回使用できます。または*使用してすべてのライブラリを追跡できます。たとえば、 -l libnative-lib.so -l libanother-lib.soまたは-l * 。com.example.myapplicationトレースするAndroidパッケージです。このパッケージは既にデバイスにインストールする必要があります。オプションの引数を以下に示します。
-R <host>:<port> - リモートFRIDAサーバーのネットワーク位置を指定するために使用されます。 a:が不特定の場合、localhost:27042は難聴によって使用されます。-m <spawn|attach> - 使用するFRIDAアタッチメカニズムを指定するために使用されます。スポーンまたはアタッチすることができます。スポーンはデフォルトおよび推奨されるオプションです。-b <fuzzy|accurate|none> - バックトレース出力を制御するために使用されます。デフォルトでは、 jnitrace accurateモードでバックトレーサーを実行します。このオプションは、 fuzzyモードに変更するか、 noneオプションを使用してバックトレースを停止するために使用できます。違いについての説明については、Frida Docsを参照してください。-i <regex> - トレースする必要のあるメソッド名を指定するために使用されます。これは、特に大規模なJNIアプリでノイズを減らすのに役立ちます。オプションは複数回提供できます。たとえば、 -i Get -i RegisterNativesは、名前にgetまたはregisterNativesを含むJNIメソッドのみが含まれます。-e <regex> - トレースで無視する必要があるメソッド名を指定するために使用されます。これは、特に大規模なJNIアプリでノイズを減らすのに役立ちます。オプションは複数回提供できます。たとえば、 -e ^Find -e GetEnv結果からgetEnvを見つけた、または封じ込めているすべてのJNIメソッド名を除外します。-I <string> - トレースする必要があるライブラリからのエクスポートを指定するために使用されます。これは、少数の方法のみを追跡するライブラリに役立ちます。 Jnitraceがエクスポートした関数は、Java側から直接呼び出す可能性のある関数、そのため、登録感を使用してバインドされた方法を含む機能です。オプションは複数回提供できます。たとえば、 -I stringFromJNI -I nativeMethod([B)V nativeMethod([B)Vして、 Java_com_nativetest_MainActivity_stringFromJNIと呼ばれるライブラリからのエクスポートを含めることができます。-E <string>は、トレースされてはならないライブラリからのエクスポートを指定するために使用されます。これは、無視したい忙しいネイティブコールのグループがあるライブラリに役立ちます。 Jnitraceがエクスポートした関数は、Java側から直接呼び出す可能性のある関数、そのため、登録感を使用してバインドされた方法を含む機能です。オプションは複数回提供できます。たとえば、 -E JNI_OnLoad -E nativeMethod 、 JNI_OnLoad関数呼び出しと名前nativeMethodの任意のメソッドをトレースから除外します。-o path/output.json jnitraceすべてのトレースデータを保存する出力パスを指定するために使用されます。情報はJSON形式で保存され、後でトレースデータの後処理が可能になります。-p path/to/script.js提供されるパスは、 jnitraceスクリプトがロードされる前に、FRIDAスクリプトをターゲットプロセスにロードするために使用されます。これは、 jnitraceが開始する前に、抗フリダまたは反開発コードを倒すために使用できます。-a path/to/script.js提供されたパスは、 jnitraceがロードされた後にFRIDAスクリプトをターゲットプロセスにロードするために使用されます。--hide-dataコンソールに表示される出力の量を減らすために使用されます。このオプションは、hexdumpsまたは文字列の参照として表示される追加データを非表示にします。--ignore-envこのオプションを使用すると、Jnienv structを使用してアプリが作成しているすべての呼び出しを非表示にします。--ignore-vmこのオプションを使用すると、Javavm structを使用してアプリが作成しているすべての呼び出しを非表示にします。--aux <name=(string|bool|int)value> - アプリケーションを産むときにカスタムパラメーターを渡すために使用されます。たとえば--aux='uid=(int)10'デフォルトのユーザー0ではなく、ユーザー10のアプリケーションを生成します。注記
jnitrace実行する前にFrida-Serverが走っている必要があることを忘れないでください。 Fridaをインストールするためのデフォルトの指示が守られている場合、次のコマンドはjnitraceの準備ができているサーバーを起動します。
adb shell /data/local/tmp/frida-server
Jnitraceを動かすエンジンは、別のプロジェクトとして利用できます。このプロジェクトを使用すると、Jnitraceをインポートして、FRIDA Interceptorを使用して関数とアドレスに接続することに慣れた方法で、個々のJNI APIコールを追跡できます。
import { JNIInterceptor } from "jnitrace-engine" ;
JNIInterceptor . attach ( "FindClass" , {
onEnter ( args ) {
console . log ( "FindClass method called" ) ;
this . className = Memory . readCString ( args [ 1 ] ) ;
} ,
onLeave ( retval ) {
console . log ( "tLoading Class:" , this . className ) ;
console . log ( "tClass ID:" , retval . get ( ) ) ;
}
} ) ;詳細:https://github.com/chame1eon/jnitrace-engine
ソースからjnitraceを構築するには、最初にnodeをインストールする必要があります。 nodeをインストールした後、次のコマンドを実行する必要があります。
npm installnpm run watch npm run watchバックグラウンドでfrida-compile実行して、ソースを出力ファイルにコンパイルしますbuild/jnitrace.js 。 jnitrace.py build/jnitrace.jsからのロードはデフォルトであるため、更新を実行するために他の変更は必要ありません。
Frida-Traceと同様に、出力はAPIコールスレッドに基づいて色付けされます。
ディスプレイのスレッドIDのすぐ下には、JNI APIメソッド名があります。メソッド名は、 jni.hヘッダーファイルで見られるものと正確に一致します。
後続の行には、a |-によって示される引数のリストが含まれています。 |-文字の後、引数タイプの後に引数値が続きます。 JMethodsの場合、JFieldsとJClassesの場合、JavaタイプはCurly Bracesに表示されます。これは、 jnitrace元の方法、フィールド、またはクラスの検索を見たことに依存しています。バッファーを渡すすべてのメソッドについて、 jnitrace引数からバッファーを抽出し、引数値の下のヘクスダンプとして表示します。
リストの下部に戻り値が| |=で表示され、voidメソッドには存在しません。
バックトレースが有効になっている場合、メソッドコールの下にFrida Backtraceが表示されます。 Frida Docsによると、ファジーなバックトレースは必ずしも正確ではなく、正確なバックトレースが限られた結果をもたらす可能性があることに注意してください。
このプロジェクトの目標は、ほとんどのAndroidアプリケーションにJNI API呼び出しを効率的にトレースできるツールを作成することでした。
残念ながら、Jnienv構造のすべての機能ポインターに接続するという最も単純なアプローチは、アプリケーションに過負荷になります。 libart.soで同じ関数を使用して、他の無関係なライブラリによって作成された機能呼び出しの数に基づいてクラッシュを引き起こします。
そのパフォーマンスの障壁に対処するために、 jnitrace 、追跡したいライブラリに提供できるシャドウJnienvを作成します。 Jnienvには、Jni APIがいくつかのカスタムFrida Nativecallbacksを介して呼び出している一連の関数トランポリンが含まれており、それらの関数の入力と出力を追跡します。
ジェネリックFrida APIは、これらの機能トランポリンを最小限の労力で構築するためのプラットフォームを提供するという素晴らしい仕事をしています。ただし、その単純なアプローチは、Jnienv APIのすべてでは機能しません。すべての方法をトレースする際の重要な問題は、APIでのバリアジック引数の使用です。呼び出されるJavaメソッドのすべての異なる組み合わせが事前に知られていないため、これらの機能のNativecallbackを事前に作成することはできません。
解決策は、実行時からメソッド識別子を検索するために使用されるGetMethodIDまたはGetStaticMethodIDへの呼び出しのプロセスを監視することです。 jnitrace jmethodID Lookupを見ると、メソッドシグネチャへのIDのマッピングが既知のマッピングがあります。その後、JNI Javaメソッドコールが作成されると、最初のNativeCallbackを使用して、コールでメソッドIDを抽出します。そのメソッドの署名は、メソッド引数を抽出するために解析されます。 jnitraceメソッドの引数を抽出すると、そのメソッドのNativecallbackを動的に作成できます。新しいNativecallbackが返され、アーキテクチャ固有のシェルコードが少し扱われ、スタックとレジスタのセットアップを扱い、その呼び出しが正常に実行されるようにします。特定の方法のこれらのnativecallbackは、複数回呼び出された場合、コールバックをより効率的に実行できるようにキャッシュされます。
単純なNativecallbackがメソッドコールから引数を抽出するのに十分ではないもう1つの場所は、最終的な引数としてVA_ARGSポインターを使用するコールです。この場合、 jnitraceいくつかのコードを使用して、提供されたポインターから引数を抽出します。繰り返しますが、これはアーキテクチャ固有です。
これらの関数呼び出しでトレースされているすべてのデータは、ユーザーにフォーマットおよび表示するPythonコンソールアプリケーションに送信されます。
このツールのほとんどのテストは、Marshmallowを実行しているAndroid X86_64エミュレーターで行われています。別のデバイスで実行された問題は問題を提出してください。また、可能であれば、同様のエミュレータで実行してみることをお勧めします。
jnitraceを実行している問題については、GitHubで問題を作成してください。提出された問題に次の情報を含めてください。