
Android полная цепочка эксплойтов, позволяющая повысить привилегии от локального недоверенного приложения до root/ядра, объединяющая CVE-2026-49881 и CVE-2026-43284
Полная цепочка эксплуатации, которая позволяет повысить привилегии от локального ненадёжного приложения до root/ядра. Она не включает повреждения памяти или гонки данных, поэтому атакующим не нужно выполнять сложный heap spraying или обходить механизмы защиты от уязвимостей повреждения памяти, такие как KASLR, MTE или CFI, что делает эту цепочку эксплуатации успешной на 100% на уязвимых устройствах.
Протестировано на Pixel 10 с первоначальным официальным релизом Android 17. Обратите внимание, что она не работает на Pixel 6a, и эта проблема также может возникать на других устройствах с деревьями ядра 6.1.xxx-android14 из-за другой ошибки в этих ядрах.
Использование: установите приложение KernelSU, откройте это приложение, нажмите «Run userspace exploit», затем «Run kernel exploit and load KernelSU». После успешной эксплуатации KernelSU будет активирован, и вы сможете использовать его для предоставления root-доступа другим приложениям. Известная проблема: если вы уже запускали эксплойт ядра и хотите запустить его снова, необходимо перезагрузить устройство.
Видеозапись экрана: нажмите здесь
Цепочка состоит из двух различных уязвимостей: одна — 0-day в сервисе Telecom, а другая — 1-day в ядре, раскрытая 3 месяца назад. Но AOSP и устройства Pixel (кроме тех, что работают на бета-версиях QPR) остаются уязвимыми на момент написания.
Первая уязвимость цепочки — простая логическая ошибка, появившаяся в Android 17. Она берёт начало из безумного изменения, которое добавляет следующий код в InCallController.java:
PackageManager packageManager = mContext.getPackageManager();
Context userContext = mContext.createContextAsUser(userHandle,
0 /* flags */);
PackageManager userPackageManager = userContext != null ?
userContext.getPackageManager() : packageManager;
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() определён следующим образом:
/**
* 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.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY для загрузки кода из произвольного приложения; хотя, похоже, существуют некоторые меры, предназначенные для предотвращения произвольного выполнения кода, такие как передача false в Class.forName() для предотвращения инициализации класса, приложение всё равно может объявить собственный AppComponentFactory, который вызывается при обращении к getClassLoader().
С другой стороны, ошибка существует в InCallController.java, который является частью пакета com.android.server.telecom, а не com.android.phone. Стоит отметить, что пакет объявляет android:sharedUserId="android.uid.system" и android:process="system" в AndroidManifest.xml, поэтому он работает в процессе system_server, одном из наиболее привилегированных пользовательских процессов в Android. Таким образом, теперь у нас есть возможность выполнять произвольный Java-код внутри system_server.
Удивительно, что даже инженер Google может совершить такую большую ошибку в эпоху ИИ. Мы обнаружили её и сообщили в команду безопасности Android 23 июля 2026 года. Они сказали нам, что это дубликат. Google перешёл с ежемесячного на ежеквартальный выпуск бюллетеня безопасности, что может объяснить, почему уязвимость не была исправлена через 3 месяца после выпуска Android 17.
Уязвимости присвоен номер CVE-2026-49881, и она исправлена в сентябре 2026 года с помощью Remove serviceClassExists logic to address security vulnerability.
Первая ошибка позволяет нам повысить привилегии до system, но это всё ещё далеко от root. Полный root требует как минимум UID 0 и отсутствия ограничений со стороны SELinux.
Теперь пришло время представить 1-day в ядре: уязвимости DirtyFrag. Основные принципы здесь подробно описываться не будут; пожалуйста, обратитесь к writeup первоначального репортёра. Существует 2 варианта: CVE-2026-43500 требует RxRPC, который отключён для Android Generic Kernels; CVE-2026-43284 требует xfrm-ESP и эксплуатируется на Android. Однако SELinux запрещает ненадёжным приложениям использовать эту функцию:
# Privileged netlink socket interfaces.
neverallow { appdomain -network_stack }
domain:{
netlink_tcpdiag_socket
netlink_nflog_socket
netlink_xfrm_socket
netlink_audit_socket
netlink_dnrt_socket
} *;
Единственные разрешённые домены — это system_server, network_stack и netd. Чтобы эксплуатировать DirtyFrag, атакующие должны сначала скомпрометировать один из процессов, включённых в белый список.
Объединим две ошибки. В то время как ошибка в пользовательском пространстве позволяет нам выполнять Java-код внутри system_server, SELinux также запрещает system_server как загружать нативные библиотеки из /data, так и отображать анонимную исполняемую память. Это делает невозможным использование нативного кода, что повышает сложность эксплуатации. Поэтому предпочтительнее выполнять код внутри com.android.networkstack, который может загружать нативный код из нашего APK и имеет достаточные привилегии для эксплуатации DirtyFrag.
К счастью, system_server — это процесс, в котором работает ActivityManager. ActivityManager хранит дескрипторы IApplicationThread каждого процесса приложения в Java-карте, и, поскольку мы работаем в том же процессе, что и ActivityManager, мы можем получить их с помощью Java-рефлексии. С их помощью мы можем отправлять произвольные команды в com.android.networkstack, чтобы заставить его загрузить наш код. Для получения дополнительной информации об этом трюке, пожалуйста, обратитесь к моему предыдущему эксплойту для CVE-2026-0091.
DirtyFrag позволяет перезаписывать файлы, доступные только для чтения. Это мощный примитив в мире Linux, поскольку мы можем перезаписать бинарный файл su, у которого установлен бит SUID. Однако в мире Android у нас нет su. Мы обращаемся к эксплойту polygraphene для DirtyPipe, чтобы превратить DirtyFrag в выполнение кода ядра на Android:
libc.so, libc++.so и /vendor/lib64/libstagefright_aidl_bufferpool2.so через DirtyFrag. libstagefright_aidl_bufferpool2.so помечен доменом vendor_file, поэтому к нему нельзя получить доступ из процесса сетевого стека. Решение состоит в том, чтобы сначала пропатчить /apex/com.android.runtime/bin/crash_dump64, выполнить его, и как только мы перейдём в домен crash_dump, мы сможем открыть libstagefright_aidl_bufferpool2.so.init. Поскольку libc++.so пропатчен, наш код выполняется с UID 0 в домене init. Затем мы выполняем /vendor/bin/modprobe, чтобы перейти в домен .vendor_modprobemodprobe, поскольку libc.so также пропатчен, наш код выполняется в домене vendor_modprobe. Теперь мы можем загружать модули ядра, но только для файлов с определёнными метками. Мы загружаем libstagefright_aidl_bufferpool2.so, у которого есть метка vendor_file.libstagefright_aidl_bufferpool2.so, реальное содержимое этого файла было заменено нашим собственным модулем ядра. Модуль ядра загружается, и теперь мы можем делать что угодно, включая изменение политики SELinux или перевод SELinux в permissive-режим.