Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
TLPE — CVE-2026-49881 — логическая ошибка в классе InCallController в службе Telecom в Android 17, которая позволяет непривилегированному приложению получить произвольное выполнение кода с правами UID 1000 system_server. | Kitploit
Инструменты/GitHubGitHub/supersonic/tlpe
Безопасность AndroidПовышение привилегийМеханизмы персистентностиАнализ уязвимостейЭксплуатацияМобильная безопасностьЭксплуатация Бинарных Файлов
GitHubsupersonic/tlpe

TLPE

CVE-2026-49881 — логическая ошибка в классе InCallController в службе Telecom в Android 17, которая позволяет непривилегированному приложению получить произвольное выполнение кода с правами UID 1000 system_server.

Репозиторий
1118 ч 57 мин назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Это PoC и разбор CVE-2026-49881 — логической ошибки в классе InCallController в Telecom-сервисе Android 17, которая позволяет непривилегированному приложению получить выполнение произвольного кода с UID 1000 system_server без дополнительного взаимодействия с пользователем. Мы также показываем, что выполнение кода в system_server по-прежнему можно легко адаптировать для получения устойчивости (persistence), даже в современных версиях Android.

Я сообщил об этой уязвимости команде Android Security 2026-04-10, она была подтверждена 2026-05-06 и исправлена в сентябрьском бюллетене безопасности Android за 2026 год. (см. здесь патч)

На момент сообщения, насколько мне известно, она активно затрагивала только сборки Pixel начиная с Android 16 QPR3 (а также бета-сборки 17), но позже она попала и в стабильный релиз AOSP Android 17.

Чтобы защититься от этой проблемы, обязательно установите обновление системы Google Play, а также системный патч безопасности. (Telecom является компонентом mainline начиная с Android 17)

TLPE

Примечания к PoC

  • PoC демонстрирует получение выполнения кода в system_server с помощью уязвимости, записывает id и стек вызовов в logcat, а затем переустанавливает себя как компонент system_server.
  • После установки PoC его запуск происходит либо по нажатию кнопки Start Exploit, либо при совершении вызова через telecom-стек.
  • Скомпилируйте PoC, запустив прилагаемый build.sh. В противном случае вы можете вручную выполнить ./gradlew assembleSystemRelease, переместить полученный app-system-release.apk в app/src/poc/assets/system.apk, а затем выполнить ./gradlew assemblePocRelease.
  • После успешной эксплуатации PoC зарегистрирует собственный сертификат как сертификат-предок для UID 1000. Это состояние сохраняется после OTA-обновлений, включая установку патча самой уязвимости. Чтобы очистить устройство после запуска PoC, нажмите кнопку «Uninstall» в PoC (она удаляет внедрённый сертификат) — тем не менее, я настоятельно рекомендую использовать собственный release-keystore для подписи скомпилированного APK PoC во время тестирования (а не тот, который PoC использует по умолчанию: TLPE/app/teststore.jks).
  • Обратите внимание, что PoC после попадания в system_server также отключает Play Protect, устанавливая package_verifier_user_consent в -1 в Settings.Global, поскольку тот иногда может перехватывать транзакцию переустановки из-за неизвестных подписей. После тестирования вам следует снова включить эту опцию в настройках.
  • Он проверен на Pixel Android и AOSP, но этап выполнения кода должен работать и в кастомизированных OEM-версиях Android 17. Этап переустановки в качестве system_server может потребовать индивидуальной настройки для каждого OEM, поскольку он опирается на обход структуры PMS с использованием символов, которые OEM иногда меняют.

Ожидаемый вывод logcat для PoC:

root@kitploit:~
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. (примечание: именно это использует кнопка Start Exploit в PoC) Этот API требует MANAGE_OWN_CALLS, но это обычное и незаметное для пользователя разрешение, выдаваемое автоматически при установке.

Это перечисление реализовано в уязвимых версиях так:

