
Android cadeia de exploração completa que permite escalonamento de privilégios de um aplicativo local não confiável para root/kernel, combinando CVE-2026-49881 e CVE-2026-43284
Uma cadeia de exploração completa que permite escalonamento de privilégios de um aplicativo local não confiável para root/kernel. Ela não envolve corrupção de memória ou condições de corrida, portanto os atacantes não precisam realizar heap spraying complexo ou contornar mitigações contra vulnerabilidades de corrupção de memória como KASLR, MTE ou CFI, tornando esta cadeia de exploração uma taxa de sucesso de 100% em dispositivos vulneráveis.
Testado no Pixel 10 executando a versão oficial inicial do Android 17. Observe que não funciona no Pixel 6a e esse problema também pode ocorrer em outros dispositivos que executam árvores de kernel 6.1.xxx-android14 devido a outro bug nesses kernels.
Uso: Instale o aplicativo KernelSU, abra este aplicativo, clique em "Run userspace exploit" e depois em "Run kernel exploit and load KernelSU". Após uma exploração bem-sucedida, o KernelSU será ativado e você poderá usá-lo para conceder acesso root a outros aplicativos. Problema conhecido: Se você já executou o exploit do kernel e quiser executá-lo novamente, será necessário reiniciar o dispositivo.
Gravação de tela: clique aqui
A cadeia é composta por duas vulnerabilidades distintas: uma é um 0-day no serviço Telecom, enquanto a outra é um 1-day do kernel que foi divulgado há 3 meses. Mas AOSP e dispositivos Pixel (exceto aqueles que executam versões beta do QPR) permanecem vulneráveis no momento em que este texto foi escrito.
A primeira vulnerabilidade da cadeia é um bug lógico simples introduzido no Android 17. Ele se origina de uma mudança maluca, que adiciona o seguinte código a 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;
}
}
}
O método serviceClassExists() relevante é definido da seguinte forma:
/**
* 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;
}
}
Esta é a vulnerabilidade mais inacreditável que já vi. O código usa Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY para carregar código de um aplicativo arbitrário; Embora pareça haver algumas medidas destinadas a impedir a execução arbitrária de código, como passar false para Class.forName() para impedir a inicialização da classe, o aplicativo ainda pode declarar um AppComponentFactory personalizado que é invocado quando getClassLoader() é chamado.
Por outro lado, o bug existe em InCallController.java, que faz parte do pacote com.android.server.telecom em vez de com.android.phone. Vale a pena notar que o pacote declara android:sharedUserId="android.uid.system" e android:process="system" no AndroidManifest.xml, portanto ele é executado no processo system_server, um dos processos de userspace mais privilegiados do Android. Portanto, agora temos a capacidade de executar código Java arbitrário dentro do system_server.
É uma surpresa que até mesmo um engenheiro do Google possa cometer um erro tão grande na era da IA. Nós o encontramos e o reportamos à Equipe de Segurança do Android em 23 de julho de 2026. Eles nos disseram que era uma duplicata. O Google mudou o boletim de segurança mensal para lançamento trimestral, o que pode explicar por que a vulnerabilidade não foi corrigida 3 meses após o lançamento do Android 17.
A vulnerabilidade recebeu o CVE-2026-49881 e foi corrigida em setembro de 2026 por Remove serviceClassExists logic to address security vulnerability.
O primeiro bug nos permite escalar privilégios para system, mas ainda está longe do root. Um root completo requer pelo menos UID 0 e não ser restringido pelo SELinux.
Agora é hora de apresentar o 1-day do kernel: as vulnerabilidades DirtyFrag. Os princípios subjacentes não serão elaborados aqui; consulte o writeup do relator original. Existem 2 variantes: CVE-2026-43500 requer RxRPC, que está desabilitado para Kernels Genéricos do Android; CVE-2026-43284 requer xfrm-ESP e é explorável no Android. No entanto, o SELinux proíbe aplicativos não confiáveis de usar esse recurso:
# Privileged netlink socket interfaces.
neverallow { appdomain -network_stack }
domain:{
netlink_tcpdiag_socket
netlink_nflog_socket
netlink_xfrm_socket
netlink_audit_socket
netlink_dnrt_socket
} *;
Os únicos domínios permitidos são system_server, network_stack e netd. Para explorar o DirtyFrag, os atacantes devem primeiro comprometer um dos processos privilegiados na lista de permissões.
Combine os dois bugs. Enquanto o bug de userspace nos permite executar código Java dentro do system_server, o SELinux também proíbe o system_server de carregar bibliotecas nativas de /data ou mapear memória executável anônima. Isso torna impossível usar código nativo, aumentando a dificuldade da exploração. Portanto, seria preferível executar código dentro do com.android.networkstack, que pode carregar código nativo do nosso APK e tem privilégios suficientes para explorar o DirtyFrag.
Felizmente, system_server é o processo onde o ActivityManager é executado. O ActivityManager armazena handles IApplicationThread de cada processo de aplicativo em um mapa Java e, como executamos no mesmo processo do ActivityManager, podemos recuperá-los usando reflexão Java. Com isso, podemos enviar comandos arbitrários para com.android.networkstack para forçá-lo a carregar nosso código. Para mais informações sobre esse truque, consulte meu exploit anterior para CVE-2026-0091.
O DirtyFrag permite sobrescrever arquivos somente leitura. Este é um primitivo poderoso no mundo Linux, porque podemos sobrescrever o binário su que tem o bit SUID. No entanto, não temos su no mundo Android. Referimo-nos ao exploit do polygraphene para DirtyPipe para transformar o DirtyFrag em execução de código no kernel no Android:
libc.so, libc++.so e /vendor/lib64/libstagefright_aidl_bufferpool2.so através do DirtyFrag. libstagefright_aidl_bufferpool2.so é rotulado como domínio vendor_file, portanto não pode ser acessado a partir do processo da network stack. A solução é corrigir /apex/com.android.runtime/bin/crash_dump64 primeiro, executá-lo e, uma vez que fizermos a transição para o domínio crash_dump, podemos abrir libstagefright_aidl_bufferpool2.so.init. Como libc++.so foi corrigido, nosso código é executado como UID 0 com o domínio init. Em seguida, executamos /vendor/bin/modprobe para fazer a transição para o domínio .vendor_modprobemodprobe é executado, como libc.so também foi corrigido, nosso código é executado sob o domínio vendor_modprobe. Agora podemos carregar módulos do kernel, mas apenas para arquivos que têm rótulos especificados. Carregamos libstagefright_aidl_bufferpool2.so, que tem o rótulo vendor_file.libstagefright_aidl_bufferpool2.so, o conteúdo real desse arquivo foi substituído pelo nosso próprio módulo do kernel. O módulo do kernel é carregado e agora podemos fazer qualquer coisa, incluindo ajustar a política do SELinux ou definir o SELinux como permissivo.