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
ThisSeemsWrong — Writeup y exploit para CVE-2024-49746: Parcel::continueWrite de Android cierra descriptores de archivo que se utilizan más tarde | Kitploit
Herramientas/GitHubGitHub/michalbednarski/thisseemswrong
Seguridad AndroidFrameworks de ExploitsAnálisis de VulnerabilidadesRecopilación de InformaciónDesarrollo de PayloadsExplotación de Binarios
GitHubmichalbednarski/thisseemswrong

ThisSeemsWrong

Writeup y exploit para CVE-2024-49746: Parcel::continueWrite de Android cierra descriptores de archivo que se utilizan más tarde

Ver Repositorio
4715hace 10 mesesRevisado 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

La corrección para este problema apareció como CVE-2024-49746: boletín, parche

"Esto parece incorrecto"

El título anterior es el comentario del método Parcel::continueWrite, que en realidad es el responsable de redimensionar los objetos Parcel, ya sea cuando el usuario lo solicita explícitamente (por ejemplo a través de setDataSize()) o al llamar a uno de los métodos write cuando la capacidad de datos actual es demasiado pequeña```cpp status_t Parcel::continueWrite(size_t desired) { // SNIP: Validate desired size // SNIP: Assign kernelFields & rpcFields from variant member of this class // SNIP: Count number of objects (Binder handles and File Descriptors) // that will be present after resize and assign to objectsSize

root@kitploit:~
if (mOwner) {
    // If the size is going to zero, just release the owner's data.
    if (desired == 0) {
        freeData();
        return NO_ERROR;
    }

    // If there is a different owner, we need to take
    // posession.
    uint8_t* data = (uint8_t*)malloc(desired);
    // SNIP: Check if malloc succeeded
    binder_size_t* objects = nullptr;

    if (kernelFields && objectsSize) {
        objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
        // SNIP: Check if calloc succeeded

        // Little hack to only acquire references on objects
        // we will be keeping.
        size_t oldObjectsSize = kernelFields->mObjectsSize;
        kernelFields->mObjectsSize = objectsSize;
        acquireObjects();
        kernelFields->mObjectsSize = oldObjectsSize;
    }
    // SNIP: rpcFields handling for non-/dev/binder Parcels

    if (mData) {
        memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
    }
    if (objects && kernelFields && kernelFields->mObjects) {
        memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
    }
    // ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
    if (kernelFields) {
        // TODO(b/239222407): This seems wrong. We should only free FDs when
        // they are in a truncated section of the parcel.
        closeFileDescriptors();
    }
    mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
           kernelFields ? kernelFields->mObjectsSize : 0);
    mOwner = nullptr;

    // SNIP: Allocation count tracking
    // SNIP: Assign data and objects to this object
} else if (mData) {
    // SNIP: Resize data owned by this instance of Parcel
} else {
    // SNIP: Allocate initial data for currently empty Parcel
}

return NO_ERROR;

}

root@kitploit:~
[Cuando se introdujo ese comentario](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/), la llamada a `closeFileDescriptors()` se movió de `IPCThreadState::freeBuffer()` (que se invoca en el código anterior a través del puntero de función `mOwner()`) al método `continueWrite()`, aunque la lógica era la misma que antes. Después de todo, `Parcel` es una parte central del IPC de Android y si el IPC central estuviera cerrando descriptores de archivo que no debería, sería un problema evidente

Lo que nos lleva a la parte importante: ¿cuándo se utiliza el código anterior? Se utiliza cuando la clase `Parcel` transfiere la propiedad de los datos recibidos del controlador de Binder (que en ese momento residen en el [`mmap` de `/dev/binder`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) y no se puede escribir en ellos (cualquier intento de escribir en esa memoria provocaría un `SIGSEGV`)), es decir, que `Parcel` sea o bien los datos de transacción entrantes (el argumento `data` pasado a [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))) o bien la respuesta entrante (es decir, el objeto `Parcel` que se pasó a la llamada [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) como argumento `reply`; `transact()` establece la referencia dentro de ese objeto `Parcel`)

En la práctica, el único caso en el que entraríamos en el bloque `if (mOwner)` es cuando el sistema [llama a `setDataSize(0)` para liberar los datos de transacción](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567), pero en ese caso también entraríamos en `if (desired == 0)`, que hace un retorno anticipado. Durante el uso legítimo del sistema no existe ningún caso en el que entremos en la ruta "Si hay un propietario distinto, debemos tomar posesión"

# Activar la ruta "tomar posesión"

En uno de mis exploits anteriores mostré el [caso en el que `createFromParcel()` puede realmente llamar a `writeInt(0)` sobre el `Parcel` del que debería estar leyendo](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects). Si bien la corrección aplicada allí impidió la ejecución de cualquier método `createFromParcel()` que no fuera de `Intent` dentro de `AccountManagerService`, la ruta de `createFromParcel()` a `writeInt(0)` se mantuvo intacta

Para recapitular, [dentro de `PackageParser` tenemos el siguiente código](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> cons = cls.getConstructor(Parcel.class);

intentsList = new ArrayList<>(N);
for (int i = 0; i < N; ++i) {
    intentsList.add(cons.newInstance(in));
}

Por lo tanto, podemos tener un objeto Parcel que se pasó a createFromParcel pasado a cualquier constructor public disponible en el sistema que acepte un único argumento Parcel

Y en otro lugar tenemos el siguiente código:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }

root@kitploit:~
Por lo tanto, para activar la ruta de "take possession", necesitamos tener una llamada arbitraria a `readParcelable` en un `Parcel` que se pasó a `onTransact()` como `data`; en este exploit estoy usando para eso [la misma ruta que usé previamente en otro](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them). Hago que `readParcelable` llame a `PackageParser$Activity.CREATOR.createFromParcel()`, que a su vez lee el nombre de `PooledStringWriter` y llama a su constructor; después de eso, los datos del `Parcel` terminan, por lo que `writeInt()` necesita reasignar el Parcel, entrando en nuestra ruta de "take possession".

Cabe señalar aquí que, si no hubiera fin de los datos de `Parcel` en ese punto, `writeInt()` intentaría sobrescribir los datos en el lugar, lo que, en el caso de datos respaldados por `/dev/binder` `mmap`, provocaría un `SIGSEGV`.

# File Descriptor Sanitizer

Mi idea inicial era hacer que la ruta de "take possession" cerrara los File Descriptors, después de lo cual, al final de la transacción, esos mismos Descriptors se cerrarían de nuevo, pero entre esas cosas, colocaría otro File Descriptor dentro de `system_server` en otra transacción y, más tarde, recuperaría mi File Descriptor, ya que ese FD se refiere en ese momento a un archivo diferente.

Eso funcionó en mi emulador con una versión antigua de AOSP; sin embargo, cuando probé con una versión más reciente, ese plan fue detenido por [File Descriptor Sanitizer (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md).

En particular, [en `android-14.0.0_r29`, la cobertura de FDSan se amplió para cubrir FDs dentro de `Parcel`](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/).

De hecho, después de que FDSan cubriera Parcel, ni siquiera pude llegar a la llamada `closeFileDescriptors()` cuando el `Parcel` contenía FDs. Antes de esa llamada, hay una llamada a `acquireObjects();`, que adquiere referencias a manejadores de `Binder` (que son liberados por la llamada a `mOwner()` más adelante dentro de esa función); sin embargo, `acquireObjects()` también [establece etiquetas FDSan para FDs](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe):```cpp
case BINDER_TYPE_FD:
    if (obj.cookie != 0) { // owned
        FdTag(obj.handle, nullptr, who);
    }

La cosa es que nosotros ya etiquetamos los FDs recibidos del kernel```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }

root@kitploit:~
Por lo tanto, llamar dos veces a `FdTag` (sin cerrar ni cambiar la etiqueta al especificar la etiqueta antigua esperada) provoca un error de FDSan, que aborta el proceso, y todavía no hemos llegado siquiera a la llamada `closeFileDescriptors()`. Dado que la ruta de "toma de posesión" es código muerto durante el uso normal, estos problemas podrían pasar desapercibidos.

Sin embargo, podemos ver la condición `if (obj.cookie != 0)`. Si eso es falso, significa que el FD presente dentro del `Parcel` no es realmente propiedad de ese `Parcel` y es responsabilidad del usuario del `Parcel` mantenerlos abiertos mientras exista ese `Parcel`. Sin embargo, como este `Parcel` acaba de provenir del kernel, los valores de `cookie` en realidad provienen del proceso original y se consideran irrelevantes cuando el `Parcel` realmente tiene `mOwner`. Pero la ruta de "toma de posesión" en realidad no considera eso y simplemente copiará los valores de `cookie`.

Combinando todo eso, al establecer los valores de `cookie` a cero en el lado del emisor, podemos obtener un `Parcel` que referencia descriptores de archivo que fueron cerrados, pero que además no se considera propietario de ellos, lo que significa que no los cerrará de nuevo. Eso nos permite evitar desencadenar FDSan, aunque también elimina cualquier ruta de explotación por doble cierre.

# Trucos en el lado Java de Parcel

Tales FDs aún podrían pasarse a otro `Parcel` (y luego a otro proceso), sin embargo nuestra forma de provocar la creación de tales FDs colgantes implica la construcción de `PooledStringWriter` mediante reflexión, después de lo cual se lanza una `ClassCastException` cuando intentamos `add()` a `ArrayList<IntentInfo>`.

Necesitaríamos:

* Estar al final del `Parcel` que se pasó como argumento `data` a `onTransact()`
* Realizar la construcción de `PooledStringWriter`, después de lo cual el Parcel tendrá FDs colgantes, pero también se lanzará `ClassCastException`
* Esperar a que los FDs que queremos filtrar se asignen dentro de `system_server`
* Hacer que los FDs de ese `Parcel` se copien a algún otro `Parcel` que se enviaría a nuestro proceso

Para cumplir todos estos requisitos necesitaré usar algunos trucos tanto antiguos como nuevos.

## Trucos antiguos

Comencemos revisando los trucos antiguos; la mayoría ya fueron descritos en mi exploit [`LazyValue` que usa `Parcel` después de `recycle()`](https://github.com/michalbednarski/LeakValue).

1. [La clase `RemoteViews` realiza la deserialización del `Bundle` contenido con `Parcel.ReadWriteHelper` establecido](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4). [Cuando `ReadWriteHelper` está establecido, los `Bundle`s no se deserializan de inmediato, sino de forma diferida](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545). También vale la pena señalar en este punto que todos los `Bundle`s contenidos dentro de ese `Bundle` bajo `RemoteViews` también se deserializarán inmediatamente, no solo el que está directamente dentro de `RemoteViews`.
2. [Si se lanza `BadParcelableException` mientras se deserializa un `Bundle` dentro de `system_server`, esa `BadParcelableException` se capturará silenciosamente y el contenido del `Bundle` se borrará](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e). Ten en cuenta, sin embargo, que nuestro desencadenante de escritura en `createFromParcel` lanza `ClassCastException`, la cual no se capturará aquí.
3. [La clase `ParceledListSlice` durante la deserialización realiza una llamada Binder saliente bloqueante al objeto especificado en los datos serializados](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364), que podemos usar para retrasar la ejecución de la deserialización.

Todos estos serán necesarios ahora, pero eso no es todo.

## Trucos nuevos

[AIDL es una herramienta para generar implementaciones de interfaces RPC](https://developer.android.com/guide/components/aidl); sin embargo, además de eso, también es capaz de generar implementaciones de estructuras `Parcelable`.

Estas estructuras tienen prefijo de longitud, por lo que diferentes versiones de la misma estructura son compatibles dentro del sistema siempre que no se añadan campos en el medio (es decir, las versiones son compatibles si una versión es prefijo de la otra).

Echemos un vistazo al código que AIDL ha generado para la [estructura `ReceiverInfo`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl):```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
  int _aidl_start_pos = _aidl_parcel.dataPosition();
  int _aidl_parcelable_size = _aidl_parcel.readInt();
  try {
    if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    data = _aidl_parcel.readString();
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
    // SNIP: Other fields
  } finally {
    if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
      throw new android.os.BadParcelableException("Overflow in the size of parcelable");
    }
    _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
  }
}

Este código nos permitirá hacer los dos trucos restantes, ambos de los cuales necesitaremos para realizar acciones adicionales después de activar la ruta de "tomar posesión", que requiere que estemos al final del Parcel y lanza ClassCastException.

Primero, estamos leyendo la longitud del Parcel, la usamos para omitir campos que no están presentes en la versión que se escribió y al final ajustaremos la posición dentro del Parcel según esa longitud. No se nos permite movernos antes de la posición de ese Parcelable de AIDL (y moverse más allá del final del Parcel es posible, aunque en realidad no hace nada peligroso). Sin embargo, podemos movernos hacia el medio de un objeto ya leído, por lo que durante la primera lectura podríamos llegar al final del Parcel, tener PooledStringWriter construido y luego retroceder hasta el medio de algo que está dentro de este objeto ReceiverInfo.

Eso se encarga del problema de estar al final del Parcel; todavía queda el problema de ClassCastException. Aquí, sin embargo, tenemos un bloque finally que, antes de llamar a setDataPosition(), valida que no haya desbordamiento. Si hay un desbordamiento, se lanzará una BadParcelableException. Ahora, ¿qué sucede si el bloque finally lanza una Exception cuando hay otra pendiente? La Exception lanzada dentro de finally tiene prioridad; la Exception anterior desaparece silenciosamente. Ahora, en lugar de ClassCastException, tenemos BadParcelableException, que Bundle ignora amablemente para evitar dentro de .

Nota sobre el parche de ParcelableListBinder

Estamos usando ParcelableListBinder de MediaSession para poner un Parcelable arbitrario dentro de system_server y luego recuperar ese objeto.

Recientemente hubo un parche de ParcelableListBinder que prohíbe exactamente eso.

Desde el punto de vista de este exploit, este parche en realidad no me detiene, pero necesito manejar su presencia y ausencia de manera diferente.

Si ese parche está presente, los elementos que no son QueueItem se eliminan silenciosamente de la lista que recibe system_server (no causan Exception). Dado que necesitamos un no-QueueItem para los efectos secundarios, podemos poner un no-QueueItem como primer elemento y luego seguirlo con un QueueItem real, lo cual no nos permite realizar una deserialización arbitraria de Parcelable, pero sí contiene un Bundle que puede contener descriptores de archivo (que querríamos que se pasen a nuestro proceso).

Si ese parche está ausente, no tenemos ese obstáculo; sin embargo, tampoco podemos usar el mismo flujo. Dado que en el caso anterior tendríamos una lista con tanto RemoteViews como QueueItem, ParceledListSlice rechazaría la transferencia de una lista de tipos mixtos. En ese caso necesito poner nuestros FDs filtrados dentro de una instancia de RemoteViews.

Pero lo más interesante de este parche es la razón por la que se implementó: el mensaje del commit menciona "permitir que las aplicaciones se inicien en segundo plano" y, aunque Android Security Bulletin no dice mucho, podemos encontrar información útil en la entrada de CVE, que se refiere al campo Notification.mAllowlistToken, que durante la lectura se puede tomar de un campo estático, que dentro de system_server es un token que permite inicios de Activity en segundo plano y más tarde ese token se escribiría en Notification.writeToParcel(). ¿Significa eso que ahora todos los casos en los que system_server deserializa un Parcelable arbitrario y lo envía de vuelta a la aplicación son vulnerabilidades? De todos modos, por ahora eso es solo una idea; en este exploit quiero hacer más de todas formas.

Poniendo todo junto

Creo que este exploit incluye la cadena de gadgets Parcelable más compleja que he hecho:

  • RemoteViews (1)
    • ReflectionAction (2)
      • Bundle
        • Parcelable[] (3)
          • ReceiverInfo (para seek, 4 y 10)
            • Intent
              • ComponentName (segmento A, 5)
                • Descriptores de archivo de relleno opcionales
                • ParceledListSlice (11)
                • ParcelableParcel o QueueItem (12)
              • Bundle (para capturar, segmento B, 6)
                • ReceiverInfo (para relanzar, 7)
                  • Bundle (8)

Las anotaciones "segmento A" y "B" en la lista anterior se refieren a bloques entre los comentarios "START A"/"END A"/"START B"/"END B" en mi clase FdLeaker.java; los números se refieren a puntos en la lista siguiente.

El árbol anterior describe la jerarquía desde la perspectiva del emisor; desde la perspectiva del receptor, sin embargo, se ve un poco diferente:

  1. Estamos recibiendo datos de ParcelableListBinder; el objeto más externo es RemoteViews.
  2. En ese RemoteViews hay un Bundle anidado. RemoteViews establecerá Parcel.ReadWriteHelper para que este Bundle y todos los Bundle dentro de él se lean de inmediato. Esto es necesario porque de lo contrario no podríamos ejecutar un readParcelable arbitrario desde ReceiverInfo.readFromParcel().
  3. Parcelable[] es solo un envoltorio de conveniencia aquí para agrupar todos los Parcelable que estoy poniendo dentro de .