root@kitploit:~
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 выполняется для любого компонента, который объявляет intent InCallService вместе со значением метаданных android.telecom.CLASS_EXISTENCE_CHECK, а не только для валидных/включённых InCallService. (эта проверка реализована ниже по ветке с помощью getInCallServiceType и isServiceEnabled)

serviceClassExists реализован так:

root@kitploit:~
/**
 * 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;
    }
}

Опасности createPackageContext с CONTEXT_IGNORE_SECURITY хорошо документированы, и в данном случае сам system_server выполняет его против недоверенного компонента лишь для того, чтобы проверить, существует ли класс в DEX этого приложения. На первый взгляд, однако, это может выглядеть безопасно, поскольку полученный контекст используется в Class.forName с initialize=false — разработчик, скорее всего, знал об этом риске и указал этот аргумент, чтобы гарантировать, что чужой класс не будет инициализирован в system_server.

К сожалению, эта осторожность проявляется слишком поздно — ущерб уже наносится вызовом getClassLoader на чужом контексте. Если приложение атакующего определяет AppComponentFactory в своём манифесте как android:appComponentFactory, метод getClassLoader на LoadedApk, соответствующем контексту, созданному с CONTEXT_INCLUDE_CODE и CONTEXT_IGNORE_SECURITY, сначала извлекает загрузчик классов по умолчанию чужого приложения с диска, а затем выполняет и конструктор фабрики, и её метод instantiateClassLoader, прежде чем вернуть загрузчик классов.

Поскольку этот класс контролируется приложением атакующего, это немедленно приводит к выполнению произвольного кода в контексте Telecom.

Пост-эксплуатация

Чтобы показать последствия успешного выполнения кода в system_server, PoC демонстрирует переустановку себя в качестве постоянного компонента system_server. (техника адаптирована из метода, опубликованного в PoC AbxOverflow / CVE-2024-34740 Михалом Беднарским)

Мы делаем это следующим образом:

  • Получаем объект Signature нашего PoC-приложения, динамически рефлектируя в PackageManagerService.mSettings напрямую, оказавшись в system_server.
  • Получаем SharedUserSetting, соответствующий "android.uid.system", и внедряем Signature PoC дважды в его getSigningDetails().mPastSigningCertificates с CertCapabilities.SHARED_USER_ID.
  • Принудительно удаляем PoC-приложение и переустанавливаем его вариант с добавленными в манифест android:sharedUserId="android.uid.system" и android:process="system". (это также сбрасывает изменчивую модификацию из предыдущего шага в packages.xml)

Это заставляет canJoinSharedUserId() в PackageSignatures пройти проверку благодаря совпадению истории ротации и даёт постоянные привилегии system_server.

Заключительное примечание

Один вопрос, который у вас может возникнуть: почему эта уязвимость вообще существует — в частности, почему существует опциональная проверка существования класса, скрытая за недокументированным флагом метаданных?

Невозможно сказать наверняка, но вероятный ответ на эту загадку можно найти, копнув глубже в AOSP: и проверка класса, и сравнение метаданных, вероятно, были добавлены для учёта android.net.ConnectivityCallListenerService, который реализовывался примерно в то же время.

Этот сервис изначально был определён в манифесте фреймворка, но нигде не был реализован. (а когда он был добавлен, то за флагом функции Flags.FLAG_ENABLE_INCALL_SERVICE_API) После того как в тестировании наблюдались краши, кто-то, вероятно, решил реализовать исправление как переиспользуемую проверку «защиты в глубину», а не как жёстко прописанное исключение — определение этого сервиса в манифесте говорит, что он определяет "android.telecom.CLASS_EXISTENCE_CHECK", чтобы «указать, что класс этого сервиса может отсутствовать во всех сборках» и «указать Telecom проверить существование класса перед попыткой привязки». (и именно поэтому мы сейчас здесь)

Скачать инструмент