
Demostración de CVE-2019-2215 (Bad Binder) para Android Q
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:
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í:
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.
CVE-2019-2215 es un Use-After-Free (UAF) en el subsistema IPC Binder del kernel de Android.
De forma simplificada:
struct binder_thread que describe el hilo que realiza
llamadas a Binder;waitqueue);remove_wait_queue,
lo que abre un escenario clásico de UAF;Un análisis teórico más detallado lo hice basándome en los materiales:
Según la tarea se recomienda usar un AVD con imagen Android 10.0 (Q) x86_64.
Hice lo siguiente:
/dev/binder.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:
addr_limit,Este es un matiz importante: todo el código y el informe a continuación — educativos, no «de combate».
Hice una pequeña aplicación Android:
Pasos principales:
Creé un proyecto normal en Android Studio (Kotlin, soporte mínimo para Android 10).
Conecté NDK y CMake.
Añadí el archivo nativo con el exploit (el famoso cve-2019-2215.c con las funciones
leak_task_struct, overwrite_addr_limit, etc.).
En CMakeLists.txt añadí la compilación de libcve-2019-2215.so.
En MainActivity:
init {
System.loadLibrary("cve-2019-2215")
}
external fun runNativeExploit(): String
external fun setNativeLogger(logger: NativeLogger)
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.
Compilo e instalo la aplicación:
./gradlew installDebug
Arranco el AVD y la propia aplicación.
En pantalla veo una «terminal» y el botón RUN EXPLOIT.
Al pulsarlo:
runNativeExploit() en un hilo en segundo plano.En un kernel vulnerable real esperaría ver al final algo como:
[+] 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.
A continuación — el esquema lógico del exploit con referencia a funciones C específicas.
El plan de alto nivel es el siguiente:
struct binder_thread y usarlo para
filtrar la dirección de task_struct del propio proceso (leak_task_struct).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.arb_read / arb_write).cred del proceso actual y la base del kernel (verifying),
luego:
selinux_enforcing = 0),cred para convertirse en root y obtener el conjunto completo
de capabilities (runNativeExploit).En paralelo integré un logger JNI para que todas estas etapas fueran visibles directamente en la UI.
task_struct (leak_task_struct)Función clave:
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:
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.
Abre /dev/binder, crea un descriptor epoll:
fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
El descriptor de Binder se registra en epoll:
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
Prepara un array de struct iovec iov_buffers[IOVEC_N] y asigna memoria: