「Fridaの解析からアプリを保護する技術」の探求
ChillStack セキュリティ部の一瀬です。
オープンソースの動的解析ツールFridaは安定性/利便性の高さや機能の豊富さから各プラットフォームでのセキュリティ診断、マルウェア解析などの文脈では非常に重宝する解析ツールとなっています。
一方でこうした解析ツールを悪用する人間は少なからず存在し、アプリケーションクラックやゲームのチート行為などに悪用されることがあります。
そのため、特にユーザーの手元で動作するアプリケーションを提供する際は、解析ツールの悪用に対して対策が必要となる可能性があります。
本稿では、 Linux/Android 環境における Frida の内部的な原理まで踏み込みながら、Frida の解析からアプリを保護する「Anti-Frida」技術について考察します。
[入門]基本的な Anti-Frida 手法について
Anti-Frida 手法は、大きく「Frida に特化した防御手法」と「汎用的な防御手法」に大別されます。
それぞれの手法について、内容を解説していきます。
-
「Frida に特化した防御手法」の例
i. メモリ上にマッピングされた共有ライブラリの情報を全て取得
ii. 共有ライブラリのファイル名に"frida"が含まれていた場合、Frida に解析されていると判断 -
「汎用的な防御手法」の例
i. 子プロセスを生成し、親であるプロセス自身に子プロセスから ptrace でアタッチする
ii. ptrace では 2 つ以上のプロセスが同時にアタッチできないため、Frida からの解析を阻止出来る
※ 汎用的な防御手法を Anti-Frida に含めるかは定義によりますが、本稿では便宜上含めさせて頂きます。
【手法例 1】プロセスのメモリマップからの Frida 検知
/proc/[pid]/maps について
プロセスの状態の確認方法として、Linux/Android では/proc
ファイルシステムが利用可能です。
特に重要なのが/proc/[pid]/maps
又は/proc/self/maps
で、これはプロセスの仮想メモリマッピング情報を提供します。
/proc/[pid]/maps
は 読み取り対象のプロセスのプロセス ID を指定(プロセス自身も可能)し、/proc/self/maps
はプロセス自身のメモリマップを読み取る場合に利用します。
試しに実際の Android 端末に対して、adb コマンドを用いて shell に入り cat /proc/self/maps
コマンドを実行してみます。
すると、下記のような出力を得ることができます。(あくまで一例です)
$ cat /proc/self/maps
出力例:
...
79d34b7000-79d353e000 r-xp 00050000 07:30 24 /apex/com.android.runtime/lib64/bionic/libc.so
79d353e000-79d3547000 ---p 00000000 00:00 0
79d3547000-79d354b000 r--p 000e0000 07:30 24 /apex/com.android.runtime/lib64/bionic/libc.so
79d354b000-79d355a000 ---p 00000000 00:00 0
79d355a000-79d355c000 rw-p 000e3000 07:30 24 /apex/com.android.runtime/lib64/bionic/libc.so
79d355c000-79d39ae000 rw-p 00000000 00:00 0 [anon:.bss]
...
下記 1 行目の内容について、解説します。
79d34b7000-79d353e000 r-xp 00050000 07:30 24 /apex/com.android.runtime/lib64/bionic/libc.so
アドレス範囲
-
開始アドレス:
79d34b7000
-
終了アドレス:
79d353e000
この範囲は、プロセスの仮想アドレス空間内の連続したメモリ領域を示しています。この特定の領域は、ファイルシステム上のファイルがマッピングされているか、あるいは特別な用途(スタック、ヒープなど)に割り当てられています。
権限属性 (r-xp)
- r: 読み取り可能 (Readable)
- -: 書き込み不可 (Not Writable)
- x: 実行可能 (Executable)
- p: プライベートマッピング (Private mapping) - 変更は他プロセスに共有されない
マッピング情報
-
ファイルパス:
/apex/com.android.runtime/lib64/bionic/libc.so
(Android C ライブラリ)
ここまで、/proc/[pid]/maps
について具体例を交えながら紹介しました。
ここから、/proc/[pid]/maps
の情報を用いて Frida を検知する手法を紹介します。
frida-agent の検知
上記で取得したメモリマップを利用して、Frida の検知を実装してみます。
まず、適当な Android 端末のプロセス(PID:5432)に対して Frida でアタッチします。
$ frida -U -p 5432
この状態で、再度/proc/[pid]/maps
を読み取ります。
$ su # 権限が必要なためrootユーザーとして次のコマンドを実行する
# cat /proc/5432/maps | grep frida
70cff06000-70d0930000 r--p 00000000 00:01 3924 /memfd:frida-agent-64.so (deleted)
70d0931000-70d1669000 r-xp 00a2a000 00:01 3924 /memfd:frida-agent-64.so (deleted)
70d1669000-70d173a000 r--p 01761000 00:01 3924 /memfd:frida-agent-64.so (deleted)
70d173b000-70d1756000 rw-p 01832000 00:01 3924 /memfd:frida-agent-64.so (deleted)
/memfd:frida-agent-64.so
という Frida に関連するライブラリが見つかりました。
そのため、以下の流れで Frida を検知することが可能であると判断出来ます。
-
/proc/self/maps
からメモリ上にマップされた共有ライブラリを全て取得 - 共有ライブラリのファイル名に
frida
が含まれていた場合、frida にアタッチされていると判断
一般的な実装例
void check_frida_maps() {
// /proc/self/maps ファイルを読み取りモードで開く
FILE *fp = fopen("/proc/self/maps", "r");
char line[512];
// ファイルから1行ずつ読み込む
while(fgets(line, sizeof(line), fp)) {
// 行の中に "frida" という文字列が含まれているか確認
if(strstr(line, "frida")) {
// "frida" が見つかった場合、プロセスを終了する(終了コード 1)
exit(1);
}
}
fclose(fp);
}
【手法例 2】ptrace を用いた agent のインジェクションの防止
続いて、2 つ目の Anti-Frida 手法を紹介します。
Frida 解析の前段階として agent のインジェクションを行う際にptraceシステムコールを用いて対象プロセスにアタッチします。
Linux/Android では、こうした ptrace に対する防御策として、子プロセスを生成し、親である自分自身をアタッチさせるという手法が存在します。
- ptrace では、自分自身をアタッチできない
- 同一プロセスに一度にアタッチできるのは、一つのプロセスまで
- 子プロセスから親プロセスはアタッチできる
事前に子プロセスから親である自分自身をアタッチさせることにより、上記 2.の制約により他のプロセスからのアタッチをブロックできるという原理です。
こうした anti-ptrace の手法の詳細はOWASP MASTGの下記項目に記載されています。
OWASP MASTG Android Anti-Reversing Defenses
- 上記に記載されている anti-ptrace の実装例
void fork_and_attach()
{
int pid = fork();
if (pid == 0)
{
int ppid = getppid();
if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)
{
waitpid(ppid, NULL, 0);
/* Continue the parent process */
ptrace(PTRACE_CONT, NULL, NULL);
}
}
}
[深掘り] Frida のコードを分析し、Anti-Frida 手法を考えてみる
前項では一般的な手法を紹介しましたが、Frida のコードを分析した上で一般的な手法以外の Anti-Frida を考察してみます。
ここでは Frida 16.5.0 から実装されたsetHardwareBreakpoint / setHardwareWatchpointに対する防御手法を考えていきます。
まず Frida によってインジェクションされるエージェントは、frida-gumというプロジェクトに実装されています。
Linux/Android では対象プロセスのデバッグレジスタにアクセスする際には、必ず ptrace システムコールによるアタッチが必要です。
前項で解説した ptrace の自分自身にはアタッチできないという制約により、Frida は clone システムコールで子プロセスを生成してプロセス自身にアタッチさせるという手法を実装しています。
frida-gum/gum/backend-linux/gumprocess-linux.c
の関数である、gum_linux_modify_thread
の実装
こうした Frida の実装を踏まえて、対策手法を実装していきます。
seccomp による ptrace 呼び出しの阻止
子プロセスからの ptrace によるアタッチを阻止できれば、setHardwareBreakpoint / setHardwareWatchpoint は阻止できるはずです。
今回はseccomp(secure computing mode)という Linux/Android のセキュリティ機構を用いて、ptrace 呼び出しを無効化する手法を実装します。
- 一般的なアプリケーションで、ptrace システムコールは呼び出されない
- seccomp のフィルタモードを用いると、特定のシステムコールの実行を阻止できる
- clone システムコールの引数にCLONE_VMフラグを設定しているため、seccomp フィルターは子プロセスにも継承される
上記から、ptrace 呼び出しを完全に無効化する以下の filter を設定します。
struct sock_filter filter[] = {
/* システムコール番号をロードする */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
/* システムコール番号が__NR_ptrace(ptraceシステムコール)と一致するか確認 */
/* 一致したら次の命令へ(オフセット0)、一致しなければスキップ(オフセット1) */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_ptrace, 0, 1),
/* ptraceシステムコールの場合、ブロック */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
/* その他のシステムコールは許可(ALLOW) */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
// seccompフィルタモードを設定し、定義したフィルタを適用:省略
BPF (Berkeley Packet Filter) は、元々はネットワークパケット処理のために開発されたフィルタリング技術ですが、Linux カーネルでは seccomp の一部として拡張され、システムコールのフィルタリングにも使用されています。
実験と結果
seccomp のフィルタ の設定をした上で、以下の Watchpoint を設定するコードを実行します。
function installWatchpoint(address, size, conditions) {
// プロセスの最初のスレッドを取得
const thread = Process.enumerateThreads()[0];
// 例外ハンドラを設定
Process.setExceptionHandler((e) => {
// 現在のスレッドが監視対象スレッドで、かつ例外がブレークポイントまたはシングルステップの場合
if (
Process.getCurrentThreadId() === thread.id &&
["breakpoint", "single-step"].includes(e.type)
) {
// ハードウェアウォッチポイントを無効化
thread.unsetHardwareWatchpoint(0);
return true; // 例外を処理したことを示す
}
return false; // アプリケーションに例外を渡す
});
// ハードウェアウォッチポイントを設定
thread.setHardwareWatchpoint(0, address, size, conditions);
}
// メモリを確保
const addr = Memory.alloc(4); // 例: 4バイトのメモリを確保
// ウォッチポイントを設定(読み書き両方の操作を監視)
installWatchpoint(addr, 4, "rw");
seccomp のフィルタを設定した状態で、上記 JavaScript コードを実行するとSIGSYSが発生し 、 ptrace の呼び出しが seccomp のフィルタによってブロックされます。
また Frida を実行しているターミナルはタイムアウトが発生し、以下の状態になります。
$ frida -U sample -l set_watchpoint.js
...
...
Connected to Android Emulator 5554 (id=emulator-5554)
Failed to load script: timeout was reached
ptrace の呼び出しを無効化することで、Frida が対象プロセスに HardwareBreakpoint / HardwareWatchpoint を設定できない状態を実現することができました。
ここまで、Frida のコードを分析しながら、一般的な手法よりも一歩踏み込んだAnti-Fridaを実装を紹介しました。
考察
本稿で紹介させて頂いた手法は全て、Frida によって解析され所謂Anti-Anti-Fridaによって無効化される危険性が存在します。
例えば手法例:1 では strstr, fgets, exit 関数等を Frida のInterceptor.attachによってフッキングを行い、引数と戻り値を改変すれば無効化は容易に可能です。
クライアント側のセキュリティ対策は攻撃者とのイタチごっこになりがちですが、単一でない複合的な対策を積み重ねることで解析が困難なレベルに高めることは可能であると考えます。
まとめ
私自身は、原理を学ぶことで表面的な部分でなく深いレベルでの対策を考えられるようになると考え日々研究を行なっています。
またこうした対策手法を日頃から考察しておくことは、脆弱性診断を行う際にも分析力の強化の意味で非常に有効であると感じています。
本稿が読者の皆様の Frida やセキュリティ対策の知見の向上に繋がれば幸いです。
著者は Frida の機能改善も行なっている。こうした活動は別記事で紹介します!
最後に、ChillStack では現在エンジニアを積極採用中です!
「今は転職する気はないけど、ちょっと興味を持ったので話を聞いてみたい」という方も歓迎ですので、ご興味ある方お待ちしてます!
-
公式 Note
https://note.com/chillstack
Discussion