Chromiumの新しい証明書有効期間要件下でのAndroid WebViewトラフィックのインターセプト

Chromiumの新しい証明書有効期間要件下でのAndroid WebViewトラフィックのインターセプト

原文: Intercepting Android WebView traffic under new certificate validity requirement by Chromium
著者: Jeroen Beckers (NVISO) | モバイルセキュリティ | 8分


最近のセキュリティ診断において、AndroidアプリのWebViewから発信されるトラフィックをインターセプトできない事象に遭遇しました。アプリは証明書のピニングを使用しておらず、通常のインターセプトを阻むような特別な設定もされていませんでした。同じアプリを別のデバイスでテストしたところ、トラフィックは直接インターセプトできたため、問題はアプリではなくデバイス側にあると判断しました。

TL;DR: Chromiumは現在、Burpが生成する証明書よりも短いリーフ証明書の有効期間を強制しています。これにより証明書検証が失敗します。PortSwiggerがBurpで証明書の有効期間を設定可能にするまで、最も簡単な解決策はデバイスからWebViewのアップデートを削除し、古い証明書検証ロジックに戻すことです。その他の方法は記事の最後に記載しています。

テスト用アプリの作成

まず、WebサイトにWebViewコンポーネントで読み込み、10秒ごとにリフレッシュする非常に基本的なテストアプリを作成しましょう。

public class MainActivity extends AppCompatActivity {
    private final Handler handler = new Handler(Looper.getMainLooper());
    private WebView webView;
    private final Runnable refreshRunnable = new Runnable() {
        @Override
        public void run() {
            if (webView != null) {
                webView.loadUrl("https://nviso.eu");
            }
            handler.postDelayed(this, 10000);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = findViewById(R.id.webview);
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        handler.post(refreshRunnable);
    }
}

トラフィックをインターセプトしない場合、アプリは正常に動作し、NVISOのメインWebサイトを表示します。しかし、Burp Suiteをインターセプトプロキシとして設定すると、WebViewはページをロードできなくなります。

WebViewが黒くなるのに加え、Burp Suiteの典型的なプロキシエラーが表示され、TLSの問題であることが確認できます。

典型的な「クライアントがTLS接続をネゴシエートできなかった… unknown_ca」エラー

さらに、logcatにもエラーメッセージが出力されています(アプリ自体から出力されたものではありません):

07-07 16:49:14.759 3347 8552 E chromium: [0707/164914.759550:ERROR:net/socket/ssl_client_socket_impl.cc:944] handshake failed; returned -1, SSL error code 1, net_error -213

証明書の有効期間の問題

このエラーは、WebViewコンポーネントのレンダリングエンジンであるChromiumによってスローされます。エラー番号 -213CERT_VALIDITY_TOO_LONG です。

// The certificate claimed DNS names that are in violation of name constraints.
NET_ERROR(CERT_NAME_CONSTRAINT_VIOLATION, -212)
// The certificate's validity period is too long.
NET_ERROR(CERT_VALIDITY_TOO_LONG, -213)
// Certificate Transparency was required for this connection, but the server
// did not provide CT information that complied with the policy.
NET_ERROR(CERTIFICATE_TRANSPARENCY_REQUIRED, -214)

このエラーは、Chromiumが証明書を検証する際に cert_verify_proc でスローされます:

// Flag certificates using too long validity periods.
if (verify_result->is_issued_by_known_root && HasTooLongValidity(*cert)) {
    verify_result->cert_status |= CERT_STATUS_VALIDITY_TOO_LONG;
    if (rv == OK)
        rv = MapCertStatusToNetError(verify_result->cert_status);
}

HasTooLongValidity の内部ロジックを見てみましょう:

bool CertVerifyProc::HasTooLongValidity(const X509Certificate& cert) {
    base::Time start = cert.valid_start();
    base::Time expiry = cert.valid_expiry();
    if (start.is_max() || start.is_null() || expiry.is_max() ||
        expiry.is_null() || start > expiry) {
        return true;
    }
    // The maximum lifetime of publicly trusted certificates has reduced
    // gradually over time.

    // 現在の制限(CABF Baseline Requirements v2.1.7より)
    static constexpr base::Time kTime_2029_03_15 = ...;
    static constexpr base::Time kTime_2027_03_15 = ...;
    static constexpr base::Time kTime_2026_03_15 = ...;

    // 2029年3月15日以降に発行された証明書: 最大47日
    if (start >= kTime_2029_03_15) {
        return validity_duration > base::Days(47);
    }
    // 2027年3月15日以降に発行された証明書: 最大100日
    if (start >= kTime_2027_03_15) {
        return validity_duration > base::Days(100);
    }
    // 2026年3月15日以降に発行された証明書: 最大200日
    if (start >= kTime_2026_03_15) {
        return validity_duration > base::Days(200);
    }
    // 現在の制限(Chrome Root Certificate Policy)
    // 2020年9月1日以降に発行された証明書: 最大398日
    return validity_duration > base::Days(398);
}

証明書有効期間のロジックは、コード内のドキュメントで明確に説明されており、評価されるすべてのリーフ証明書に適用されます。

証明書の発行日 最大有効期間
2012-07-01〜 60ヶ月
2015-04-01〜 39ヶ月
2018-03-01〜 825日
2020-09-01〜 398日
2026-03-15〜 200日
2027-03-15〜 100日
2029-03-15〜 47日

つまり、2026年3月15日以降に発行された証明書について、Chromiumはリーフ証明書の有効期間を最大200日に制限するようになりました。 Burp Suiteが提供する証明書を簡単なスポットチェックで確認してみましょう。

$ openssl s_client \
  -proxy 127.0.0.1:8080 \
  -connect nviso.eu:443 \
  -servername nviso.eu \
  -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -startdate -enddate

subject=C = BE, O = NVISO, OU = SSA, CN = nviso.eu
issuer=C = BE, ST = Some-State, O = NVISO, OU = SSA, CN = NVISO.eu
notBefore=Jun 23 14:40:01 2026 GMT
notAfter=Jun 23 14:40:01 2027 GMT

証明書のデフォルト有効期間は1年で、新しいルールでは許可されなくなっています。一方、Burp Suiteの代わりにmitmproxyでプロキシすると、インターセプトは成功します。

接続が成功する理由は、mitmproxyが生成する証明書の有効期間がすでに短いためです:

$ openssl s_client \
  -proxy 127.0.0.1:9090 \
  -connect nviso.eu:443 \
  -servername nviso.eu \
  -showcerts </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -startdate -enddate

subject=CN = nviso.eu
issuer=C = BE, ST = Some-State, O = NVISO, OU = SSA, CN = NVISO.eu
notBefore=Jul  6 08:55:01 2026 GMT
notAfter=Jan 21 08:55:01 2027 GMT

問題の解決策

簡単な順に、いくつかの解決策があります:

