rev-frida について
動的解析向けに Frida スクリプトの作成方法を示すガイドで、ランタイム計測や検査に使えます。最新の Frida API を扱い、モジュール読み込み時のフック導入と初期化時フックの慎重な扱いを重視します。
主な機能
- 01ネイティブエクスポート関数のフックスクリプト生成
- 02Java と ObjC のメソッドフック
- 03引数、戻り値、実行トレースの取得
- 04メモリの読み書きとダンプ
- 05モジュール読み込みイベントでのフック導入
- 06初期化時フックのリスク説明
活用シーン
- 01アプリの実行時呼び出しとデータの検査
- 02対象ネイティブモジュールの読み込み後にフック
- 03Java または ObjC メソッドの動作解析
- 04コンストラクター実行時のアンチデバッグ調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ p4nda0s/ reverse-skills --skill rev-fridaインストール後、Claude Code で/rev-fridaを入力して読み込まれたか確認します。
詳細
rev-frida - Frida スクリプトジェネレーター
動的解析、フック、実行時の調査に使用する Frida インストルメンテーションスクリプトを生成します。
概要
Frida は、以下の用途に使用します。
- ネイティブエクスポートのフック
- Java または ObjC メソッドのフック
- 実行時のトレース
- 引数または戻り値の取得
- メモリダンプ
- ローダーを考慮したネイティブインストルメンテーション
重要:モダンな Frida CLI
モダンな Frida CLI では --no-pause は使用しません。プロセスはスクリプトの読み込み後に再開します。
# Spawn and hook
frida -U -f com.example.app -l hook.js
# Attach to running process
frida -U com.example.app -l hook.js
# Attach by PID
frida -U -p 1234 -l hook.js
モダンな API リファレンス
モジュールとシンボルの検索
const mod = Process.getModuleByName("libssl.so");
mod.name;
mod.base;
mod.size;
mod.path;
const ptr = mod.getExportByName("SSL_read");
Process.enumerateModules();
mod.enumerateExports();
mod.enumerateImports();
const addr = Module.getExportByName(null, "open");
Interceptor
Interceptor.attach(ptr, {
onEnter(args) {
console.log("arg0:", args[0].toInt32());
console.log("arg1 str:", args[1].readUtf8String());
},
onLeave(retval) {
console.log("ret:", retval.toInt32());
}
});
Interceptor.replace(ptr, new NativeCallback(function (a0, a1) {
console.log("replaced");
return 0;
}, "int", ["pointer", "int"]));
NativeFunction と NativeCallback
const open = new NativeFunction(
Module.getExportByName(null, "open"),
"int",
["pointer", "int"]
);
const fd = open(Memory.allocUtf8String("/etc/hosts"), 0);
const cb = new NativeCallback(function (arg) {
console.log("called with:", arg);
return 0;
}, "int", ["int"]);
メモリ操作
ptr(addr).readByteArray(size);
ptr(addr).readUtf8String();
ptr(addr).readU32();
ptr(addr).readPointer();
ptr(addr).writeByteArray(bytes);
ptr(addr).writeUtf8String("hello");
ptr(addr).writeU32(0x41414141);
const buf = Memory.alloc(256);
const str = Memory.allocUtf8String("hello");
Memory.scan(mod.base, mod.size, "48 89 5C 24 ?? 48 89 6C", {
onMatch(address, size) {
console.log("found at:", address);
},
onComplete() {}
});
ObjC
if (ObjC.available) {
const hook = ObjC.classes.ClassName["- methodName:"];
Interceptor.attach(hook.implementation, {
onEnter(args) {
const selfObj = new ObjC.Object(args[0]);
const param = new ObjC.Object(args[2]);
console.log(selfObj.toString());
console.log(param.toString());
}
});
}
Java
if (Java.available) {
Java.perform(function () {
const Activity = Java.use("android.app.Activity");
Activity.onCreate.implementation = function (bundle) {
console.log("onCreate called");
return this.onCreate(bundle);
};
});
}
スクリプト生成ガイドライン
Frida スクリプトを生成する際は、以下に従ってください。
Process.getModuleByName()やmod.getExportByName()など、常にモダンな API を使用します。--no-pauseは使用しません。- ポーリングではなく、ロードイベントをトリガーとしたネイティブフックを優先します。
- ポインターとバッファーは読みやすい形式で出力します。
- リスクのあるフックは
try/catchで囲みます。 - バイナリを調査するには
hexdump()を使用します。
ネイティブモジュールのロードタイミングに対応する
対象の .so がすでにロードされていると決めつけないでください。
推奨する順序:
android_dlopen_extまたはdlopenをフックし、対象ライブラリのロード時にフックを設定します。Process.findModuleByName()で即時にチェックし、ロード済みのモジュールを確認します。- フォールバックとしてのみポーリングを使用します。
デフォルトでは、以下のヘルパーを使用します。
function hookModuleLoad(moduleName, callback) {
const dlopen = Module.findGlobalExportByName("android_dlopen_ext")
|| Module.findGlobalExportByName("dlopen");
if (!dlopen) {
throw new Error("dlopen/android_dlopen_ext not found");
}
const hooked = new Set();
Interceptor.attach(dlopen, {
onEnter(args) {
this.path = args[0].isNull() ? null : args[0].readCString();
this.shouldHook = this.path && this.path.indexOf(moduleName) !== -1;
},
onLeave(retval) {
if (!this.shouldHook || retval.isNull()) {
return;
}
const mod = Process.findModuleByName(moduleName);
if (!mod) {
return;
}
const key = mod.base.toString();
if (hooked.has(key)) {
return;
}
hooked.add(key);
callback(mod);
}
});
}
次のように使用します。
hookModuleLoad("libtarget.so", function (mod) {
const target = mod.getExportByName("target_export");
Interceptor.attach(target, {
onEnter(args) {
console.log("target_export called");
}
});
});
対象がすでにロードされている可能性がある場合は、即時チェックとロードフックを組み合わせます。
function hookNowOrOnLoad(moduleName, callback) {
const mod = Process.findModuleByName(moduleName);
if (mod) {
callback(mod);
return;
}
hookModuleLoad(moduleName, callback);
}
フォールバックとしてのみポーリングを使用します。
function hookWhenReady(moduleName, exportName, callbacks) {
const mod = Process.findModuleByName(moduleName);
if (mod) {
Interceptor.attach(mod.getExportByName(exportName), callbacks);
return;
}
const timer = setInterval(function () {
const loaded = Process.findModuleByName(moduleName);
if (!loaded) {
return;
}
clearInterval(timer);
Interceptor.attach(loaded.getExportByName(exportName), callbacks);
}, 100);
}
注意:
- Android では、
dlopenよりもandroid_dlopen_extを優先します。 - パスだけでなく、モジュールのベースアドレスで重複を排除します。
dlopen/android_dlopen_extのonLeaveは、コンストラクターの実行後にフックを設定する適切なタイミングであることがほとんどです。- Java がネイティブのロードを制御している場合は、
System.loadLibrary、Runtime.loadLibrary0、dlsym、またはRegisterNativesも検討します。
init 系関数を無差別にフックしない
.init、.init_array、コンストラクター、または JNI_OnLoad を無差別にフックするようユーザーに提案しないでください。
これらは脆弱なポイントです。
- 多くのライブラリがここで一度限りの初期化処理を行います。
- 不適切なフックにより、本来の対象ロジックが実行される前にプロセスがクラッシュすることがあります。
- 早すぎるフックはタイミングを変化させ、ユーザーが調査したい挙動を隠すことがあります。
- コンストラクターのコードは、多数の無関係なヘルパー関数を呼び出すことがよくあります。
初期化段階のフックを提案する前に、以下を確認します。
- 早期フックが必要な理由を特定します。
- 対象モジュールと初期化ルーチンを正確に特定します。
- 対象のシンボルまたは挙動が、通常のエクスポートからアクセス可能になる前にしか存在しないか確認します。
- 同じ情報を得られるのであれば、より後の安定したフックを優先します。
以下の順序を優先します。
- モジュールのロード後に、安定したエクスポート関数をフックします。
RegisterNatives、dlsym、または最初の実際のビジネス関数をフックします。- ネイティブ登録またはアンチデバッグの設定がそこで行われる場合に限り、
JNI_OnLoadをフックします。 - 重要なロジックがコンストラクターまたは
.init_arrayにあることを示す強い根拠がある場合に限り、これらをフックします。
初期化段階の早期フックを提案する場合は、以下を明示します。
- 通常のエクスポートフックでは不十分な理由
- フックすべき正確な関数またはアドレス
- 想定される障害モード
- フックが初期化を妨げていないことを確認する方法
悪いアドバイス:
// do not suggest this without a reason
Interceptor.attach(Module.findBaseAddress("libtarget.so").add(0x1234), ...);
より良いアドバイス:
- モジュールのロードを待ちます。
- シンボル、クロスリファレンス、文字列、またはトレースの証拠によって、コンストラクターの対象を確認します。
- 正確な初期化関数を特定してからアタッチします。
- 初期化段階のフックを使用する前に、そのリスクをユーザーに説明します。
盲目的な init フックよりコンストラクタのディスパッチャーを優先する
コンストラクタチェーン内にアンチデバッグロジックがある疑いがある場合は、.init_array のエントリに手当たり次第アタッチするのではなく、まずディスパッチャーの監視またはフックを優先してください。
より高レベルなターゲットには、次のものがあります。
call_constructorscall_array- リンカー側のコンストラクターウォーカー
- コンストラクターテーブルを反復処理するアプリ側のラッパー関数
この方法がより安全な理由:
- 1つのフックでコンストラクターの実行順序全体を把握できます
- 終了またはアンチデバッグの設定の直前に、どのコンストラクターが実行されたかを確認できます
- 無関係な初期化コードを手当たり次第にパッチするのを減らせます
- まずコンストラクターのターゲットをログに記録し、後から問題のあるものだけにパッチできます
推奨ワークフロー:
- モジュールの読み込みをフックします
- コンストラクターの実行中にプロセスが終了するかどうかを確認します
- 終了する場合は、利用可能であれば
call_constructorsまたはcall_arrayをフックします - ディスパッチされる各コンストラクターのターゲットをログに記録します
- アンチデバッグチェックを実行するコンストラクターを正確に特定します
- そのコンストラクターをパッチまたはフックするか、内部の特定のアンチデバッグ分岐にパッチします
call_constructors または call_array を提案するのは、次の場合に限ります。
- 対象のプラットフォームまたはリンカービルドで、これらの関数が公開されている
- シンボル、トレース、または逆アセンブル結果から、実際に使用されていることが分かる
- コンストラクター実行時のアンチデバッグ動作を可視化する必要がある
次の制約について警告してください。
- これらの関数はローダーまたはリンカーの内部関数であり、名前は Android のバージョン、ベンダービルド、またはプラットフォームによって異なる場合があります
- エクスポートされていない場合があり、シンボルの復元やオフセットベースのアタッチが必要になることがあります
- 早い段階でフックするとすべてのライブラリの読み込みに影響する可能性があるため、ログの対象範囲を慎重に設定してください
- まず原因の特定に使用し、デフォルトで恒久的な回避策として使わないでください
適切な指針:
- 「ネイティブライブラリの初期化中にプロセスが終了します。まず、存在する場合は
call_constructorsやcall_arrayなどのコンストラクターディスパッチャーをフックしてコンストラクターのターゲットをログに記録し、その後、問題のあるコンストラクターを正確に特定してください。」
不適切な指針:
- 「すべての
.init_arrayエントリをフックし、クラッシュが止まるまでパッチしてください。」
ポインターとバッファーのログ記録
console.log(args[0]);
console.log(args[0].toString());
console.log(hexdump(args[0], {
offset: 0,
length: 64,
header: true,
ansi: false
}));