Entonces, ¿qué descriptores de archivo podemos tomar?

Echemos un vistazo al ataque anterior a alto nivel:

  1. Se abre un descriptor de archivo dentro de system_server, se guarda una referencia a él y el FD se cierra.
  2. Hago que system_server abra un descriptor de archivo diferente.
  3. El descriptor de archivo colgante que creé en el paso 1 me es devuelto.

Hay una limitación significativa de este ataque: no podemos tomar descriptores de archivo que se abrieron antes de que nuestro ataque haya comenzado.

Sin embargo, todavía hay algunas cosas útiles que podríamos hacer.

Capturar InputChannel

Para ser honesto, esta es la única variante del exploit que he logrado hacer funcionar sin suposiciones adicionales.

Los eventos de entrada, es decir, los eventos de la pantalla táctil y del teclado, son recibidos por la aplicación a través de un socket UNIX de system_server. Cuando una Activity se inicia o añade una nueva ventana al sistema, se crea un nuevo InputChannel, lo que a su vez significa que se crea un par de sockets UNIX y uno de los extremos se envía a la aplicación, y el otro es usado por system_server para enviar eventos.

El diseño de las estructuras enviadas a través de estos sockets está bien definido (ya que deben ser compatibles entre procesos de 32 bits y 64 bits) y parece que nada se queja por números de secuencia inesperados. Además, InputChannel parece ser el único socket asignado dentro de system_server después de la llamada a startActivity(), por lo que se puede determinar fácilmente qué FD es el socket del lado del servidor de InputChannel.

Captura de pantalla de la pantalla de configuración "Acerca del teléfono" con el diálogo "Nombre del dispositivo", en el que se escribe "key injection demo"

Si bien este es un ejemplo trivial, también podríamos aprobar solicitudes de permisos o la instalación de aplicaciones, habilitar Media Projection o el Servicio de Accesibilidad.

Capturar la conexión a zygote durante el arranque

Esta es principalmente teórica; pude realizar este ataque en un emulador que corría lentamente; sin embargo, en un dispositivo real la ventana de carrera era demasiado pequeña.

system_server abre la conexión a /dev/socket/zygote solo una vez al inicio; después de eso, todas las solicitudes se envían usando esa conexión.

Durante el arranque de system_server, MediaSessionService (que se usa para enviar y recibir Parcelable hacia/desde system_server) se publica en servicemanager antes de que se establezca la conexión con zygote.

Por lo tanto, teóricamente es posible que una aplicación lance un proceso secundario, haga que system_server se bloquee y luego, desde ese proceso en segundo plano, realice el ataque durante el arranque de system_server.

Capturar la conexión a zygote a través de SensorService

También hay otro bug que encontré: aquí está el método SensorService::createSensorDirectConnection()```cpp sp SensorService::createSensorDirectConnection( const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format, const native_handle *resource) { // SNIP: Reject direct connections when sensor privacy is enabled // SNIP: Irrelevant parameter checks

root@kitploit:~
// check specific to memory type
switch(type) {
    case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
        if (resource->numFds < 1) {
            ALOGE("Ashmem direct channel requires a memory region to be supplied");
            android_errorWriteLog(0x534e4554, "70986337");  // SafetyNet
            return nullptr;
        }
        // SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
    }
    case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
        // no specific checks for gralloc
        break;
    default:
        ALOGE("Unknown direct connection memory type %d", type);
        return nullptr;
}

native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
    return nullptr;
}
native_handle_set_fdsan_tag(clone);

sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
    // SNIP: usual case where sensor belong to this device (not app streaming)
} else {
    auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
    if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
        ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
    } else {
        int fd = dup(clone->data[0]);
        channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
    }
}
// SNIP: Return connection

}

root@kitploit:~
Tenemos una llamada `dup(clone->data[0])`. `clone` es un `native_handle_t` recibido de un proceso remoto. El native handle contiene un cierto número de FDs y un cierto número de enteros simples dentro de data, cuyas cantidades el usuario del native handle debe verificar [mirando `numFds` y `numInts`](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e) antes de acceder a `data`

Aquí incluso tenemos una sección "check específico del tipo de memoria" que comprueba que, para `SENSOR_DIRECT_MEM_TYPE_ASHMEM`, esto se verifica aquí; para `SENSOR_DIRECT_MEM_TYPE_GRALLOC`, el formato del native handle es específico del dispositivo y no se puede validar aquí. La cuestión es que, para los "sensores en tiempo de ejecución", el tipo siempre se trata como `SENSOR_DIRECT_MEM_TYPE_ASHMEM`, pero podemos especificar `SENSOR_DIRECT_MEM_TYPE_GRALLOC` para omitir la validación en ese caso.

Sin embargo, este código solo se puede alcanzar si hay "sensores en tiempo de ejecución" presentes. No estoy seguro de en qué caso ocurre realmente; creo que es cuando el usuario usa "Nearby app streaming" (?)

Sin embargo, para pruebas, estoy añadiendo una pequeña clase que permite el registro de [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary), puedes usarla a través de```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'

