Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

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

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

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
LeakValue — Exploit para CVE-2022-20452, escalada de privilegios en Android desde una aplicación instalada a una aplicación del sistema (u otra aplicación) mediante LazyValue usando Parcel después de recycle() | Kitploit
Herramientas/GitHubGitHub/michalbednarski/leakvalue
Seguridad AndroidEscalada de PrivilegiosAnálisis de VulnerabilidadesExplotaciónSeguridad MóvilExplotación de Binarios
GitHubmichalbednarski/leakvalue

LeakValue

Exploit para CVE-2022-20452, escalada de privilegios en Android desde una aplicación instalada a una aplicación del sistema (u otra aplicación) mediante LazyValue usando Parcel después de recycle()

Ver Repositorio
34765hace 3 añosRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Android 13 introduce muchas mejoras para endurecer el mecanismo de serialización Parcel

Aquí está la presentación del equipo de Seguridad y Privacidad de Android sobre las mejoras realizadas

Eso es genial, definitivamente elimina o vuelve inexplotables muchas vulnerabilidades. También describen cómo rompieron mi exploit anterior, que permitía a las aplicaciones cargar su código en otras aplicaciones (incluidas las del sistema)

Pero ahora vuelvo con un nuevo exploit que logra lo mismo, aunque de manera diferente. Se basa en las siguientes vulnerabilidades que se introdujeron durante el mencionado endurecimiento de Parcel:

  • CVE-2022-20452 (boletín, parche)
  • CVE-2022-20474 (boletín, parche)

Captura de pantalla de la aplicación mostrando texto. Título: LeakValue. Texto principal: Created 6 ValueLeaker-s. Locking ActivityTaskManagerService. Locked ActivityTaskManagerService. Unlocking ActivityTaskManagerService. Unlocked ActivityTaskManagerService. leakedBinders=[android.os.BinderProxy@f06702e]. Leaked interface: android.app.IApplicationThread. Requesting code execution. Shellcode has been executed in uid=1000 pid=6904 packageName=com.android.settings uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0. En la parte inferior de la pantalla hay dos botones: START y MANUAL TESTING

(También logcat de la ejecución de la aplicación, la explotación es ruidosa en los registros)

Introducción a los bugs de desajuste entre Parcel y Parcelable

La clase Parcel de Android es la base de la comunicación entre procesos

Los objetos pueden implementar la interfaz Parcelable para permitir escribirlos en Parcel, por ejemplo (copiado de AOSP):```java public class UsbAccessory implements Parcelable { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public UsbAccessory createFromParcel(Parcel in) { String manufacturer = in.readString(); String model = in.readString(); String description = in.readString(); String version = in.readString(); String uri = in.readString(); IUsbSerialReader serialNumberReader = IUsbSerialReader.Stub.asInterface( in.readStrongBinder());

root@kitploit:~
        return new UsbAccessory(manufacturer, model, description, version, uri,
                serialNumberReader);
    }
};

public void writeToParcel(Parcel parcel, int flags) {
    parcel.writeString(mManufacturer);
    parcel.writeString(mModel);
    parcel.writeString(mDescription);
    parcel.writeString(mVersion);
    parcel.writeString(mUri);
    parcel.writeStrongBinder(mSerialNumberReader.asBinder());

} }

root@kitploit:~
Note that `Parcel` internally stores position at which write or read is performed, `readString()` parses data into String as well as advances position. That position can be manually get/set through [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)). Implementations of `Parcelable` interface must ensure that their `writeToParcel` and `createFromParcel` write/read same amount of data, otherwise all subsequent reads will get data from wrong offsets

[`Bundle`](https://developer.android.com/reference/android/os/Bundle) (key-value map that can be sent across processes) can contain [variety of objects that can be written to Parcel through `writeValue()`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937). When contents of `Bundle` are read from `Parcel`, any `Parcelable` class available in system there can be read

`Bundle` defers actual parsing of contents by having length of whole parcelled data written into `Parcel` and then just [copying relevant part of original Parcel to secondary Parcel stored in `mParcelledData`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683) (this allows for example [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)) to provide `Parcelable`s which are not available in `system_server`, whole `Bundle` is then passed to `system_server` and back verbatim without parsing contents)

Once however any value in `Bundle` was accessed, all values inside `Bundle` [were unparcelled](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) and [every present key-value pair was parsed](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). If such map contained `Parcelable` which had unbalanced `writeToParcel` and `createFromParcel` methods and later such `Bundle` was forwarded to another process, that another process could see different contents of `Bundle`. This made all such [mismatches in classes available in system vulnerabilities](https://github.com/michalbednarski/ReparcelBug) as there are [places in system where `Bundle` is inspected to be safe](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=5037-5046) and then forwarded to another process

In this writeup I'm calling such `Bundle` which presents one contents and then other after being forwarded a self-changing `Bundle`

Another important thing here is that besides just bytes (Strings, numbers, objects made of above), `Parcel` can also contain File Descriptors and `Binder`s. `Binder`s are objects on which one can make RPC call, that is one process creates `Binder` object and overrides [`onTransact()` method](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Then `Binder` is passed to another process, in example code above you can see `read`/`writeStrongBinder()` calls used to read and write it to `Parcel`. In another process, when `readStrongBinder()` is used a `BinderProxy` object is created (hidden behind [`IBinder` interface](https://developer.android.com/reference/android/os/IBinder)). Then that another process can call [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) on that object and in original object `onTransact()` will be executed. Usually through, one doesn't manually write `transact()`/`onTransact()` but [uses AIDL instead](https://developer.android.com/guide/components/aidl)

# Entra `LazyValue`, el fin de los `Bundle`s auto-cambiables

Dado que en el pasado hubo muchos casos de clases con desajuste en `writeToParcel`/`createFromParcel`, Android 13 soluciona el problema de que cualquier clase de ese tipo esté presente en cualquier parte del sistema permitiendo la construcción de un `Bundle` auto-cambiable mediante la [introducción de `LazyValue`](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/)

Ahora, cuando se usa `writeValue`, si el valor que se escribe no es primitivo, [también se escribe la longitud del valor en el `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804)

