Android/iOS SSL Pinning実装:Fridaによる回避攻撃をネイティブレベルで防御する方法

金融系アプリや機密情報を扱うモバイルアプリケーションにおいて、単なる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.initOkHostnameVerifier.verifyをフックし、検証結果を強制的にtrueにする。これにより、AndroidセキュリティおよびiOSセキュリティの標準的な防御壁は無力化される。

したがって、我々が実装すべきは「証明書の固定(Pinning)」と「フック検知(Anti-Frida)」の二段構えの防御策である。詳細な脅威モデルについてはOWASP MASTGを参照されたい。

The Solution: 実装と防御コード

ここでは、最も一般的かつ強力なライブラリを使用したSSL Pinningの実装と、JNI/Native層でのFrida防御ロジックを示す。

1. 堅牢なSSL Pinningの実装

AndroidではOkHttpCertificatePinnerを使用し、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;
}
Note: ネイティブコードでの検知は強力だが、高度な攻撃者はlibcの関数(openread)自体をフックして/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