  1. デバイスからChromiumのアップデートをアンインストールする
  2. mitmproxyとBurp Suiteを組み合わせる
  3. libwebviewchromiumライブラリにパッチを当てる

PortSwiggerにはすでに連絡済みで、証明書の有効期間を設定可能にすることを期待しています。meantime、各オプションを見ていきましょう。

デバイスからChromiumのアップデートをアンインストールする

最も簡単な方法です。Google Playストアで「Android System WebView」コンポーネントを検索するか、以下のadbコマンドで実行できます:

$ adb shell am start -a android.intent.action.VIEW \
    -d "market://details?id=com.google.android.webview"

「アップデートを削除」をクリックするだけで元のバージョンにフォールバックできます。

WebViewコンポーネントのバージョンは dumpsys webviewupdate で確認できます:

# ダウングレード前: バージョン149
$ adb shell dumpsys webviewupdate
Current WebView package (name, version): (com.google.android.webview, 149.0.7827.159)

# ダウングレード後: バージョン113
$ adb shell dumpsys webviewupdate
Current WebView package (name, version): (com.google.android.webview, 113.0.5672.61)

mitmproxyとBurp Suiteを組み合わせる

mitmproxyはすでに正しい証明書を生成しているため、mitmproxyとBurp Suiteをチェーンさせて、デバイスからmitmproxyにトラフィックを送り、mitmproxyがBurp Suiteに転送するようにできます。これはアップストリームプロキシと呼ばれ、--mode upstream で設定できます:

./mitmproxy --listen-port 9090 \
    --set confdir="$(pwd)/mitmproxy-conf" \
    --mode upstream:http://localhost:8080 \
    --ssl-insecure

注意: mitmproxyがBurp Suiteの自己署名証明書を受け入れるよう、--ssl-insecure が必要です。

このセットアップでは、Burp Suiteはポート8080、mitmproxyはポート9090でリッスンし、デバイスはmitmproxyをプロキシとして使用するように設定します。Burp Suiteとmitmproxyの両方がリクエストをインターセプトできます。

libwebviewchromiumライブラリにパッチを当てる

可能ですが、正しい cert_verify_proc 関数を見つけるのは難しいです。インストールされているバージョン(149.0.7827.159)では、HasTooLongValidity 関数が cert_verify_proc にインライン化されており、オフセット 0x3e015f8 にあります。

デフォルトではWebViewはマルチプロセスモードで実行されるため、アプリ自体ではなく webview_zygote にインジェクトする必要があります。これは少しトリッキーで、エラーが発生するとシステム上のすべてのWebViewが破損する可能性があります。代わりに、WebViewをマルチプロセスサポートなしで実行するように設定することで、アプリ自体にインジェクトして検証を無効化できます:

# マルチプロセスからシングルプロセスに切り替え
$ adb shell cmd webviewupdate disable-multiprocess
$ adb shell ps -A | grep -iE "nviso|webview"
u0_a263 8780 891 330974052 291592 do_epoll_wait 0 S eu.nviso.webview

これでアプリ内から cert_verify_proc をフックできます:

var base = Process.findModuleByName("libwebviewchromium.so").base;
var cert_verify_proc = base.add(0x3e015f8);

Interceptor.attach(cert_verify_proc, {
    onEnter: function(args) {
        console.log("CertVerifyProc called");
    },
    onLeave: function(retval) {
        console.log("CertVerifyProc returned: " + retval.toInt32());
        retval.replace(ptr(0));
    }
});

ただし、ライブラリがTLS検証の結果を CachingCertVerifier::Verify で実際にキャッシュしているため、結果に一貫性がなくなります。チェックを完全に無効にするには、CachingCertVerifierもパッチする必要があります。この関数はオフセット 0x3e00068 にあります:

var verify_cache = base.add(0x3e00068);

Interceptor.attach(verify_cache, {
    onEnter: function(args) {
        console.log("verify_cache called");
    },
    onLeave: function(retval) {
        console.log("verify_cache returned: " + retval.toInt32());
        retval.replace(ptr(0));
    }
});

これら2つのフックを組み合わせることで、バージョン 149.0.7827.159 で検証の無効化に成功します。

まとめ

Googleは、すべてを完全に自動化することを目的として、リーフ証明書の有効期間をどんどん短くする方向に進めています。レジリエンスの観点からは理にかなっていますが、テストツールがこれらの新しい要件に追いついていない場合、問題になることがあります。

幸い、上記の解決策はBurp Suiteが追いつくまで、または同様の状況で使用できます。