Cuando una aplicación normal usa directamente `Parcel.readValue()`, [todo sucede como antes excepto que se imprime una advertencia si la `length` leída de `Parcel` no coincide con el tamaño de los datos realmente leídos](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804) (Nota: [`Slog.wtfStack` nunca lanza](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577))

`Bundle`, sin embargo, ahora usa [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804) en su lugar

Veamos más de cerca cómo funciona: en la clase `LazyValue` hay un [buen comentario explicando la estructura de los datos `LazyValue` dentro de `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4392-4399;drc=03c34f57c05feecfb090de3917787f049cb5f804):```
                     |   4B   |   4B   |
mSource = Parcel{... |  type  | length | object | ...}
                     a        b        c        d
length = d - c
mPosition = a
mLength = d - a

mSource es una referencia al Parcel original en el que se llamó a readLazyValue()

mPosition y mLength describen la ubicación de todos los datos de LazyValue en el Parcel original, incluyendo type y length

"length" (sin "m" al principio) se refiere al valor de longitud tal como se escribió en el Parcel y excluye el encabezado (type y length)

Entonces, esto es lo que sucede cuando alguien (ya sea el sistema o una aplicación) toma un valor de un Bundle que se leyó de un Parcel:

  1. La persona que llama utiliza uno de los muchos métodos get*() de la clase Bundle, por ejemplo el nuevo getParcelable() con argumento de tipo (El flujo será el mismo tanto para métodos nuevos como antiguos; solo que los métodos nuevos aseguran que el argumento clazz no sea null mientras que los antiguos lo establecen a null)
  2. Se llama a unparcel(), que verificará si este Bundle tiene mParcelledData (lo que significa que se leyó de Parcel pero aún no se accedió a ningún valor y los nombres de las claves aún no se desempaquetaron; si ese no es el caso, saltar al paso 5.)
  3. unparcel() delega en , . se establece a , una copia del que el ha creado, y el parámetro se establece a para indicar que el pasado es propiedad del y está bien llamar a sobre él

Si el Bundle se está reenviando mientras aún contiene LazyValue (lo que significa que no se accedió a este valor en particular, pero sí se accedió a algún otro valor de ese Bundle (es decir, se llamó a unparcel(), pero no se llamó a LazyValue.apply() para ese elemento)):

  1. LazyValue es detectado por Parcel.writeValue() y la escritura se delega a LazyValue.writeToParcel()
  2. LazyValue.writeToParcel() usa out.appendFrom(source, mPosition, mLength) para copiar todos los datos de LazyValue desde el Parcel original (de nuevo, mPosition y mLength incluyen el encabezado de LazyValue, por lo que esto también copia type y length del original)

Parcel.ReadWriteHelper y Parcel.readSquashed

(Los detalles de estos no son importantes para este exploit; lo único relevante aquí es que estos mecanismos existen)

Otra característica interesante de Parcel es la capacidad opcional de deduplicar Strings y objetos escritos

La deduplicación de Strings se realiza sobrescribiendo la clase Parcel.ReadWriteHelper: Parcel.readString() en realidad delega en ReadWriteHelper y el helper predeterminado directamente lee el String del Parcel

Una implementación alternativa de Parcel.ReadWriteHelper puede reemplazar las llamadas a readString con lectura previa de un pool de Strings y uso de readInt para obtener índices de Strings en el pool; sin embargo, esto nunca se hace con Parcels controlados por la aplicación

Parcel ofrece el método hasReadWriteHelper(), que permite a quienes llaman detectar la presencia de dicho mecanismo de deduplicación activo y deshabilitar funciones incompatibles con él

Otro mecanismo de deduplicación disponible en Parcel es el "squashing":

  1. Primero, el squashing debe habilitarse con Parcel.allowSquashing()
  2. Luego, cuando se escribe una clase que soporta squashing, primero llama a Parcel.maybeWriteSquashed(this). Si ese método devuelve true, significa que el objeto ya fue escrito en este Parcel y ahora solo se escribió el desplazamiento al dato del objeto anterior en el Parcel. De lo contrario (ya sea porque el squashing no está habilitado o es la primera vez que se escribe este objeto), maybeWriteSquashed escribe cero como desplazamiento para indicar que el objeto no está "squashed" y devuelve false para indicar a quien llama que debe escribir los datos reales del objeto
  3. Al leer, se llama a Parcel.readSquashed y se le pasa la función de lectura real como lambda. readSquashed verifica si el desplazamiento escrito por indica que ya se leyó otra ocurrencia del objeto: si es así, se devuelve el objeto leído previamente; de lo contrario, se llama a la lambda proporcionada para leerlo ahora

Use-after-Parcel.recycle()

En el lado de Java, los objetos Parcel se pueden reciclar en un pool, es decir, una vez que has terminado con un Parcel, llamas a recycle() sobre él y la próxima vez que alguien llame a Parcel.obtain() obtendrá un Parcel previamente reciclado. Esto permite reducir la cantidad de asignaciones de objetos y la posterior recolección de basura

Por otro lado, dicha gestión manual de la memoria trae la posibilidad de errores del tipo Use-After-Free en Java (aunque con seguridad de tipos, a diferencia del Use-After-Free habitual en C)

Como se señaló anteriormente, Bundle crea una copia del Parcel y no llamará a Parcel.recycle() si está presente LazyValue; sin embargo, ese no es el caso si Parcel.hasReadWriteHelper() es true, en ese caso:

  1. Se llama a initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);. Esto significa que Bundle no reciclará el Parcel ya que todavía pertenece a quien llama; sin embargo, esto crea LazyValues que se refieren al Parcel original y podrían superar la vida útil del Parcel original
  2. Por lo tanto, lo siguiente después de eso es la llamada a unparcel(/* itemwise */ true), que usará getValueAt() en todos los elementos para reemplazar todos los LazyValues presentes en el Bundle con valores reales

