
Writeup e exploit per CVE-2024-49746: il metodo Parcel::continueWrite di Android chiude descrittori di file che vengono successivamente utilizzati.
Fix for this issue appeared as CVE-2024-49746: bulletin, patch
Il titolo sopra è il commento dal metodo Parcel::continueWrite, che in realtà è responsabile del ridimensionamento degli oggetti Parcel, sia quando esplicitamente richiesto dall'utente (ad esempio tramite setDataSize()) o quando si chiama uno dei metodi write quando la capacità attuale dei dati è troppo piccola```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
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;
}
[Quando quel commento è stato introdotto](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/), la chiamata a `closeFileDescriptors()` è stata spostata da `IPCThreadState::freeBuffer()` (che viene chiamata nel codice precedente tramite il puntatore a funzione `mOwner()`) al metodo `continueWrite()`, tuttavia la logica era la stessa di prima. Dopotutto, `Parcel` è una parte fondamentale dell'IPC di Android e se l'IPC principale stesse chiudendo File Descriptors che non dovrebbe, sarebbe un problema ovvio.
Questo ci porta alla parte importante: quando viene utilizzato il codice sopra? Viene utilizzato quando la classe Parcel trasferisce la proprietà dei dati ricevuti dal driver Binder (che a quel punto risiedono in [`/dev/binder` `mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) e non possono essere scritti (qualsiasi tentativo di scrivere in quella memoria porterebbe a `SIGSEGV`)), cioè quando `Parcel` è o dati di transazione in arrivo (argomento `data` passato a [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))) o risposta in arrivo (cioè, l'oggetto `Parcel` che è stato passato alla chiamata [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) come argomento `reply`, `transact()` imposta un riferimento all'interno di quell'oggetto `Parcel`)
In pratica, l'unico caso in cui entreremmo nel blocco `if (mOwner)` è quando il sistema [chiama `setDataSize(0)` per rilasciare i dati di transazione](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567), ma in quel caso entreremmo anche in `if (desired == 0)` che effettua un ritorno anticipato. Durante l'uso legittimo del sistema non c'è alcun caso in cui entreremmo nel percorso "Se c'è un proprietario diverso, dobbiamo prendere possesso"
# Attivare il percorso "take possession"
In uno dei miei precedenti exploit ho mostrato [un caso in cui `createFromParcel()` può effettivamente chiamare `writeInt(0)` su `Parcel` che dovrebbe leggere](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects). Mentre la correzione ha impedito l'esecuzione di qualsiasi metodo `createFromParcel()` non-`Intent` all'interno di `AccountManagerService`, il percorso da `createFromParcel()` a `writeInt(0)` è stato mantenuto intatto.
Per ricapitolare, [all'interno di `PackageParser` abbiamo il seguente codice](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));
}
Pertanto, possiamo avere un oggetto Parcel che è stato passato a createFromParcel passato a qualsiasi costruttore public disponibile nel sistema che accetta un singolo argomento Parcel
E altrove abbiamo il seguente codice:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }
Pertanto, per attivare il percorso di "prendere possesso", abbiamo bisogno di una chiamata arbitraria a `readParcelable` su `Parcel` che è stata passata a `onTransact()` come `data`; in questo exploit sto usando per quello [lo stesso percorso che ho usato in precedenza in un altro](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them). Ho una chiamata `readParcelable` a `PackageParser$Activity.CREATOR.createFromParcel()`, che a sua volta legge il nome di `PooledStringWriter` e chiama il suo costruttore, e dopo di che i dati di `Parcel` finiscono, quindi `writeInt()` deve riallocare Parcel, entrando così nel nostro percorso di "prendere possesso"
Da notare, se non ci fosse la fine dei dati di `Parcel` a quel punto, `writeInt()` tenterebbe di sovrascrivere i dati sul posto, il che nel caso di dati supportati da `/dev/binder` `mmap` porterebbe a `SIGSEGV`
# Sanitizer dei File Descriptor
La mia idea iniziale era quella di far chiudere i File Descriptor dal percorso di "prendere possesso", dopo di che alla fine della transazione gli stessi Descriptor sarebbero stati chiusi di nuovo, ma tra questi eventi avrei messo un altro File Descriptor all'interno di `system_server` in un'altra transazione e in un momento successivo riottenere il mio File Descriptor, poiché quel FD a quel punto si riferisce a un file diverso
Ciò ha funzionato sul mio emulatore usando una vecchia versione di AOSP, ma quando ho provato con una versione più recente, quel piano è stato bloccato dal [Sanitizer dei File Descriptor (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md)
In particolare, [nella versione `android-14.0.0_r29` la copertura di FDSan è stata estesa per coprire i FD all'interno di `Parcel`](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)
Infatti, dopo che FDSan ha coperto Parcel, non sono nemmeno riuscito a raggiungere la chiamata a `closeFileDescriptors()` quando `Parcel` conteneva FD. Prima di quella chiamata, c'è una chiamata a `acquireObjects();`, che acquisisce riferimenti agli handle di `Binder` (che vengono rilasciati dalla chiamata `mOwner()` più avanti in quella funzione), tuttavia `acquireObjects()` imposterà anche [i tag FDSan per i FD](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);
}
Il fatto è che abbiamo già taggato i FD ricevuti dal 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); }
Quindi, un doppio `FdTag` (senza chiudere o cambiare tag mentre si specifica il vecchio tag previsto) porta a un errore FDSan, che interrompe il processo, e non siamo nemmeno arrivati alla chiamata `closeFileDescriptors()`. Poiché il percorso di "presa di possesso" è, durante l'uso normale, codice morto, tali problemi potrebbero passare inosservati
Tuttavia, possiamo vedere la condizione `if (obj.cookie != 0)`. Se è falsa, significa che l'FD presente all'interno della `Parcel` non è effettivamente di proprietà di quella `Parcel` ed è responsabilità dell'utente della `Parcel` mantenerli aperti finché quella `Parcel` esiste. Tuttavia, poiché questa `Parcel` è appena arrivata dal kernel, i valori `cookie` provengono effettivamente dal processo originale e sono considerati irrilevanti quando la `Parcel` ha effettivamente `mOwner`. Ma il percorso di "presa di possesso" non considera effettivamente ciò e copierà semplicemente i valori `cookie`
Mettendo tutto insieme, impostando i valori `cookie` a zero sul lato mittente, possiamo ottenere una `Parcel` che fa riferimento a File Descriptor che sono stati chiusi, ma che non li considera di sua proprietà, il che significa che non li chiuderà di nuovo. Questo ci permette di evitare di inciampare in FDSan, ma elimina anche qualsiasi percorso di sfruttamento di doppia chiusura
# Trucchi sul lato Java di Parcel
Tali FD potrebbero comunque essere passati a un'altra `Parcel` (e poi a un altro processo), tuttavia il nostro modo di innescare la creazione di tali FD pendenti implica la costruzione di `PooledStringWriter` tramite reflection, dopodiché viene lanciata `ClassCastException` quando proviamo ad `add()`arlo a `ArrayList<IntentInfo>`
Dovremmo:
* Essere alla fine della `Parcel` passata come argomento `data` a `onTransact()`
* Effettuare la costruzione di `PooledStringWriter`, dopo di che la Parcel avrà FD pendenti, ma viene anche lanciata `ClassCastException`
* Attendere che gli FD che vogliamo divulgare vengano allocati all'interno di `system_server`
* Fare in modo che gli FD di quella `Parcel` vengano copiati in un'altra `Parcel` che verrà inviata al nostro processo
Per soddisfare tutti questi requisiti, dovrò utilizzare alcuni trucchi sia vecchi che nuovi
## Vecchi trucchi
Iniziamo rivedendo i vecchi trucchi, la maggior parte dei quali sono già stati descritti nel mio exploit [`LazyValue`-using-`Parcel`-after-`recycle()`](https://github.com/michalbednarski/LeakValue)
1. [La classe `RemoteViews` esegue la deserializzazione del `Bundle` contenuto con `Parcel.ReadWriteHelper` impostato](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4). [Quando `ReadWriteHelper` è impostato, i `Bundle` non vengono deserializzati in modo eager ma lazy](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545). Vale anche la pena notare a questo punto che tutti i `Bundle` contenuti all'interno di quel `Bundle` sotto `RemoteViews` verranno deserializzati in modo eager, non solo quello direttamente dentro `RemoteViews`
2. [Se viene lanciata `BadParcelableException` durante la deserializzazione di `Bundle` all'interno di `system_server`, quella `BadParcelableException` verrà silenziosamente catturata e i contenuti del `Bundle` verranno cancellati](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e). Nota però che il nostro trigger di scrittura in `createFromParcel` lancia `ClassCastException` che non verrà catturata qui
3. [La classe `ParceledListSlice` durante la deserializzazione effettua una chiamata Binder in uscita bloccante all'oggetto specificato nei dati serializzati](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364), che possiamo usare per bloccare l'esecuzione della deserializzazione
Tutti questi saranno necessari ora, ma non è tutto
## Nuovi trucchi
[AIDL è uno strumento per generare implementazioni di interfacce RPC](https://developer.android.com/guide/components/aidl), tuttavia oltre a ciò è anche in grado di generare implementazioni di strutture `Parcelable`
Queste strutture sono prefissate con la lunghezza, quindi versioni diverse della stessa struttura sono compatibili all'interno del sistema purché non vengano aggiunti campi nel mezzo (cioè, le versioni sono compatibili se una versione è un prefisso dell'altra)
Diamo un'occhiata a quale codice AIDL ha generato per la struttura [`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);
}
}
Questo codice ci permetterà di eseguire i restanti due trucchi, entrambi necessari per compiere ulteriori azioni dopo aver attivato il percorso "take possession", che richiede di essere alla fine di Parcel e lancia ClassCastException
Per prima cosa, leggiamo la lunghezza da Parcel, la usiamo per saltare i campi che non sono presenti nella versione scritta e alla fine regoleremo la posizione all'interno di Parcel in base a quella lunghezza. Non possiamo spostarci prima della posizione di quel Parcelable AIDL (e spostarsi oltre la fine di Parcel è possibile, ma non fa nulla di pericoloso). Possiamo però spostarci nel mezzo di un oggetto già letto, quindi durante la prima lettura potremmo raggiungere la fine di Parcel, avere PooledStringWriter costruito, poi tornare indietro nel mezzo di qualcosa che si trova all'interno di questo oggetto ReceiverInfo
Questo risolve il problema di essere-alla-fine-di-Parcel, rimane il problema ClassCastException. Qui abbiamo però un blocco finally che prima di chiamare setDataPosition() verifica che non ci sia overflow. Se c'è overflow, verrà lanciata una BadParcelableException. Ora, cosa succede se il blocco finally lancia una Exception mentre ce n'è un'altra in sospeso? L'eccezione lanciata all'interno di finally ha la priorità, l'Exception precedente scompare silenziosamente. Ora invece di ClassCastException abbiamo BadParcelableException, che Bundle ignora volentieri per evitare Exception all'interno di
ParcelableListBinderStiamo usando ParcelableListBinder di MediaSession per inserire un Parcelable arbitrario in system_server e poi recuperare quell'oggetto
Recentemente c'è stata una patch di ParcelableListBinder che impedisce proprio questo
Dal punto di vista di questo exploit, questa patch in realtà non mi ferma, ma devo gestirla in modo diverso a seconda che sia presente o assente
Se la patch è presente, i non-QueueItem vengono rimossi silenziosamente dalla lista ricevuta da system_server (non causano Exception). Poiché abbiamo bisogno di un non-QueueItem per gli effetti collaterali, possiamo mettere un non-QueueItem come primo elemento e poi seguirlo con un QueueItem reale, il che non ci permette di eseguire deserializzazione arbitraria di Parcelable, ma contiene un Bundle che può contenere File Descriptor (che vorremmo far passare al nostro processo)
Se la patch è assente, non abbiamo questo ostacolo, ma non possiamo nemmeno usare lo stesso flusso. Poiché nel caso precedente avremmo una lista con sia RemoteViews che QueueItem, ParceledListSlice rifiuterebbe il trasferimento di una lista di tipo misto. In tal caso devo mettere i nostri FD trapelati all'interno dell'istanza RemoteViews
Più interessante di questa patch è però il motivo per cui è stata messa in atto, il messaggio di commit menziona "permettere alle app di avviarsi in background" e mentre Android Security Bulletin non dice molto, possiamo trovare informazioni utili nella voce CVE, che si riferisce al campo Notification.mAllowlistToken, che durante la lettura può essere preso da un campo statico, che all'interno di system_server è un token che permette l'avvio di Activity in background e successivamente quel token viene scritto in Notification.writeToParcel(). Significa che ora tutti i casi in cui system_server deserializza un Parcelable arbitrario e lo rispedisce all'app diventano vulnerabilità? Comunque, per ora è solo un pensiero, in questo exploit voglio fare di più
Penso che questo exploit presenti la catena di gadget Parcelable più complessa che abbia mai fatto:
RemoteViews (1)
ReflectionAction (2)
Bundle
Parcelable[] (3)
ReceiverInfo (per seek, 4 e 10)
Intent
ComponentName (segmento A, 5)
ParceledListSlice (11)ParcelableParcel o QueueItem (12)Bundle (per catch, segmento B, 6)
ReceiverInfo (per rilancio, 7)
Bundle (8)
Le annotazioni "segmento A" e "B" nell'elenco sopra si riferiscono ai blocchi tra i commenti "START A"/"END A"/"START B"/"END B" nella mia classe FdLeaker.java, i numeri si riferiscono ai punti nell'elenco sottostante
L'albero sopra descrive la gerarchia dal punto di vista del mittente, dal punto di vista del destinatario appare leggermente diverso:
ParcelableListBinder, l'oggetto più esterno è RemoteViewsRemoteViews c'è un Bundle annidato. RemoteViews imposta Parcel.ReadWriteHelper quindi questo Bundle e tutti i Bundle al suo interno verranno letti avidamente. Questo è necessario perché altrimenti non potremmo eseguire readParcelable arbitrario da ReceiverInfo.readFromParcel()Parcelable[] è solo un wrapper di comodo qui per raggruppare tutti i Parcelable che sto inserendo all'interno di RemoteViewsDiamo un'occhiata all'attacco sopra ad alto livello:
system_server, un riferimento ad esso viene memorizzato e il FD viene chiusosystem_server per aprire un File Descriptor diversoC'è una limitazione significativa di questo attacco: Non possiamo afferrare File Descriptor che sono stati aperti prima dell'inizio del nostro attacco
Ci sono comunque ancora alcune cose utili che possiamo fare
InputChannelAd essere sincero, questa è l'unica variante di exploit che sono riuscito a far funzionare senza ipotesi aggiuntive
Gli eventi di input, cioè gli eventi provenienti dal touch screen e dalla tastiera, vengono ricevuti dall'app tramite socket UNIX da system_server. Quando un Activity viene avviato o aggiunge una nuova finestra al sistema, viene creato un nuovo InputChannel, il che a sua volta significa che viene creata una coppia di socket UNIX e una delle estremità viene inviata all'app e l'altra viene usata da system_server per inviare eventi
La struttura dei dati inviati su questi socket è ben definita (poiché devono essere compatibili tra processi a 32 e 64 bit) e sembra che nulla si preoccupi di numeri di sequenza inaspettati. Inoltre, InputChannel sembra essere l'unico socket allocato all'interno di system_server dopo la chiamata startActivity(), quindi può essere facilmente determinato quale FD è il socket lato server di InputChannel

