金融系アプリや機密情報を扱うモバイルアプリケーションにおいて、単なるHTTPS通信だけではセキュリティとして不十分だ。Charles ProxyやBurp Suiteを使えば、中間者攻撃(MITM)は容易に成立してしまう。我々のチームが直面したのは、単純なSSL Pinningを実装しても、攻撃者がFridaを用いてSSL検証ロジックをランタイムでフックし、無効化してしまうという現実だった。本稿では、堅牢な証明書ピニングの実装と、それを回避しようとするFridaスクリプトをネイティブレベルで検知・遮断する具体的な手法を共有する。
なぜSSL Pinningだけでは突破されるのか
モバイルセキュリティの現場では、「いたちごっこ」が常態化している。AndroidのNetworkSecurityConfigやiOSのInfo.plist設定によるピニングは、OSレベルの信頼ストアに依存しているため、ルート化された端末では比較的容易にバイパスされる。
特に脅威となるのが、動的解析ツールであるFridaだ。攻撃者はアプリの再コンパイルなしに、メモリ上の関数を書き換えることができる。例えば、以下のようなJavaScriptコードを注入されるだけで、多くのJava製SSL検証ロジックは「常にTrue」を返すように改変されてしまう。
攻撃者は
SSLContext.initやOkHostnameVerifier.verifyをフックし、検証結果を強制的にtrueにする。これにより、AndroidセキュリティおよびiOSセキュリティの標準的な防御壁は無力化される。
したがって、我々が実装すべきは「証明書の固定(Pinning)」と「フック検知(Anti-Frida)」の二段構えの防御策である。詳細な脅威モデルについてはOWASP MASTGを参照されたい。
The Solution: 実装と防御コード
ここでは、最も一般的かつ強力なライブラリを使用したSSL Pinningの実装と、JNI/Native層でのFrida防御ロジックを示す。
1. 堅牢なSSL Pinningの実装
AndroidではOkHttpのCertificatePinnerを使用し、iOSではURLSessionDelegateを使用する。設定ファイルではなくコードで実装することで、静的解析への耐性を高める。
// [Android] OkHttpによるPinning (Kotlin)
val certificatePinner = CertificatePinner.Builder()
.add("api.yourdomain.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
// [iOS] URLSessionによるPinning (Swift)
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 公開鍵のハッシュを取得し、ハードコードされたハッシュと比較するロジックをここに記述
if isValid(serverTrust) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
// 攻撃検知:即座にセッションを切断しログを送信
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
2. Native層でのFrida検知 (Android JNI)
Java層の検知ロジックはFrida自身によって容易にフックされる。そのため、C/C++を用いたネイティブコードで/proc/self/mapsをスキャンし、Frida関連のライブラリ(frida-agent.soなど)がロードされていないかを確認する手法が有効だ。
// [Native] detect_frida.c
#include <jni.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Fridaの痕跡を探す関数
int check_frida_traces() {
FILE *fp = fopen("/proc/self/maps", "r");
if (fp == NULL) return 0;
char line[512];
while (fgets(line, 512, fp)) {
// "frida"を含むライブラリがロードされているかチェック
if (strstr(line, "frida-agent") || strstr(line, "frida-server")) {
fclose(fp);
return 1; // 検知
}
}
fclose(fp);
// デフォルトのFridaサーバーポート(27042)のチェックも推奨される
return 0;
}
JNIEXPORT jboolean JNICALL
Java_com_example_security_NativeLib_isFridaDetected(JNIEnv *env, jobject thiz) {
if (check_frida_traces()) {
// 検知時はクラッシュさせるか、ダミーデータを返す
return JNI_TRUE;
}
return JNI_FALSE;
}
libcの関数(openやread)自体をフックして/proc/self/mapsの内容を偽装する場合がある。これに対抗するには、システムコールを直接発行するインラインアセンブラの使用を検討すべきだ。
防御効果の検証
実装後のアプリに対し、一般的なFridaスクリプト(例:codeshare/universal-ssl-pinning-bypass)を実行した際の結果を以下に示す。
| 防御手法 | 攻撃ツール | 結果 | 攻撃者の視点 |
|---|---|---|---|
| 標準HTTPS | Burp Suite (Cert install) | 突破 | 通信内容が平文で見える |
| SSL Pinning (Javaのみ) | Frida Script | 突破 | SSL検証関数がバイパスされる |
| Pinning + Native検知 | Frida Script | 防御成功 | アプリが即座に終了、または通信不能になる |
Conclusion
モバイルアプリのセキュリティにおいて、絶対的な安全は存在しない。しかし、SSL Pinningとネイティブ層でのFrida防御を組み合わせることで、攻撃のコストを劇的に高めることができる。重要なのは、Java/Swift層だけでなく、C/C++層を含めた多層防御(Defense in Depth)を構築することだ。さらなる強化には、商用の難読化ツール(ProGuard/R8以上のもの)の導入を推奨する。
Post a Comment