Ahora, ¿podemos hacer que estos LazyValues sobrevivan al paso 2 y convertir ese comportamiento en Use-After-Recycle?

Si la deserialización falla (por ejemplo, no se pudo encontrar la clase con el nombre especificado dentro del Parcel), se lanza una BadParcelableException y luego es capturada por getValueAt(). Si el campo estático BaseBundle.sShouldDefuse es true, no se lanza una excepción y la ejecución continúa dejando el Bundle conteniendo un LazyValue que se refiere al Parcel original. sShouldDefuse indica que los valores no disponibles del Bundle no deberían causar excepciones en un proceso particular y se establece a true en

Si el Parcel original se recicla y después el Bundle leído de él se escribirá en otro Parcel, los contenidos del Parcel original se copiarán al Parcel de destino, pero en ese punto el Parcel original podría reutilizarse para otra cosa y se podrían copiar datos de una operación IPC no relacionada

De acuerdo, pero ¿cómo logramos que Parcel.hasReadWriteHelper() sea true mientras se deserializa un Bundle proporcionado por nosotros?

Resulta que la clase RemoteViews (normalmente utilizada, por ejemplo, para pasar widgets a la pantalla de inicio) establece explícitamente ReadWriteHelper al leer Bundles anidados en ella. Este ReadWriteHelper no hace deduplicación de String y está presente solo para hacer que Bundle omita copiar datos a un Parcel secundario. La razón para hacer esto es que RemoteViews habilita el squashing con el fin de deduplicar objetos ApplicationInfo anidados en ella, pero esto también podría causar que los objetos ApplicationInfo presentes dentro del Bundle sean "squashed", por lo que la lectura de ese no puede diferirse porque entonces esos objetos "squashed" no podrían "des-squash" correctamente

Poner Parcelables en system_server y recuperarlos

Así que ahora queremos que system_server lea nuestro RemoteViews que contiene un Bundle con un LazyValue que falla al deserializar y luego (en otra transacción IPC Binder) nos devuelva ese objeto

Probablemente podría hacerse a través de algunos medios legítimos, como registrarnos como host de widgets de aplicación (pero eso requeriría interacción del usuario para otorgarnos permiso) o publicar una Notification con contentView establecido (pero eso causaría interacción con otros procesos y/o sería visible para el usuario; y preferí evitar ambas opciones)

He decidido en su lugar crear un MediaSession y llamar a setQueue(List<MediaSession.QueueItem> queue) sobre él para enviar un objeto a system_server y luego recuperarlo a través del método List<MediaSession.QueueItem> getQueue() de MediaController (que se puede obtener mediante MediaSession.getController()). Si bien estos métodos no parecen que pudieran aceptar RemoteViews, en realidad lo hacen gracias al Type Erasure de Java y al hecho de que internamente se implementan usando operaciones de serialización genéricas sobre List

Sin embargo, no estoy usando estos métodos del SDK; estoy escribiendo manualmente los datos para las transacciones Binder subyacentes (porque necesito escribir y luego leer datos serializados malformados), así que echemos un vistazo a cómo funcionan estos métodos

Ambos métodos tenían que tener en cuenta que el tamaño total de la cola podría exceder el tamaño máximo de la transacción Binder, por lo que la transferencia se puede dividir en múltiples transacciones

Enviar la "cola" a system_server normalmente funciona de la siguiente manera:

  1. MediaSession.setQueue() primero llama a ISession.getBinderForSetQueue()
  2. En el lado de system_server, ese método construye y devuelve un objeto ParcelableListBinder
  3. Después, MediaSession.setQueue() llama a ParcelableListBinder.send(), que enviará los contenidos de la lista al Binder proporcionado, posiblemente en múltiples transacciones:
    • La primera transacción contiene al principio el número total de elementos que estarán en la lista, antes de los contenidos de la primera parte
    • Luego, por cada elemento transferido se escribe 1 y el elemento real se escribe mediante (que escribe el nombre de la clase que se envía y luego llama a para enviar los datos)

Por otro lado, recuperar la "cola" funciona de manera un poco diferente:

  1. MediaController.getQueue() simplemente llama a ISessionController.getQueue() y desempaqueta el ParceledListSlice recibido
  2. En el lado de system_server, getQueue() simplemente envuelve mQueue en un ParceledListSlice y lo devuelve
  3. Toda la lógica de división en múltiples transacciones está dentro de los métodos ParceledListSlice.writeToParcel() y createFromParcel(), en particular, writeToParcel() al alcanzar el límite de tamaño seguro escribe un objeto que permite recuperar los siguientes fragmentos

En cuanto a por qué son diferentes: hay un esfuerzo continuo para asegurarse de que system_server no realice llamadas Binder síncronas salientes a otras aplicaciones, porque si esas llamadas se colgaran, podrían colgar todo system_server. Esto significa que system_server no debería recibir ParceledListSlices. Si bien hay código que advierte sobre transacciones síncronas salientes desde system_server, aún no se pudo hacerlo obligatorio porque todavía hay casos en los que system_server realiza dichas llamadas, por ejemplo, al recibir realmente un ParceledListSlice

Elegir el objetivo de la fuga

Así que ahora tenemos las primitivas necesarias para hacer que system_server ejecute parcel_que_se_enviará_a_nosotros.appendFrom(algun_parcel_reciclado, posición_algo_controlada, tamaño_controlado)

Podríamos intentar aleatoriamente extraer datos de Parcel del sistema o organizar cosas para tomar algo específico