Sebbene questo sia un esempio giocattolo, potremmo anche approvare richieste di permessi o installazione di applicazioni, abilitare Media Projection o Accessibility Service
zygote durante l'avvioQuesta è per lo più teorica, sono riuscito a eseguire questo attacco su un emulatore lento, ma su un dispositivo reale la finestra di race condition era troppo piccola
system_server apre la connessione a /dev/socket/zygote solo una volta all'avvio, dopodiché tutte le richieste vengono inviate tramite quella connessione
Durante l'avvio di system_server, MediaSessionService (che viene usato per inviare e ricevere Parcelable da/a system_server) viene pubblicato in servicemanager prima che la connessione a zygote venga stabilita
Pertanto, teoricamente è possibile per un'app avviare un processo secondario, far crashare system_server e poi da quel processo in background eseguire l'attacco durante l'avvio di system_server
zygote tramite SensorServiceC'è quindi un altro bug che ho trovato, ecco il metodo 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
// 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
}
Abbiamo una chiamata `dup(clone->data[0])`. `clone` è un `native_handle_t` ricevuto da un processo remoto. L'handle nativo contiene un certo numero di FD e un certo numero di interi semplici all'interno di data, le cui quantità l'utente dell'handle nativo dovrebbe controllare [osservando `numFds` e `numInts`](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e) prima di accedere a `data`
Qui abbiamo anche una sezione "controllo specifico per tipo di memoria" che verifica che, per `SENSOR_DIRECT_MEM_TYPE_ASHMEM` questo venga controllato qui, per `SENSOR_DIRECT_MEM_TYPE_GRALLOC` il formato dell'handle nativo è specifico del dispositivo e non può essere validato qui. Il fatto è che per i "sensori runtime" il tipo viene sempre trattato come `SENSOR_DIRECT_MEM_TYPE_ASHMEM`, ma possiamo specificare `SENSOR_DIRECT_MEM_TYPE_GRALLOC` per bypassare la validazione in quel caso
Questo codice è comunque raggiungibile solo se i "sensori runtime" sono presenti. Non sono sicuro in quale caso ciò accada realmente, penso sia quando l'utente utilizza "Nearby app streaming" (?)
Per i test però, sto aggiungendo una piccola classe che permette la registrazione di [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary), puoi usarla tramite```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'
Dopo di ciò, è possibile eseguire codice come uid di sistema su un dispositivo di produzione

system_serverPackageParser$Activity (9)
PooledStringWriterReceiverInfo più esterno ha una lunghezza definita che cade nel mezzo dei suoi dati, tuttavia durante la lettura questo non è ancora noto e Intent viene letto normalmente da essoreadString fatta da statico ComponentName.readFromParcel() (usando ComponentName perché usa readString UTF-16 indipendentemente dalla versione di Android, quel readString restituirà comunque null perché quella stringa si sovrappone agli oggetti Binder, quindi mentre la costruzione di ComponentName ha saltato i dati nascosti da questo passaggio, l'oggetto ComponentName non viene creatoBundle annidato, alla fine della deserializzazione di questo Bundle verrà catturata una BadParcelableExceptionReceiverInfo. Questo ReceiverInfo ha lunghezza specificata come Integer.MAX_VALUE e quindi lancerà BadParcelableException nel blocco finally, scartando silenziosamente ClassCastExceptionBundle. Questo serve solo per poter raggiungere readParcelable arbitrario da ReceiverInfo, cosa che ora possiamo fare poiché siamo all'interno di RemoteViews quindi Bundle viene letto avidamentePackageParser$Activity + PooledStringWriter attiva una chiamata writeInt(0) sul Parcel che viene letto. Poiché eravamo alla fine di Parcel, writeInt() deve espandere la capacità di Parcel, attivando il percorso "take possession". Questa combinazione porta anche a ClassCastException, che viene inghiottita come descritto nei passaggi 7 e 6 sopraReceiverInfo esterno, ReceiverInfo cerca la posizione in base alla lunghezza nella sua intestazione e cade all'interno dei dati che erano precedentemente all'interno di ComponentName, che vengono letti come elementi successivi in Parcelable[]ParceledListSlice che esegue una transazione Binder bloccante verso il mio processo. A questo punto i File Descriptor definiti in questo Parcel sono stati chiusi, ma nulla di interessante ha preso il loro posto ancora. Mentre questa deserializzazione attende il ritorno da questa chiamata, posso far sì che il sistema apra alcuni File Descriptor interessanti che mi verranno poi inviatiParcelableListBinder filtri gli elementi o meno
a. Se ParcelableListBinder non filtra gli elementi, i FD vengono salvati all'interno di ParcelableParcel, che analogamente a Bundle copia i dati di Parcel alla lettera usando Parcel.appendFrom(), ma non ha la logica speciale hasReadWriteHelper() quindi lo fa nonostante sia sotto il Bundle di RemoteViews
b. Se ParcelableListBinder filtra gli elementi, questa è la fine di RemoteViews, l'oggetto RemoteViews viene scartato da ParcelableListBinder, ma per me va bene perché gli effetti collaterali sono già accaduti. L'elemento successivo ricevuto da ParcelableListBinder è QueueItem, che contiene MediaDescription, che a sua volta contiene Bundle, che è dove sono conservati i miei FD trapelati