Después de eso, es posible ejecutar código como uid de sistema en el dispositivo de producción

Aplicación mostrando texto largo con lista de descriptores de archivo, al final de la cual tenemos, "device=2 fd=181", "Found zygote, sending request", "uid=1000(system) gid=1000(system), groups=1000(system),1065(reserved_disk),3009(readproc) context=u:r:system_app:s0"

Descargar herramienta
Exception
system_server
  • PackageParser$Activity (9)
    • PooledStringWriter
RemoteViews
  • El ReceiverInfo más externo tiene una longitud definida que cae en medio de sus datos; sin embargo, durante la lectura eso aún no se conoce y Intent se lee de él normalmente.
  • Lo que necesitaremos leer más tarde se lee a través de la llamada readString realizada por static ComponentName.readFromParcel() (usando ComponentName porque usa readString UTF-16 independientemente de la versión de Android; ese readString seguirá devolviendo null porque esa cadena se superpone con objetos Binder, por lo que, si bien la construcción de ComponentName ha saltado sobre datos ocultos en esta pasada, el objeto ComponentName no se crea.
  • Entramos en el segundo Bundle anidado; al final de la deserialización de este Bundle se capturará una BadParcelableException.
  • Luego entramos en el segundo ReceiverInfo. Este ReceiverInfo tiene una longitud especificada como Integer.MAX_VALUE y, por lo tanto, lanzará BadParcelableException en el bloque finally, descartando silenciosamente la ClassCastException.
  • Tercer Bundle. Esto es solo para poder alcanzar un readParcelable arbitrario desde ReceiverInfo, lo cual ahora podemos hacer ya que estamos dentro de RemoteViews, por lo que el Bundle se lee de inmediato.
  • La combinación PackageParser$Activity + PooledStringWriter dispara una llamada writeInt(0) sobre el Parcel que se está leyendo. Como estábamos al final del Parcel, writeInt() debe expandir la capacidad del Parcel, activando la ruta de "tomar posesión". Esta combinación también conduce a una ClassCastException, que es engullida como se describe en los pasos 7 y 6 anteriores.
  • Llegamos al final del ReceiverInfo externo; ReceiverInfo se desplaza a la posición según la longitud en su cabecera y cae dentro de datos que estaban previamente dentro de ComponentName, los cuales se leen como los siguientes elementos en Parcelable[].
  • Hay un ParceledListSlice que realiza una transacción Binder bloqueante hacia mi proceso. En este punto, los descriptores de archivo definidos en este Parcel se cerraron, pero nada interesante ha ocupado su lugar todavía. Mientras esta deserialización espera el retorno de esta llamada, puedo hacer que el sistema abra algunos descriptores de archivo interesantes que luego me serán enviados.
  • Esta es la parte en la que los descriptores de archivo se toman de este Parcel para enviármelos. Difiere entre los casos en que ParcelableListBinder filtra elementos o no. a. Si ParcelableListBinder no filtra elementos, los FDs se guardan dentro de ParcelableParcel, que de manera similar a Bundle copia los datos del Parcel textualmente usando Parcel.appendFrom(), pero no tiene la lógica especial hasReadWriteHelper(), por lo que hace eso a pesar de estar bajo el Bundle de RemoteViews. b. Si ParcelableListBinder filtra elementos, este es el final de RemoteViews; el objeto RemoteViews es descartado por ParcelableListBinder, pero eso está bien para mí porque los efectos secundarios ya ocurrieron. El siguiente elemento recibido por ParcelableListBinder es QueueItem, que contiene MediaDescription, que a su vez contiene Bundle, que es donde se guardan mis FDs filtrados.