Hay las siguientes consideraciones:* Cuando se llama a Parcel.recycle(), el contenido de ese Parcel se borra. Esto significa que el Parcel del cual queremos copiar datos no debe ser recycle()d, lo que aproximadamente implica que no podemos tomar datos de una transacción Binder que ya ha finalizado.

  • Alternativamente, podríamos tomar datos de algún Parcel de algún Bundle presente en el sistema (esto incluye Intent extras y savedInstanceState de Activity). Por lo general, estos no se recycle()n en absoluto (se limpian mediante el recolector de basura y no regresan al grupo; cuando el grupo se agota, Parcel.obtain() crea nuevos objetos Parcel. Por supuesto, los Parcel a los que mantenemos una referencia no serán GCeados, incluso si el sistema no tiene otro uso para ellos).
  • Los Parcel usados para transacciones Binder entrantes utilizan un grupo separado del resto de los Parcel del sistema. Cuando se realiza una transacción Binder saliente, copia datos a un secundario, o una aplicación usa para sus propios fines, llaman a . Por otro lado, cuando hay una transacción entrante, , que . En ambos casos, luego se usa , que se encarga de . Esto significa que el exploit debe hacer que lea desde un perteneciente al mismo grupo del cual queremos filtrar datos. Antes de decidirme por una variante particular, escribí ambas, por lo que puedes encontrar los métodos y en mi clase .

Al final, decidí intentar capturar el Binder IApplicationThread, que la aplicación envía a system_server cuando se inicia el proceso de la aplicación, y system_server lo usa para indicarle a la aplicación qué componentes debe cargar.

Cuando el proceso de la aplicación comienza inicialmente, una de las primeras cosas que hace es enviar IApplicationThread a system_server mediante la llamada a attachApplication(), y es de esta transacción de la que capturaré ese Binder. Hay otros lugares donde se coloca IApplicationThread en un Parcel, como cuando se pasa para identificación del llamante por parte del sistema al iniciar una actividad (pero no tenía mucho control sobre cuándo la aplicación objetivo hace eso) o cuando el sistema lo envía a la aplicación como parte de la gestión del ciclo de vida de Activity (pero esto se hace en una transacción oneway saliente de system_server y las posibilidades de ganar la carrera contra Parcel.recycle() serían escasas).

Dicho esto, capturar el Binder que system_server recibe durante la transacción attachApplication() tampoco es trivial y hubo algunos problemas que superar.

Rebobinando el Parcel

El primer problema para capturar el Binder IApplicationThread del Parcel del cual se reciben los datos para attachApplication() es que este Binder se encuentra en una dataPosition() bastante temprana/baja, mucho más baja de lo que podría estar nuestro LazyValue en el Bundle dentro de RemoteViews.

Los datos para la transacción attachApplication() consisten solo en un encabezado RPC seguido del Binder IApplicationThread. El encabezado RPC (escrito mediante Parcel.writeInterfaceToken()) consiste en unos pocos ints y el nombre de la interfaz, en este caso "android.app.IActivityManager".

Mientras tanto, para leer el Bundle incrustado en RemoteViews, necesitaríamos superar al menos (se omiten algunos elementos menores):

  • El indicador de presencia del elemento para iniciar readParcelable.
  • El nombre del Parcelable: "android.view.RemoteViews".
  • El objeto ApplicationInfo bastante grande presente en RemoteViews (además, debe no ser null y tener un packageName no null o RemoteViews.writeToParcel() fallará cuando intentemos que este objeto sea enviado de vuelta).
  • Finalmente, llegamos a , que , que construye un , que después de finalmente .

Ahora, en Bundle, solo necesitamos poner una clave String y comienza la lectura de LazyValue, se recuerda la posición en Parcel, pero en este punto está muy por delante de la posición donde estaría el Binder IApplicationThread.

¿Podríamos, al llegar a este punto, rebobinar la posición en el Parcel? En otras palabras, ¿podríamos hacer que se llame a Parcel.setDataPosition() con un valor que apunte a una posición anterior a la actual?

Resulta que sí podemos, gracias a otro error en LazyValue. Este es el código utilizado para leerlo:```java public Object readLazyValue(@Nullable ClassLoader loader) { int start = dataPosition(); int type = readInt(); if (isLengthPrefixed(type)) { int objectLength = readInt(); int end = MathUtils.addOrThrow(dataPosition(), objectLength); int valueLength = end - start; setDataPosition(end); return new LazyValue(this, start, valueLength, type, loader); } else { return readValue(type, loader, /* clazz */ null); } }

root@kitploit:~
([Original en AOSP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804), el constructor `LazyValue` solo asigna parámetros a campos)

La cuestión es que `MathUtils.addOrThrow()` verifica el desbordamiento, [pero está perfectamente bien con valores negativos](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)

Si intentáramos hacer `Parcel.writeValue()` en `LazyValue` con `mLength` negativo (rellenado desde el parámetro `valueLength`), [eso lanzaría una excepción en `appendFrom()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804); sin embargo, dado que estamos durante la lectura de `Bundle` con `Parcel.hasReadWriteHelper()` siendo `true`, todos los `LazyValue` se desempaquetan después de la lectura, y tuvimos que poner intencionalmente un `Parcelable` defectuoso dentro para que permanezca como `LazyValue`. Si ponemos datos válidos empaquetados en la posición donde está `LazyValue`, se desempaquetará y, como se señaló anteriormente, la longitud no coincidente solo activará un mensaje en `logcat`. Este exploit en particular establece el tipo en `VAL_MAP` y el número de pares clave-valor en cero. En `logcat` al leer ese valor podemos ver el siguiente mensaje: "`E Parcel  : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP  consumed 4 bytes, but -540 expected.`"

(También `LazyValue` con longitud negativa especificada se puede usar (sin usar otros errores descritos en este informe) para crear un `Bundle` que se cambia a sí mismo, la cosa que `LazyValue` fue creado para eliminar. Pero esa es otra historia (y reportada por separado a Google), en este exploit apunto a más)

Entonces, ¿cuánto queremos retroceder?

Después de que ocurra la llamada `setDataPosition()`, la lectura continuará al siguiente par clave-valor en `Bundle`, por lo que necesitamos elegir una posición donde tendremos:

1. Clave del Bundle, leída usando `Parcel.readString()`, puede ser prácticamente cualquier cosa, incluso apuntar a una longitud inválida (negativa o que exceda el tamaño total de `Parcel`), en ese caso `readString()` devolvería `null`, lo cual es una clave válida en `Bundle`
2. Tipo de valor, este debe ser uno de los [tipos para los cuales `isLengthPrefixed()` devuelve `true`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804)
3. Longitud del valor, esta también debe ser un valor controlado por nosotros. `Parcel.appendFrom()` fallará si la longitud no está alineada o excede el tamaño total de la `Parcel` fuente

