这是 CVE-2026-49881 的 PoC 和 writeup,该漏洞是 Android 17 Telecom 服务中 InCallController 类的一个逻辑问题,允许无特权的应用在无需额外用户交互的情况下,以 UID 1000 system_server 的身份获得任意代码执行能力。我们在此还展示了,即使在现代 Android 版本中,system_server 代码执行仍然可以轻松地用于获得持久化。
我于 2026-04-10 向 Android 安全团队报告了此漏洞,于 2026-05-06 被确认,并在 2026 年 9 月的 Android 安全公告中修复。(补丁见此处)
据我所知,在报告时,它仅主动影响 Android 16 QPR3 及之后的 Pixel 版本(以及 17 Beta 版本),但后来它进入了 Android 17 的 AOSP 稳定版。
为保护自己免受此问题的影响,请务必安装 Google Play 系统更新以及系统安全补丁。(自 Android 17 起,Telecom 是 mainline 组件)
system_server 中获得代码执行能力,将 id 和堆栈跟踪记录到 logcat,并将自身重新安装为 system_server 组件。build.sh 编译 PoC。否则,您可以手动运行 ./gradlew assembleSystemRelease,将生成的 app-system-release.apk 移动到 app/src/poc/assets/system.apk,然后运行 ./gradlew assemblePocRelease。system_server 后的 PoC 还会强制关闭 Play Protect,方法是将 Settings.Global 中的 package_verifier_user_consent 设置为 -1,因为它有时会因未知签名而拦截重新安装事务。测试后,您应在“设置”中重新启用此功能。system_server 的重新安装阶段可能需要针对每个 OEM 进行定制,因为它依赖于使用 OEM 有时会更改的符号来遍历 PMS 结构。预期的 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 中“开始利用”按钮所使用的)此 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 intent 以及 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 中。然而,乍一看,这似乎是安全的,因为获取的上下文在 Class.forName 中使用了 initialize=false——开发人员很可能意识到了这一风险,并指定了该参数以确保外部类不会在 system_server 中被初始化。
不幸的是,这种谨慎为时已晚——外部上下文上的 getClassLoader 已经造成了损害。如果攻击者应用在其清单中将 AppComponentFactory 定义为 android:appComponentFactory,则对应于使用 CONTEXT_INCLUDE_CODE 和 CONTEXT_IGNORE_SECURITY 创建的上下文的 LoadedApk 上的 getClassLoader 方法首先从磁盘检索外部应用的默认类加载器,然后在返回类加载器之前运行工厂的构造函数及其 instantiateClassLoader 方法。
由于此类可由攻击者应用控制,这立即导致在 Telecom 的上下文中执行任意代码。
为了展示成功的 system_server 代码执行的后果,PoC 演示了将自身重新安装为持久的 system_server 组件。(该技术改编自 Michał Bednarski 在 AbxOverflow / CVE-2024-34740 PoC 中发布的技术)
我们通过以下方式实现:
system_server,通过动态反射直接进入 PackageManagerService.mSettings 来检索我们 PoC 应用的 Signature 对象。"android.uid.system" 对应的 SharedUserSetting,并将 PoC 的 Signature 以 CertCapabilities.SHARED_USER_ID 注入其 getSigningDetails().mPastSigningCertificates 两次。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 在尝试绑定前验证类是否存在”。(这就是我们现在在这里的原因)