
Exploit de LPE para Android CVE-2026-43499 direcionado ao OPPO PMG110 (kernel 6.6). Usa futex PI UAF para obter root e instalar um daemon su via LD_PRELOAD.
CVE-2026-43499 (uso após liberação do rt_mutex_waiter do futex PI) escalonamento de privilégios local, portado para o OPPO PMG110 / K15 Pro+ — MediaTek MT6991, ColorOS 16.
Um único arquivo enviado, executado via LD_PRELOAD:
adb push out/preload-pmg110-16.0.9.400.so /data/local/tmp/preload.so
adb shell chmod 644 /data/local/tmp/preload.so
adb shell LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true
Em caso de sucesso, um su persistente fica disponível:
adb shell /data/local/tmp/su -c id # uid=0(root)
Verificado no dispositivo (2026-07-27): uid=0 em cerca de 35 segundos em execução pura, sem sobrescrever variáveis de ambiente, e su respondendo em seguida a partir de um adb shell comum sem privilégios:
$ adb shell "/data/local/tmp/su -c 'echo 0 > /proc/sys/kernel/kptr_restrict'"
$ adb shell "/data/local/tmp/su -c 'grep -w init_task /proc/kallsyms'"
ffffffe89033e780 D init_task
O exploit e a instalação do su foram ambos verificados neste dispositivo. O que esse shell com root leu em seguida também confirma P0_KERNEL_PHYS_LOAD, os offsets de símbolos e KS_MTE_TAGGED=0 de forma independente do exploit — consulte targets/pmg110-16.0.9.400/NOTES.md.
Ele executa a Escrita 1 (SELinux permissivo) e a Escrita 2 (cred → init_cred), leva um processo filho a uid=0 e, a partir daí, instala o daemon su embutido.
su não é um segundo artefato: su_daemon.c é compilado como um PIE aarch64 autônomo e embutido via .incbin no .rodata da biblioteca, de modo que viaja dentro de preload.so e é gravado de volta em tempo de execução. É o caminho do warhol-root, inalterado.ksud, sem KernelSUO su é instalado em três lugares, porque um deles é o que você realmente alcançará:
O daemon escuta em /data/local/tmp/temp_su.sock e registra logs em /data/local/tmp/su_daemon.log. O root não persiste entre reinicializações — execute novamente a linha com LD_PRELOAD após cada boot.
Para instalação do KernelSU, use o caminho /data/local/tmp/a/e no ghostlock-oneplus.
Tudo, exceto o núcleo do exploit, é do warhol-root, aproveitado em vez de reinventado:
targets/<device>/, preparados em source/src/ no momento da compilação, de modo que alternar DEVICE nunca deixa para trás os cabeçalhos do dispositivo anteriorsource/Makefile (NDK se houver, clang do host contra o sysroot do NDK se não houver) e a regra de incorporação em duas etapas que produz build/embed/su_daemon_aarch64_pie antes de linkar o .sosu_daemon.c e su_blob.S são byte-idênticos aos do warhol-root, e su_install.c é o instalador preload.c deleO núcleo do exploit não é do warhol-root. warhol-root é popsicle, que está fixado no GKI 6.12 / android16 e cujo generate_target.py recusa qualquer outro banner. PMG110 é 6.6 / android15, então o núcleo aqui é a árvore ghostlock 6.6 — ela própria descendente do mesmo código (kernelsnitch/utils.h e timeutils.h são byte-idênticos entre os dois repositórios), com desenvolvimento adicional.
Cada linha do exploit em si — Escrita 1, Escrita 2, KernelSnitch, o caminho pselect — é o mesmo código nas duas árvores.
Esta é a única diferença estrutural, e ela é imposta pelo fato de as duas árvores obterem root de formas diferentes.
O warhol-root dá root ao próprio processo do exploit e por isso chama install_embedded_su() direto de run_direct_root(). Aqui, a Escrita 2 troca o ponteiro cred de um filho criado via fork e o pai permanece como o chamador sem privilégios; portanto, o filho em child_main() é o único contexto que pode fazer a instalação — é lá que ela roda.
As duas árvores carregam o mesmo stub fraco install_embedded_su() em util.c, que retorna ENOSYS; fornecer a definição forte é o que ativa o caminho. Isso é útil saber porque um build que, por algum motivo, deixe de incluir su_install.c ainda linka e ainda roda — ele apenas reporta su=0/38 e não instala nada.
make # = make preload -> out/preload-<DEVICE>.so
make DEVICE=<name> # use targets/<name>/
make devices # list available DEVICE values
make info # show the selected target and the resolved toolchain
A toolchain é encontrada sozinha: ANDROID_NDK_HOME / ANDROID_NDK_ROOT primeiro, depois os locais usuais de instalação do NDK para Linux e macOS e, se nenhum deles funcionar, o clang do host apontando para o sysroot do NDK. Defina ANDROID_NDK_HOME apenas para substituir a busca. make info mostra o que foi escolhido.
O build tem duas etapas, e é isso que vale a pena saber:
su_daemon.c → build/embed/su_daemon_aarch64_pie, um PIE aarch64 autônomosu_blob.S incorpora esse binário via .incbin no .rodata, e tudo é linkado no único preload.soPortanto, make clean e um rebuild são a única forma de alterar o su embutido — editar apenas su_daemon.c é suficiente, a dependência está declarada, mas o blob é um artefato de build e não é versionado.
targets/<device>/{target.h,device_offsets.h} são preparados novamente em source/src/ a cada build, para que um cabeçalho obsoleto de outro dispositivo não seja usado silenciosamente.
out/*.so não é versionado (mesma convenção do warhol-root) — clone e rode make.
O .so é compilado com -fvisibility=hidden e exporta zero símbolos. Uma biblioteca LD_PRELOAD vence a busca de símbolos para o processo inteiro, então qualquer coisa que ela exportasse poderia sobrescrever um símbolo de mesmo nome no binário hospedeiro ou na libc. Essa flag rege apenas a geração de código C, por isso su_blob.S marca seus dois símbolos como .hidden manualmente — sem essas linhas, os limites do blob seriam a única coisa que a biblioteca ainda exportaria.
Nenhuma delas foi necessária na execução verificada.
[*] futex_hashsize 2048 (8 possible CPUs)
[*] ks collisions=3/3 baseline=8 threshold=10x (80) accepted=[1244..1597] slowest_rejected=N
[+] child uid = 0
[+] embedded su wrote 15304 bytes to /apex/com.android.virt/bin/su
[+] embedded su daemon ready pid=NNNN socket=/data/local/tmp/temp_su.sock daemon=/apex/com.android.virt/bin/su
[+] embedded su install ok=1 errno=0 daemon=NNNN
[+] su ready: /data/local/tmp/su and /apex/com.android.virt/bin/su
[+] ghostlock preload verdict: EXPLOIT OK
child uid = 0 é o exploit tendo sucesso; tudo depois disso é a instalação. Os dois são reportados separadamente de propósito, e o veredito também:
O do meio é a distinção que vale a pena ter: ele diz que os offsets em target.h estão corretos para este build e que o problema está em algum ponto da instalação, o que é algo completamente diferente de depurar. O su=0/38 (ENOSYS) dentro dele significa especificamente que o stub fraco foi o que entrou no link.
mm_struct leak failed seguido por prepare_kernel_page retry N/24 não é uma falha. É o progresso do loop, e a execução bem-sucedida também mostra isso. Nada falhou até as 24 tentativas se esgotarem e prepare_kernel_page timeout aparecer. Da mesma forma, probing cfi ... expected=9 com child uid = 2000 é uma rodada perdida, de dez.
Não julgue uma execução por um log truncado — esse erro custou uma rodada inteira de diagnóstico errado aqui.
Uma execução completamente falha também é normal. A corrida (race) do pselect não é 100%: uma execução pode perdê-la cinco vezes seguidas e terminar em Write 1 failed, e a próxima acerta de primeira com ret=9. Observado neste dispositivo. ret=4 expected=9 é a cara de uma corrida perdida, não de um target.h errado — uma falha não é motivo para sair recalculando offsets. Execute novamente.
Apenas para pesquisa de segurança autorizada e fins educacionais.
| Dispositivo | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991 (Dimensity 9500s) |
| Kernel | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, páginas de 4K) |
| Build | ColorOS 16 / PMG110_16.0.9.400(CN01) — mesmos bytes de kernel de 16.0.8.300 |
| Bug | CVE-2026-43499, não corrigido nesta imagem (comprovado por desmontagem, não pela versão) |
| Caminho | Porquê |
|---|
/apex/com.android.virt/bin/su | em um tmpfs montado sobre esse diretório; no PATH de um shell root |
/data/local/tmp/su | acessível a partir de um adb shell simples, sem jogos de PATH |
/apex/com.android.virt/bin/su no namespace de mount do adbd | instalado via setns, de modo que um novo adb shell o veja |
| Arquivo | Relação |
|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | do ghostlock, byte-idênticos |
su_daemon.c su_blob.S | do warhol-root, byte-idênticos (su_blob.S adiciona duas linhas .hidden — veja Build) |
su_install.c | instalador preload.c do warhol-root, movido para um arquivo próprio porque o preload.c desta árvore já tem outra função |
main.c | do ghostlock, mais a chamada do su no filho com root e o relato do resultado |
preload.c | existe apenas aqui — o construtor e o log com dois destinos |
offsets.h | apenas a definição da struct; a entrada é preparada a partir de targets/<device>/device_offsets.h |
| Variável | Efeito |
|---|
GHOSTLOCK_LOG | destino do log (padrão /data/local/tmp/.ghostlock.log); a saída vai para o stdout e para o arquivo |
GHOSTLOCK_KS_VERBOSE=1 | imprime os endereços de colisão e os intervalos de varredura do KernelSnitch |
GHOSTLOCK_KS_THRESHOLD=<n> | substitui o multiplicador do limite de colisão |
GHOSTLOCK_MTE=1 | varre também as tags de ponteiro do kernel (15x mais lento) |
GHOSTLOCK_PHYS_LOAD=0x... | substitui o endereço de carga física do kernel |
PSELECT_SHIFT=<n> | substitui o deslocamento do overlay de pilha (substitui, não soma) |
| Veredito | Significado |
|---|
EXPLOIT OK | root, e su responde |
EXPLOIT OK, SU INSTALL FAILED | Escrita 1 e Escrita 2 acertaram; apenas a instalação deu errado |
EXPLOIT FAILED | as escritas não acertaram |
ABORTED | a execução morreu antes de conseguir reportar — leia a última linha [!] |
| Caminho | Conteúdo |
|---|
source/src/preload.c | construtor: executa o exploit, reporta, para |
source/src/main.c | o próprio exploit (Escrita 1 / Escrita 2) |
source/src/su_daemon.c | o binário su — compilado de forma autônoma como um PIE aarch64, não linkado no .so |
source/src/su_blob.S | incorpora esse PIE via .incbin no .rodata do .so |
source/src/su_install.c | grava o blob de volta, inicia o daemon e o testa |
source/src/target.h | destino do staging (ignorado pelo git) |
targets/<device>/target.h | layout de tempo de compilação: offsets de structs, constantes do physmap, formas do slab e do futex |
targets/<device>/device_offsets.h | offsets de símbolos globais do kallsyms |
tools/extract_device.py | boot.img → offsets, campos de struct do BTF, resultado do overlay pselect |
tools/preloader_memlayout.py | preloader da MediaTek → P0_KERNEL_PHYS_LOAD |
tools/qemu_verify.py | inicializa o kernel sob QEMU: mede o overlay de pilha, verifica a estabilidade do mapa linear |
tools/device_probe.sh | verificação preliminar a partir de um adb shell sem privilégios |