Entonces, ¿qué posición en `Parcel` podría ser esa, considerando que los mismos datos ya fueron leídos y son necesarios para llegar a este punto?

* No antes del nombre de `Parcelable` (`"android.view.RemoteViews"`), porque no hay suficiente espacio
* No dentro del nombre de `Parcelable`, porque no podemos establecer el tipo y la longitud
* No directamente después del nombre de `Parcelable`, porque lo primero en `RemoteViews` es `mode`, que [debemos establecer en `MODE_NORMAL` para llegar a nuestro código](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* No después de eso, porque eso está más allá del punto donde está el `Binder` de `IApplicationThread`

Hmm, no hay un buen lugar cuando `RemoteViews` es el objeto más externo en los datos empaquetados

Necesitamos encontrar algún otro `Parcelable` que:

1. Tenga en o cerca del comienzo un lugar donde podamos poner datos arbitrarios (por ejemplo, `int`s o `String`s que son solo datos y no afectan el proceso de serialización)
2. Pueda contener `RemoteViews` (ya sea directamente o mediante un `readParcelable` arbitrario)
3. No tenga un nombre de clase completamente calificado demasiado largo, porque todavía estamos limitados en tamaño por la posición en la que se encuentra `IApplicationThread` en la `Parcel` de destino

Así que tomé la lista de clases `Parcelable` en el sistema, la ordené por longitud ascendente del nombre de clase completamente calificado y comencé a verificar los elementos de esa lista para ver si cumplen la condición 2

De esa manera llegué a [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216), que es lo que usa este exploit. Ahora el proceso de lectura de nuestro objeto preparado desde `Parcel` es el siguiente:

* [Bandera de presencia del elemento](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) para iniciar `readParcelable`
* [Nombre de `Parcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804): `"android.os.Message"`
* Pocos [`int`s que podemos establecer a los valores que queramos](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216) se leen en campos
* Llegamos a la llamada `readParcelable()`, que recorre todo lo descrito anteriormente a través de `RemoteViews` y comienza a leer `Bundle` con `Parcel.hasReadWriteHelper` siendo `true`
* Ese `Bundle` declara tener dos pares clave-valor. En el primer valor tenemos un `LazyValue` con longitud negativa, que dispara `Parcel.setDataPosition()` a la posición donde está el `String` `"android.os.Message"`
* La lectura continúa al segundo par clave-valor; la clave es `"android.os.Message"` y el tipo, longitud y datos de `LazyValue` se toman de los `int`s descritos en el tercer punto. ¡Tengo un `LazyValue` con los `mPosition` y `mLength` que quería!
* Después de leer los `LazyValue`, se desempaquetan. El de tamaño negativo se desempaqueta exitosamente y se reemplaza con un `Map` vacío, mientras que el otro falla en la deserialización, pero esa excepción es capturada y el `LazyValue` simplemente permanece en `Bundle`
* `readParcelable()` termina, pero eso no es el fin de los datos de `Message`. `Message.readFromParcel()` ahora continúa leyendo datos después del retroceso y ve datos que fueron escritos inicialmente como parte de `RemoteViews`. Si algo lanza una excepción en este punto, todo el plan se frustra
* Primera posible excepción: [hay una llamada a `readBundle()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216). [`Bundle` tiene un valor mágico y si es incorrecto se lanzará una excepción](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). Sin embargo, ese valor mágico no está presente si la longitud [es cero](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91) o [negativa](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804) y eso resultó ser el caso cuando la longitud de los datos de `LazyValue` se estableció al valor que necesitaba para capturar `IApplicationThread`. Así que simplemente tuve suerte aquí
* El siguiente posible problema podría ser [la llamada a `Messenger.readMessengerOrNullFromParcel()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216). Esto es en realidad un objeto `Binder` envuelto. La lectura de ese `Binder` falla, porque `Binder` es un objeto especial en `Parcel` y debe ser anotado fuera de banda para ser leído. Este problema es [detectado y registrado por `Parcel` en el lado nativo; sin embargo, esto no se propaga como error y simplemente se devuelve `null`](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)

# Bloqueando `attachApplication()`

Bien, entonces en el paso anterior hemos creado con éxito un objeto que nos permitirá capturar el objeto `IApplicationThread` mientras el método `attachApplication()` se está ejecutando

El asunto es que ese método se completa rápidamente y nuestras posibilidades en una competencia justa contra su finalización serían bastante escasas

Sin embargo, ese método adquiere algunos mutex (mediante el uso de bloques `synchronized () {}` de Java); si logramos adquirir uno de esos mutex y bloquearnos allí, este método también se bloqueará

Ahora volvamos a algunas cosas que ya se dijeron en este informe y que serán útiles para este propósito:

* `Bundle` realiza la deserialización de los valores contenidos en él cuando se accede a esos valores
* Existe la clase `ParceledListSlice` que, durante la deserialización, realizará una llamada `Binder` de salida bloqueante al objeto especificado dentro de los datos serializados

Sumando todo esto: si encontramos en `system_server` un lugar donde se accede al contenido de un `Bundle` proporcionado por la aplicación bajo un mutex que también es utilizado por `attachApplication()`, podremos bloquear `attachApplication()` hasta que finalice la transacción `Binder` realizada a nuestro proceso

[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) es una clase que describe varios parámetros relacionados con el inicio de una `Activity` (por ejemplo, animación). A diferencia de otras clases que describen parámetros pasados a `system_server`, esta no implementa `Parcelable`, sino que proporciona un método para convertirla a `Bundle`

En el lado de `system_server`, ese [`Bundle` se convierte de nuevo a `ActivityOptions`, lo que dispara la deserialización](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). Encontré un lugar donde [esa operación se realiza mientras se mantiene el mutex `ActivityTaskManagerService.mGlobalLock` en `ActivityTaskManagerService.moveTaskToFront()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)

Así que llamo a [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle)), pasando un `Bundle` que contiene `ParceledListSlice` en lugar del valor con el tipo esperado. Ese [`ParceledListSlice` realiza una llamada `Binder` a mi proceso](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577) y hasta que yo regrese de esa llamada, el mutex `ActivityTaskManagerService.mGlobalLock` permanecerá bloqueado

# Creando múltiples `LazyValue`s apuntando a diferentes `Parcel`s

`Parcel.recycle()` y `Parcel.obtain()` funcionan de manera [Último en Entrar, Primero en Salir](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))

Esto significa que si creo un `LazyValue` manipulado cuando no se está ejecutando ninguna otra transacción `Binder` hacia `system_server`, obtendré un `LazyValue` que apuntará a la `Parcel` que siempre se usa cuando solo hay una transacción entrante a `system_server` (hasta que ocurra que dos transacciones concurrentes entrantes a `system_server` comiencen y terminen en un orden distinto al de pila)

Como no tengo control sobre qué otras transacciones están entrando a `system_server`, para mejorar la confiabilidad del exploit he creado múltiples `LazyValue`s apuntando a varias `Parcel`s

Dado que tengo la capacidad de disparar una transacción `Binder` síncrona a mi proceso desde `system_server`, he usado esa capacidad para crear `LazyValue` en varios niveles de recursión entre mi proceso y `system_server` (aunque esta vez lo hice sin mantener un mutex global)

Entonces:

* Creo un `LazyValue`
* Disparo una llamada a `system_server`, `system_server` me llama de vuelta
    * Creo un `LazyValue`
    * Disparo una llamada a `system_server`, `system_server` me llama de vuelta
        * Creo un `LazyValue`
        * Disparo una llamada a `system_server`, `system_server` me llama de vuelta
            * ...

Luego, una vez que tengo suficientes `LazyValue`s, termino de hacer eso, regreso de todas esas llamadas y todas las `Parcel`s que fueron reservadas por estas llamadas se reciclan (`recycle()`)

Cada uno de los `LazyValue`s que hice está envuelto en un [`ParceledListSlice` separado creado por `getQueue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804) y puedo llamar al `Binder` de `ParceledListSlice` para hacer que `system_server` lo serialice y lo envíe a mi proceso

(Una forma alternativa de hacerlo sería crear múltiples `MediaSession`s)

# Iniciando el proceso de la aplicación objetivo

Ahora tenemos todo lo necesario para capturar `IApplicationThread` de `attachApplication()` cuando ocurra, pero aún necesitamos hacer que `attachApplication()` ocurra

En general, [hay algunos tipos de componentes de aplicación con los que otra aplicación puede interactuar](https://developer.android.com/guide/components/fundamentals#Components), cada uno de los cuales requiere que se inicie el proceso de la aplicación

Quería iniciar la aplicación de Configuración del sistema (que [se ejecuta bajo el uid del sistema](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) y por lo tanto tiene acceso a [todo lo que está detrás de los permisos de Android](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b))

Inicialmente intenté iniciarla a través de `startActivity()`, pero cuando lo intenté, el proceso no se inició hasta que liberé el bloqueo de `ActivityTaskManagerService`. Los detalles de por qué fue así están en la sección "Nota adicional: llamadas `Binder` y reentrancia de mutexes", pero como solución decidí solicitar al sistema un [`ContentProvider` de esa aplicación](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) en lugar de una `Activity`. Esto tuvo la ventaja adicional de evitar interferencias con mi interfaz de usuario

No he usado la [API oficial `ContentResolver` expuesta por el SDK](https://developer.android.com/reference/android/content/ContentResolver), sino que he usado [una interna del sistema, ya que necesitaba una API asíncrona](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907) porque la vinculación al `ContentProvider` no terminaría hasta que `attachApplication()` (que estoy bloqueando) finalice, aunque iniciar otro hilo podría ser una alternativa

(No importa lo que ofrezca este `ContentProvider` en particular; lo único relevante es que puedo establecer una conexión con él)

Así es como inicio el proceso de la aplicación Configuración. Me aseguro de que no esté ya en ejecución en primer lugar usando el [método oficialmente disponible `ActivityManager.killBackgroundProcesses()`](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))

# Poniendo todo junto

Ahora que las primitivas están descritas, aquí está cómo funciona todo junto (esto es básicamente una transcripción del método `MainActivity.doAllStuff()` de este exploit):

1. Habilitar el acceso a API ocultas (las API ocultas no son un límite de seguridad y ya existen [soluciones alternativas disponibles públicamente](https://www.xda-developers.com/bypass-hidden-apis/), aunque aquí he usado un método basado en [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String)), que no había visto en otro lugar)
2. (Solo si estamos re-ejecutando el exploit después del primer intento) Liberar la conexión al `ContentProvider` que establecimos en el paso 6 durante la ejecución anterior. Tenemos que hacerlo porque de lo contrario `ActivityManager.killBackgroundProcesses()` no considerará el proceso objetivo como "en segundo plano" y no lo matará
3. Matar el proceso de la aplicación víctima usando `ActivityManager.killBackgroundProcesses()`, ya que `attachApplication()` solo se llama al inicio del proceso
4. Solicitar a `system_server` que cree un montón de objetos que contengan `LazyValue` apuntando a una `Parcel` que luego se recicla. Obtengo una referencia `Binder` de `ParceledListSlice` para cada objeto que contiene `LazyValue` y puedo hacer una transacción `Binder` hacia ella para disparar que el sistema la escriba de vuelta. Cada creación de objeto `LazyValue` se realiza a diferentes profundidades de llamadas [mutuamente recursivas](https://en.wikipedia.org/wiki/Mutual_recursion) entre `system_server` y mi aplicación para hacer probable que cada uno de estos `LazyValue`s tenga una referencia colgante a un objeto `Parcel` diferente
5. Bloqueo `ActivityTaskManagerService.mGlobalLock` haciendo una llamada a `ActivityTaskManagerService.moveTaskToFront()` pasando como argumento un `Bundle` que, al deserializarse, realiza una transacción `Binder` síncrona a mi proceso. Los siguientes pasos se realizan desde ese callback y, por lo tanto, se hacen con ese bloqueo mantenido
6. Solicito a `ActivityManagerService` una conexión con el `ContentProvider` de la aplicación víctima (notar que no hay "`Task`" en el nombre; `ActivityTaskManagerService` es una clase centrada principalmente en manejar componentes `Activity` de las aplicaciones, mientras que `ActivityManagerService` maneja otros [componentes de aplicación](https://developer.android.com/guide/components/fundamentals#Components) (así como el inicio general del proceso); esta [división ocurrió en Android 10; anteriormente tanto el manejo de `Activity` como de otros componentes de aplicación estaba en `ActivityManagerService`](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. `sleep()` un poco para dar tiempo al proceso recién lanzado para comenzar a llamar a `attachApplication()`
8. Mientras el bloqueo aún está mantenido, solicito a todos los objetos `ParceledListSlice` creados previamente que envíen sus contenidos restantes (los que no cupieron en la transacción inicial), es decir, los objetos que contienen `LazyValue` apuntando a `Parcel` reciclada. Luego, desde un desplazamiento fijo que coincide con la posición de `IApplicationThread` pasado a `attachApplication()`, leo el objeto `Binder`. En este momento solo estoy guardando los `Binder`s recibidos en un `ArrayList` para evitar hacer demasiado con el bloqueo mantenido
9. Este es el final del código que ejecuto desde el callback iniciado en el paso 5. `ActivityTaskManagerService.mGlobalLock` se desbloquea
10. Tengo el `Binder` de `IApplicationThread`. Ahora puedo simplemente usarlo para cargar mi código en la aplicación víctima como se describe en la siguiente sección

# ¿Cómo uso `IApplicationThread`

Como se señaló anteriormente, el `Binder` [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) es enviado por la aplicación a `system_server` cuando el proceso de la aplicación se inicia, y luego `system_server` lo usa para indicarle a la aplicación qué componentes debe cargar

Se asume que este objeto solo se pasa a `system_server` y, por lo tanto, no hay verificaciones basadas en `Binder.getCallingUid()` allí, así que podemos simplemente llamar directamente a los métodos ofrecidos por esa interfaz

He [descrito en mi informe anterior cómo obtengo ejecución de código manipulando los argumentos de `scheduleReceiver()`](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). Ahora la situación es la misma, excepto que esta vez estoy llamando a `scheduleReceiver()` yo mismo, mientras que entonces estaba manipulando la interpretación de los argumentos de una llamada hecha por `system_server`

# Notas adicionales

En esta sección describo algunas cosas que al final no resultaron útiles en este caso, aunque pueden ser características que vale la pena conocer o son posibles errores

## Nota adicional: `Bundle.clear()`Por simplicidad, aquí he descrito el `Bundle` actualizado sin un [commit que se introdujo después, que permite que `Parcel` usado en `Bundle` para respaldar `LazyValue`s sea reciclado llamando a `Bundle.clear()`](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)

Como se indica en el mensaje del commit, se rastrea si `Bundle` es copiado y en ese caso `clear()` no reciclará el `Parcel`

Sin embargo, ese commit también cambia la semántica del parámetro/variable `recycleParcel` de [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)

Anteriormente, `recycleParcel` siendo `false` indicaba que `Parcel` no debería reciclarse, ya sea porque [el llamante estableció `recycleParcel` a `false` para indicar que `Parcel` no es propiedad de `Bundle`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1) o porque fue [establecido a `false` basado en el resultado de `parcelledData.readArrayMap()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)

Ahora las razones por las que `recycleParcel` podría ser `false` son las mismas, sin embargo la interpretación de eso cambió, ahora eso no significa "no recicles este `Parcel`", significa "aplaza el reciclaje de `Parcel` hasta la llamada a `Bundle.clear()`"

Esto significa que si se llamara a `clear()` en un `Bundle` creado con `Parcel.hasReadWriteHelper()` siendo `true`, esto llevaría a que `Parcel` sea reciclado, mientras que el código que invocó la creación de ese `Bundle` también reciclaría ese `Parcel`, llevando a un doble `recycle()`, lo que produce un comportamiento similar a un double-free: las siguientes llamadas a `Parcel.obtain()` devolverían el mismo objeto dos veces

Sin embargo, no he encontrado una forma de que se llame a `clear()` en tal `Bundle`

Desde que escribí esto originalmente, [el comportamiento de `recycle()` fue cambiado y ahora un reciclaje adicional es no-op con posible fallo a través de `Log.wtf()`](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([dependiendo de la configuración, pero nunca fallando `system_server`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056)). Diría que el nuevo comportamiento aún podría ser peligroso, especialmente cuando tenemos la capacidad de detener programáticamente la deserialización que ocurre en otro proceso, pero realmente no hay una buena manera de manejar el doble reciclaje

## Nota adicional: llamadas `Binder` y reentrancia de mutex

Una característica no muy conocida de `Binder` es que soporta el despacho de llamadas recursivas al hilo original

Es decir, si el proceso A hace una llamada `Binder` síncrona al proceso B y luego el proceso B mientras la maneja en el mismo hilo hace una llamada `Binder` síncrona al proceso A, esa llamada en el proceso A será despachada en el mismo hilo que está esperando que la llamada original al proceso B se complete

Otra cosa es que las secciones `synchronized () {}` en Java son mutex reentrantes, lo que significa que si entras dos veces desde el mismo hilo, te dejará entrar y no habrá deadlock

Esto significa que en teoría, mientras mantenemos bloqueado `ActivityTaskManagerService.mGlobalLock`, aún podríamos iniciar la aplicación de Configuración usando `startActivity(new Intent(Settings.ACTION_SETTINGS))` y entraríamos exitosamente en el [bloque `synchonized` que estamos deteniendo](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d), sin embargo iniciar esa `Activity` también implica la creación de `Task`, lo que implica llamar a [`notifyTaskCreated()`, que publica un mensaje](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) a [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547), y [el manejo de eso](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) intenta [adquirir el bloqueo que estamos deteniendo desde otro hilo](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f). Por lo tanto, hasta que liberemos `ActivityTaskManagerService.mGlobalLock`, el hilo `DisplayThread` permanecerá bloqueado. Luego, el procedimiento de iniciar `Activity` implica [publicar un mensaje al mismo hilo para iniciar el proceso de la aplicación](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c). Todo esto significa que en este caso el proceso de la aplicación no se iniciará hasta que liberemos el bloqueo y&nbsp;la&nbsp;razón por la que manteníamos ese bloqueo en primer lugar era para evitar que la transacción `attachApplication()` terminara para poder tomar los handles de ella, pero en este caso esa transacción no se iniciaría realmente

Incluso si lanzamos una `Activity` que será parte de la misma `Task` que la actual (es decir, lanzaríamos una `Activity` diferente de la aplicación de Configuración, una que no especifique `android:launchMode="singleTask"`), ese procedimiento aún implicará [`notifyTaskDescriptionChanged()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=444-450;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f), que tiene el mismo impacto aquí que `notifyTaskCreated()`

Así que aunque mi hilo podría llamar a métodos que usan `synchronized (ActivityTaskManagerService.mGlobalLock) {}`, iniciar un nuevo proceso de aplicación después de `startActivity()` implicaba el uso de ese bloqueo desde un hilo diferente y eso no fue útil en este caso, así que opté por desencadenar el inicio del proceso de aplicación a través de `ContentProvider` en su lugar

## Nota adicional: Otras formas de usar `IApplicationThread`

`IApplicationThread` es un handle muy privilegiado, por lo que considero que hacer uso de él después de obtenerlo es una post-explotación

En este exploit lo he usado directamente para solicitar ejecución de código en el proceso objetivo, aprovechando el hecho de que el acceso a esa operación está controlado por capacidad (posesión del objeto `Binder`, que aquí filtramos) y no por `Binder.getCallingUid()`

Agregar una verificación de `Binder.getCallingUid()` en [`ApplicationThread.scheduleReceiver()` (que usamos aquí para solicitar ejecución de código)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) y otros métodos de `ApplicationThread` (ya que `scheduleReceiver()` no es el único método en `IApplicationThread` que permite la carga de código) aún no evitaría usar `IApplicationThread` para cargar código en el proceso de otra aplicación, ya que el atacante podría pasar el `IApplicationThread` filtrado en lugar del propio a `attachApplication()`

Además de cargar código en el proceso, tener `IApplicationThread` permite realizar [`grantUriPermission()` usando los privilegios del proceso al que pertenece ese handle](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5631-5632;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)
Descargar herramienta
unparcel(boolean itemwise)
que llama a initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
source
mParcelledData
Parcel
Bundle
recycleParcel
true
Parcel
Bundle
Parcel.recycle()
  • initializeFromParcel llama a recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) para leer el contenido del mapa clave-valor. Las claves son Strings y los valores se leen usando readLazyValue(), creando objetos LazyValue para valores de tipos que se escriben junto con el prefijo de longitud. readArrayMap() devuelve un valor que indica si está bien reciclar el Parcel. Si había objetos LazyValue presentes, recycleParcel se establece a false y el Parcel al que se refieren los LazyValue no se reciclará (hay una excepción a esto, pero no es relevante aquí; la describiré en la sección "Nota adicional: Bundle.clear()")
  • Una vez que unparcel() finaliza, mMap se establece (no es null) y mapea claves String a valores reales si están listos o a objetos LazyValue
  • Después, se llama a getValue(), que mapea la clave (String) a un índice (int) y lo pasa a getValueAt()
  • getValueAt() detecta LazyValue mediante instanceof BiFunction y llama a apply() para deserializarlo
  • LazyValue.apply() retrocede el Parcel a la posición de LazyValue.mPosition y llama al normal Parcel.readValue() del cual ya he hablado
  • Tras la deserialización exitosa, LazyValue se reemplaza en mMap, de modo que la próxima llamada a Bundle.get*() para la misma clave devuelva directamente el valor y la deserialización de LazyValue no se repita. Cuando el Bundle se reenvía, ese valor se serializará de nuevo en lugar de copiar los datos originales textualmente (sin embargo, después de leer el Bundle reenviado, ese valor será nuevamente un LazyValue y cualquier posible discrepancia entre writeToParcel/createFromParcel no podrá afectar a otros valores)
  • Parcel
    maybeWriteSquashed()
    system_server
    Bundle
    Parcel.writeParcelable()
    Parcelable.writeToParcel
  • Si nos acercamos al límite del tamaño de la transacción Binder, se escribe 0 para indicar que no hay más elementos en esta transacción y los siguientes elementos se enviarán en otra transacción
  • Una vez que ParcelableListBinder ha recibido el número de elementos que se especificó en la primera transacción, invoca la lambda pasada a su constructor, que en este caso asigna la lista recuperada a MediaSessionRecord.mQueue
  • Binder
  • Cuando se lee ParceledListSlice desde un Parcel, lee la primera parte directamente del Parcel y luego, si no todos los elementos se escribieron en línea, llama al Binder que se escribió en el Parcel para recuperar esos elementos
  • Bundle
    Parcel
    Parcel
    Parcel.obtain(), que usa Parcel.sOwnedPool
    Binder
    el sistema llama a Parcel.obtain(long obj)
    usa Parcel.sHolderPool
    Parcel.recycle()
    devolver el objeto Parcel al grupo correspondiente
    RemoteViews
    Parcel
    makeOwnedLeaker
    makeHolderLeaker
    ValueLeakerMaker
    RemoteViews.readActionsFromParcel()
    llama a getActionFromParcel()
    ReflectionAction
    leer los parámetros comunes de BaseReflectionAction
    construye un Bundle