
Exploit per CVE-2022-20452, elevazione dei privilegi su Android da app installata ad app di sistema (o un'altra app) tramite LazyValue utilizzando Parcel dopo recycle()
Android 13 introduce numerose migliorie per indurire il meccanismo di serializzazione di Parcel
Ecco la presentazione del team Android Security and Privacy sulle migliorie apportate
È fantastico, sicuramente elimina o rende non sfruttabili molte vulnerabilità. Inoltre descrivono come hanno neutralizzato il mio precedente exploit, che consentiva alle app di caricare il proprio codice in altre app (incluse quelle di sistema)
Ma ora sono tornato con un nuovo exploit che ottiene lo stesso risultato, anche se in modo diverso. Si basa sulle seguenti vulnerabilità introdotte durante il summenzionato indurimento di Parcel:
![Schermata di un'applicazione che mostra del testo. Titolo: LeakValue. Testo principale: Creati 6 ValueLeaker-s. Blocco di ActivityTaskManagerService. ActivityTaskManagerService bloccato. Sblocco di ActivityTaskManagerService. ActivityTaskManagerService sbloccato. leakedBinders=[android.os.BinderProxy@f06702e]. Interfaccia leakata: android.app.IApplicationThread. Richiesta di esecuzione del codice. Lo shellcode è stato eseguito 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. Nella parte inferiore dello schermo ci sono due pulsanti: START e MANUAL TESTING](Screenshot_20220723-081920.png)
(Anche logcat dell'esecuzione dell'app, lo sfruttamento è rumoroso nei log)
Parcel e ParcelableLa classe Parcel di Android è la base della comunicazione tra processi
Gli oggetti possono implementare l'interfaccia Parcelable per consentire di scriverli su una Parcel, ad esempio (copiato da 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());
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());
} }
Nota che `Parcel` memorizza internamente la posizione in cui viene eseguita la scrittura o la lettura, `readString()` analizza i dati in una String e fa anche avanzare la posizione. Tale posizione può essere letta/impostata manualmente tramite [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)). Le implementazioni dell'interfaccia `Parcelable` devono garantire che i loro `writeToParcel` e `createFromParcel` scrivano/leggano la stessa quantità di dati, altrimenti tutte le letture successive otterranno dati da offset errati.
[`Bundle`](https://developer.android.com/reference/android/os/Bundle) (mappa chiave-valore che può essere inviata tra processi) può contenere [una varietà di oggetti che possono essere scritti su Parcel tramite `writeValue()`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937). Quando il contenuto di `Bundle` viene letto da `Parcel`, qualsiasi classe `Parcelable` disponibile nel sistema può essere letta.
`Bundle` rimanda l'effettiva analisi del contenuto facendo scrivere la lunghezza dell'intero dato impacchettato in `Parcel` e poi [copiando la parte rilevante del Parcel originale in un Parcel secondario memorizzato 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) (questo consente ad esempio a [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)) di fornire `Parcelable` non disponibili in `system_server`; l'intero `Bundle` viene quindi passato a `system_server` e restituito senza modifiche, senza analizzare il contenuto).
Tuttavia, una volta che si accedeva a un valore qualsiasi nel `Bundle`, tutti i valori all'interno del `Bundle` [venivano estratti dal `Parcel`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) e [ogni coppia chiave-valore presente veniva analizzata](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). Se tale mappa conteneva un `Parcelable` con metodi `writeToParcel` e `createFromParcel` sbilanciati e successivamente tale `Bundle` veniva inoltrato a un altro processo, quell'altro processo poteva vedere contenuti diversi del `Bundle`. Questo ha reso tutte queste discrepanze nelle classi disponibili nel sistema delle vulnerabilità, poiché ci sono [punti nel sistema in cui `Bundle` viene ispezionato per accertarne la sicurezza](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) e poi inoltrato a un altro processo.
In questo articolo chiamo tale `Bundle`, che presenta un contenuto e poi un altro dopo essere stato inoltrato, `Bundle` auto-modificante.
Un'altra cosa importante qui è che oltre ai semplici byte (Stringhe, numeri, oggetti composti da quanto sopra), `Parcel` può contenere anche descrittori di file e `Binder`. I `Binder` sono oggetti su cui si può effettuare una chiamata RPC; cioè un processo crea un oggetto `Binder` e fa override del [metodo `onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Poi il `Binder` viene passato a un altro processo; nell'esempio di codice qui sopra puoi vedere le chiamate `read`/`writeStrongBinder()` usate per leggerlo e scriverlo su `Parcel`. Nell'altro processo, quando viene usato `readStrongBinder()`, viene creato un oggetto `BinderProxy` (nascosto dietro l'[interfaccia `IBinder`](https://developer.android.com/reference/android/os/IBinder)). Quindi quell'altro processo può chiamare [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) su quell'oggetto e nell'oggetto originale verrà eseguito `onTransact()`. Di solito, comunque, non si scrive manualmente `transact()`/`onTransact()`, ma [si usa invece AIDL](https://developer.android.com/guide/components/aidl).
# Ecco `LazyValue`, la fine dei `Bundle` auto-modificanti
Poiché in passato ci sono stati molti casi di classi con `writeToParcel`/`createFromParcel` sbilanciati, Android 13 risolve il problema di qualsiasi classe del genere presente ovunque nel sistema che consenta la costruzione di `Bundle` auto-modificanti [introducendo `LazyValue`](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/).
Ora, quando si usa `writeValue`, se il valore scritto non è primitivo, [anche la lunghezza del valore viene scritta in `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804).
Quando una normale app usa direttamente `Parcel.readValue()`, [tutto avviene come prima, tranne per il fatto che viene stampato un avviso se la `length` letta da `Parcel` non corrisponde alla dimensione dei dati effettivamente letti](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804) (Nota comunque che [`Slog.wtfStack` non lancia mai eccezioni](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577)).
`Bundle`, tuttavia, ora usa invece [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804).
Diamo un'occhiata più da vicino a come funziona: nella classe `LazyValue` abbiamo [un bel commento che spiega la struttura dei dati di `LazyValue` all'interno di `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 è il riferimento al Parcel originale su cui è stato chiamato readLazyValue()
mPosition e mLength descrivono la posizione dell'intero dato LazyValue nel Parcel originale, inclusi type e length
"length" (senza "m" all'inizio) si riferisce al valore di lunghezza così come scritto nel Parcel ed esclude l'header (type e length)
Quindi ecco cosa succede quando qualcuno (sia il sistema che un'app) prende un valore dal Bundle che è stato letto dal Parcel:
get*() della classe Bundle, per esempio il nuovo getParcelable() con argomento di tipo (Il flusso sarà lo stesso sia per i nuovi metodi che per quelli vecchi, solo che i nuovi metodi assicurano che l'argomento clazz non sia null mentre quelli legacy lo impostano a null)unparcel() viene chiamato, il quale controllerà se questo Bundle ha mParcelledData (cioè è stato letto da un Parcel ma nessun valore è stato ancora acceduto e i nomi delle chiavi non sono stati ancora estratti; se non è così, salta al punto 5.)unparcel() delega a , , è impostato su , una copia del che il ha creato, e il parametro è impostato a per indicare che il passato è di proprietà del e che è consentito chiamare su di essoSe il Bundle viene inoltrato mentre contiene ancora LazyValue (cioè quel particolare valore non è stato acceduto, ma qualche altro valore di quel Bundle lo è stato (cioè unparcel() è stato chiamato, ma LazyValue.apply() per quell'elemento no)):
LazyValue viene rilevato da Parcel.writeValue() e la scrittura viene delegata a LazyValue.writeToParcel()LazyValue.writeToParcel() usa out.appendFrom(source, mPosition, mLength) per copiare l'intero dato LazyValue dal Parcel originale (di nuovo, mPosition e mLength includono l'header di LazyValue, quindi questo copia anche type e length dal originale)Parcel.ReadWriteHelper e Parcel.readSquashed(I dettagli di questi non sono importanti per questo exploit, l'unica cosa rilevante qui è che questi meccanismi esistono)
Un'altra caratteristica interessante di Parcel è la capacità opzionale di deduplicare le String e gli oggetti scritti
La deduplicazione delle String viene fatta sovrascrivendo la classe Parcel.ReadWriteHelper: Parcel.readString() in realtà delega a ReadWriteHelper e l'helper predefinito legge direttamente la String dal Parcel
Un'implementazione alternativa di Parcel.ReadWriteHelper può sostituire le chiamate a readString con la lettura preventiva di un pool di String e l'uso di readInt per ottenere gli indici delle String nel pool; questo però non viene mai fatto con Parcel controllati dall'app
Parcel offre il metodo hasReadWriteHelper(), che consente ai chiamanti di rilevare la presenza di tale meccanismo di deduplicazione attivo e disabilitare le funzionalità incompatibili con esso
L'altro meccanismo di deduplicazione disponibile in Parcel è lo squashing:
Parcel.allowSquashing()Parcel.maybeWriteSquashed(this). Se quel metodo restituisce true, significa che l'oggetto è già stato scritto in questo Parcel e ora in Parcel è stato scritto solo l'offset al dato dell'oggetto precedente. Altrimenti (o lo squashing non è abilitato o è la prima volta che questo oggetto viene scritto) maybeWriteSquashed scrive zero come offset per indicare che l'oggetto non è squashed e restituisce false per indicare al chiamante che deve scrivere i dati effettivi dell'oggettoParcel.readSquashed viene chiamato e la funzione di lettura effettiva gli viene passata come lambda. readSquashed controlla se l'offset scritto da indica che un'altra occorrenza dell'oggetto è stata letta in precedenza: se sì, viene restituito l'oggetto letto in precedenza, altrimenti viene chiamata la lambda fornita per leggerlo oraParcel.recycle()Lato Java, gli oggetti Parcel possono essere riciclati in un pool: una volta terminato con un Parcel, chiami recycle() su di esso e la prossima volta che qualcuno chiama Parcel.obtain() otterrà il Parcel precedentemente riciclato. Questo consente di ridurre la quantità di allocazioni di oggetti e la successiva Garbage Collection
D'altro canto, tale gestione manuale della memoria introduce la possibilità di bug simili a Use-After-Free in Java (sebbene con type safety, a differenza del solito Use-After-Free in C)
Come notato sopra, Bundle crea una copia del Parcel e non chiamerà Parcel.recycle() se è presente LazyValue; questo però non è il caso se Parcel.hasReadWriteHelper() è true, in tal caso:
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle); viene chiamato, questo significa che Bundle non riciclerà il Parcel perché appartiene ancora al chiamante, tuttavia questo crea LazyValue che si riferiscono al Parcel originale e potrebbero sopravvivere alla durata del Parcel originaleunparcel(/* itemwise */ true), che userà getValueAt() su tutti gli elementi per sostituire tutti i LazyValue presenti nel Bundle con i valori effettiviOra, possiamo far sopravvivere questi LazyValue al punto 2. e trasformare questo comportamento in Use-After-Recycle?
Se la deserializzazione fallisce (per esempio la classe con il nome specificato dentro il Parcel non può essere trovata), viene lanciata una BadParcelableException e poi catturata da getValueAt(). Se il campo statico BaseBundle.sShouldDefuse è true, non viene sollevata un'eccezione e l'esecuzione procede lasciando il Bundle contenente LazyValue che si riferisce al Parcel originale. sShouldDefuse indica che i valori non disponibili dal Bundle non devono causare eccezioni in un particolare processo ed è impostato a true in
Se il Parcel originale viene riciclato e dopo di ciò il Bundle letto da esso viene scritto in un altro Parcel, il contenuto del Parcel originale verrà copiato nel Parcel di destinazione, ma a quel punto il Parcel originale potrebbe essere stato riutilizzato per qualcos'altro e potrebbero essere copiati dati da un'operazione IPC non correlata
Okay, ma come facciamo a far sì che Parcel.hasReadWriteHelper() sia true mentre il Bundle fornito da noi viene deserializzato?
A quanto pare la classe RemoteViews (normalmente usata per esempio per passare widget alla schermata home) imposta esplicitamente ReadWriteHelper durante la lettura dei Bundle annidati in essa. Questo ReadWriteHelper non esegue la deduplicazione delle String ed è presente solo per far sì che Bundle salti la copia dei dati in un Parcel secondario. La ragione di ciò è che RemoteViews abilita lo squashing per deduplicare gli oggetti ApplicationInfo annidati in essa, ma questo potrebbe anche causare lo squashing degli oggetti ApplicationInfo presenti dentro il Bundle, quindi la lettura di quel non può essere rimandata perché altrimenti quegli oggetti squashed non riuscirebbero a fare unsquash
Parcelable in system_server e recuperarliQuindi ora vogliamo che system_server legga la nostra RemoteViews contenente un Bundle contenente un LazyValue che non riesce a deserializzare e successivamente (in un'altra transazione IPC Binder) ci rimandi indietro quell'oggetto
Probabilmente potrebbe essere fatto con mezzi legittimi, come registrarci come host di app widget (ma richiederebbe l'interazione dell'utente per concederci il permesso) o pubblicando una Notification con contentView impostato (ma ciò causerebbe interazioni con altri processi e/o sarebbe visibile all'utente e ho preferito evitare entrambe queste cose)
Ho deciso invece di creare un MediaSession e chiamare setQueue(List<MediaSession.QueueItem> queue) su di esso per inviare l'oggetto a system_server e successivamente recuperarlo tramite il metodo List<MediaSession.QueueItem> getQueue() di MediaController (che può essere ottenuto tramite MediaSession.getController()). Mentre questi metodi non sembrano poter accettare RemoteViews, in realtà lo fanno grazie alla Java Type Erasure e al fatto che sotto il cofano sono implementati usando operazioni di serializzazione generiche su List
Tuttavia, non sto usando questi metodi SDK, scrivo manualmente i dati per le transazioni Binder sottostanti (perché devo scrivere e successivamente leggere dati serializzati malformati), quindi diamo un'occhiata a come funzionano questi metodi
Entrambi questi metodi dovevano tenere conto del fatto che la dimensione totale della coda potrebbe superare la dimensione massima di una transazione Binder, quindi il trasferimento può essere suddiviso in più transazioni
L'invio della "coda" a system_server normalmente avviene così:
MediaSession.setQueue() prima chiama ISession.getBinderForSetQueue()system_server, quel metodo costruisce e restituisce un oggetto ParcelableListBinderMediaSession.setQueue() chiama ParcelableListBinder.send() che invierà il contenuto della lista nel Binder fornito, possibilmente su più transazioni:
1 e l'elemento effettivo viene scritto tramite (che scrive il nome della classe inviata e poi chiama per inviare i dati)Il recupero della "coda" d'altra parte avviene in modo un po' diverso:
MediaController.getQueue() si limita a chiamare ISessionController.getQueue() e a decomprimere il ParceledListSlice ricevutosystem_server, getQueue() semplicemente avvolge mQueue in un ParceledListSlice e lo restituisceParceledListSlice.writeToParcel() e createFromParcel(), in particolare, writeToParcel() al raggiungimento del limite di dimensione sicuro scriverà un oggetto che consente di recuperare i chunk successiviRiguardo al perché questi sono diversi: c'è uno sforzo in corso per garantire che system_server non effettui chiamate Binder sincrone in uscita verso altre app, perché se queste chiamate si bloccassero potrebbero bloccare l'intero system_server. Questo significa che system_server non dovrebbe ricevere ParceledListSlice. Mentre c'è codice che avvisa riguardo alle transazioni sincrone in uscita da system_server, non è ancora stato reso vincolante perché ci sono ancora casi in cui system_server effettua tali chiamate, per esempio ricevendo effettivamente un ParceledListSlice
Quindi ora abbiamo le primitive necessarie per far eseguire a system_server parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size)
Potremmo o tentare casualmente di estrarre dati Parcel dal sistema o organizzare le cose per prendere qualcosa di specifico
Ci sono le seguenti considerazioni:* Quando viene chiamato Parcel.recycle(), il contenuto di quella Parcel viene cancellato. Ciò significa che la Parcel da cui vorremmo far copiare i dati non deve essere sottoposta a recycle(), il che in pratica significa che non possiamo prelevare dati da una transazione Binder già terminata
Parcel di qualche Bundle presente nel sistema (questo include gli extra di Intent e il savedInstanceState di Activity). Queste di solito non vengono affatto sottoposte a recycle() (vengono pulite dal Garbage Collector e non tornano al pool; quando il pool si esaurisce, Parcel.obtain() crea nuovi oggetti Parcel. Ovviamente, le Parcel a cui teniamo un riferimento non saranno GCed, anche se il sistema non ne ha altro uso)Parcel usate per le transazioni Binder in ingresso usano un pool separato rispetto alle altre Parcel nel sistema. Quando viene effettuata una transazione Binder in uscita, copia i dati in una secondaria, oppure un'app usa per i propri scopi, e viene chiamato . D'altra parte, quando c'è una transazione in ingresso, , che . In entrambi i casi, successivamente viene usato , che si occupa di . Questo significa che l'exploit deve far leggere a da una appartenente allo stesso pool da cui vogliamo far trapelare i dati. Prima di decidermi per una variante particolare ho scritto entrambe, quindi puoi trovare sia il metodo che nella mia Alla fine ho deciso di provare a catturare il Binder IApplicationThread, che viene inviato dall'app a system_server quando il processo dell'app viene avviato e system_server lo usa per dire all'applicazione quali componenti deve caricare
Quando il processo dell'applicazione viene avviato, una delle prime cose che fa è inviare IApplicationThread a system_server tramite la chiamata a attachApplication() e questa è la transazione da cui catturerò quel Binder. Ci sono altri punti in cui IApplicationThread viene messo in una Parcel, ad esempio quando viene passato per l'identificazione del chiamante da parte del sistema all'avvio di un'activity (ma non avevo molto controllo su quando l'applicazione target lo fa) oppure quando viene inviato dal sistema all'applicazione come parte della gestione del ciclo di vita delle Activity (ma questo avviene in transazione oneway in uscita da system_server e le probabilità di vincere la gara contro Parcel.recycle() sarebbero scarse)
Detto questo, catturare il Binder che viene ricevuto da system_server durante la transazione attachApplication() è comunque non banale e ci sono stati alcuni problemi da superare
ParcelIl primo problema nel catturare il Binder IApplicationThread dalla Parcel da cui vengono ricevuti i dati per attachApplication() è che questo Binder si trova a una dataPosition() piuttosto iniziale/bassa, molto più bassa di quanto potrebbe essere la nostra LazyValue nel Bundle dentro RemoteViews
I dati per la transazione attachApplication() consistono solo di intestazione RPC seguita dal Binder IApplicationThread. L'intestazione RPC (scritta tramite Parcel.writeInterfaceToken()) consiste in pochi int e nel nome dell'interfaccia, in questo caso "android.app.IActivityManager"
Nel frattempo, per leggere il Bundle incorporato in RemoteViews dovremmo superare almeno (alcuni elementi minori vengono saltati):
readParcelableParcelable: "android.view.RemoteViews"ApplicationInfo piuttosto grande presente in RemoteViews (inoltre deve non essere null e avere un packageName non-null altrimenti RemoteViews.writeToParcel() fallirà quando proveremo a far sì che questo oggetto venga rispedito)Ora, nel Bundle, dobbiamo solo inserire una chiave String e inizia la lettura di LazyValue, la posizione nella Parcel viene ricordata, ma a questo punto è molto oltre la posizione in cui si troverebbe il Binder IApplicationThread
Possiamo forse, una volta raggiunto questo punto, riavvolgere la posizione nella Parcel? In altre parole, potremmo far sì che Parcel.setDataPosition() venga chiamato con un valore che punta a una posizione precedente rispetto a quella corrente?
A quanto pare, possiamo, grazie a un altro bug in LazyValue. Questo è il codice usato per leggerlo:```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);
}
}
([Originale in AOSP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804), il costruttore di `LazyValue` assegna e basta i parametri ai campi)
Il punto è che `MathUtils.addOrThrow()` controlla l'overflow, [ma va benissimo con valori negativi](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)
Se provassimo a fare `Parcel.writeValue()` su `LazyValue` con `mLength` negativo (riempito dal parametro `valueLength`), allora [verrebbe lanciata un'eccezione su `appendFrom()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804), tuttavia, siccome siamo durante la lettura di `Bundle` con `Parcel.hasReadWriteHelper()` che è `true`, tutti i `LazyValue` vengono de-parcellati dopo la lettura e abbiamo dovuto inserire intenzionalmente un `Parcelable` difettoso al suo interno per far sì che rimanesse come `LazyValue`. Se inseriamo dati parcelati validi nella posizione in cui si trova `LazyValue`, verrà de-parcellato e, come notato in precedenza, la lunghezza non corrispondente attiverà solo un messaggio in `logcat`. Questo particolare exploit imposta il tipo a `VAL_MAP` e il numero di coppie chiave-valore a zero. In `logcat`, alla lettura di quel valore, possiamo vedere il seguente messaggio: "`E Parcel : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP consumed 4 bytes, but -540 expected.`"
(Inoltre, un `LazyValue` con lunghezza negativa specificata può essere usato (senza utilizzare gli altri bug descritti in questo writeup) per creare un `Bundle` auto-modificante, la cosa che `LazyValue` è stato creato per eliminare. Ma questa è un'altra storia (ed è stata segnalata separatamente a Google); in questo exploit miro a qualcosa di più)
Quindi di quanto vogliamo riavvolgere?
Dopo la chiamata a `setDataPosition()`, la lettura procederà alla successiva coppia chiave-valore nel `Bundle`, quindi dobbiamo scegliere una posizione in cui avremo:
1. La chiave del `Bundle`, letta usando `Parcel.readString()`, può essere praticamente qualsiasi cosa, incluso puntare a una lunghezza non valida (negativa o superiore alla dimensione totale del `Parcel`); in quel caso `readString()` restituirebbe `null`, che è una chiave valida in `Bundle`
2. Il tipo del valore, deve essere uno dei [tipi per cui `isLengthPrefixed()` restituisce `true`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804)
3. La lunghezza del valore, anche questa deve essere un valore controllato da noi; `Parcel.appendFrom()` fallirà se la lunghezza non è allineata o supera la dimensione totale del `Parcel` sorgente
Quindi quale posizione nel `Parcel` potrebbe essere quella giusta, considerando che gli stessi dati sono già stati letti e sono necessari per arrivare a questo punto:
* Non prima del nome del `Parcelable` (`"android.view.RemoteViews"`), perché non c'è abbastanza spazio
* Non all'interno del nome del `Parcelable`, perché non siamo in grado di impostare tipo e lunghezza
* Non direttamente dopo il nome del `Parcelable`, perché la prima cosa in `RemoteViews` è `mode`, che [dobbiamo impostare a `MODE_NORMAL` per raggiungere il nostro codice](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* Non dopo quello, perché è oltre il punto in cui si trova il `Binder` di `IApplicationThread`
Hmm, non c'è un buon posto quando `RemoteViews` è l'oggetto più esterno nei dati parcellati
Dobbiamo trovare qualche altro `Parcelable` che:
1. Ha, all'inizio o quasi, un punto in cui possiamo inserire dati arbitrari (es. `int` o `String` che sono solo dati e non influenzano il processo di serializzazione)
2. Può contenere `RemoteViews` (direttamente o tramite un `readParcelable` arbitrario)
3. Non ha un nome di classe completamente qualificato troppo lungo, perché siamo ancora limitati in dimensione dalla posizione in cui `IApplicationThread` rimane nel `Parcel` di destinazione
Quindi ho preso l'elenco delle classi `Parcelable` nel sistema, l'ho ordinato per lunghezza crescente del nome di classe completamente qualificato e ho iniziato a controllare gli elementi di quell'elenco per vedere se soddisfano la condizione 2
In questo modo sono arrivato a [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216), che è ciò che questo exploit usa. Ora il processo di lettura del nostro oggetto preparato dal `Parcel` è il seguente:
* [Flag di presenza dell'elemento](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) per avviare `readParcelable`
* [Nome del `Parcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804): `"android.os.Message"`
* Alcuni [`int` che possiamo impostare a qualsiasi valore desideriamo](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216) vengono letti nei campi
* Raggiungiamo la chiamata `readParcelable()`, che percorre tutto il percorso descritto sopra attraverso `RemoteViews` e inizia a leggere `Bundle` con `Parcel.hasReadWriteHelper` che è `true`
* Quel `Bundle` dichiara di avere due coppie chiave-valore. Nel primo valore abbiamo un `LazyValue` con lunghezza negativa, che attiva `Parcel.setDataPosition()` alla posizione in cui si trova la `String` `"android.os.Message"`
* La lettura procede alla seconda coppia chiave-valore; la chiave è `"android.os.Message"` e tipo, lunghezza e dati di `LazyValue` sono presi dagli `int` descritti nel terzo punto. Ho ottenuto un `LazyValue` con `mPosition` e `mLength` come volevo. Evviva!
* Dopo che i `LazyValue` vengono letti, vengono de-parcellati. Quello con dimensione negativa viene de-parcellato correttamente e sostituito con una `Map` vuota, mentre l'altro fallisce la deserializzazione, ma quell'eccezione viene catturata e `LazyValue` rimane semplicemente nel `Bundle`
* `readParcelable()` termina, ma questa non è la fine dei dati del `Message`. `Message.readFromParcel()` ora continua a leggere i dati dopo il riavvolgimento e vede dati che erano stati inizialmente scritti come parte di `RemoteViews`. Se qualsiasi cosa lancia un'eccezione a questo punto, l'intero piano viene rovinato
* Prima possibile eccezione: [c'è la chiamata `readBundle()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216). [`Bundle` ha un valore magico e se non è corretto viene lanciata un'eccezione](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). Quel valore magico però non è presente se la lunghezza [è zero](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), e questo è capitato quando la lunghezza dei dati di `LazyValue` è stata impostata al valore che mi serviva per catturare `IApplicationThread`. Quindi qui ho semplicemente avuto fortuna
* Il successivo possibile problema potrebbe essere [la chiamata `Messenger.readMessengerOrNullFromParcel()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216). Questo è in realtà un oggetto `Binder` avvolto. La lettura di quel `Binder` fallisce, perché `Binder` è un oggetto speciale nel `Parcel` e deve essere annotato fuori banda per essere letto. Questo problema viene [rilevato e registrato da `Parcel` sul lato nativo, ma non viene propagato come errore e viene semplicemente restituito `null`](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)
# Bloccare `attachApplication()`
Ok, quindi nel passaggio precedente abbiamo creato con successo un oggetto che ci permetterà di catturare l'oggetto `IApplicationThread` mentre il metodo `attachApplication()` è in esecuzione
Il punto è che quel metodo termina rapidamente e le nostre possibilità in una gara equa contro il suo completamento sarebbero piuttosto scarse
Tuttavia, quel metodo acquisisce alcuni mutex (attraverso l'uso dei blocchi `synchronized () {}` di Java); se riusciamo ad acquisire uno di questi mutex e a bloccarci lì, anche questo metodo si bloccherà.
Ora torniamo ad alcune cose che sono già state dette in questo writeup e che saranno utili a questo scopo:
* `Bundle` esegue la deserializzazione dei valori al suo interno quando questi valori vengono acceduti
* C'è la classe `ParceledListSlice` che durante la deserializzazione effettuerà una chiamata `Binder` bloccante in uscita verso l'oggetto specificato nei dati serializzati
Mettendo insieme tutte queste cose: se troviamo in `system_server` un punto in cui i contenuti del `Bundle` fornito dall'app vengono acceduti sotto un mutex che è usato anche da `attachApplication()`, saremo in grado di bloccare `attachApplication()` finché la transazione `Binder` fatta verso il nostro processo non termina.
[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) è una classe che descrive vari parametri relativi all'avvio di un'`Activity` (ad esempio l'animazione). A differenza di altre classi che descrivono parametri passati a `system_server`, questa non implementa `Parcelable`, ma fornisce invece un metodo per convertirla in `Bundle`
Sul lato `system_server`, quel [`Bundle` viene riconvertito in `ActivityOptions`, attivando la deserializzazione](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). Ho trovato un punto in cui [quell'operazione viene eseguita mentre il mutex `ActivityTaskManagerService.mGlobalLock` è tenuto in `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)
Quindi chiamo [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle)), passando un `Bundle` che contiene `ParceledListSlice` invece del valore con il tipo previsto. Quel [`ParceledListSlice` effettua una chiamata `Binder` al mio processo](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577) e finché non ritorno da quella chiamata, il mutex `ActivityTaskManagerService.mGlobalLock` rimarrà bloccato.
# Creare più `LazyValue` che puntano a `Parcel` diversi
`Parcel.recycle()` e `Parcel.obtain()` funzionano in modalità [Last-In-First-Out](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))
Questo significa che, se creo un `LazyValue` truccato quando nessun'altra transazione `Binder` verso `system_server` è in esecuzione, otterrò un `LazyValue` che punterà al `Parcel` che viene sempre usato quando c'è una sola transazione in arrivo verso `system_server` (finché non succede che due transazioni concorrenti in arrivo verso `system_server` inizino e finiscano in ordine non a stack).
Poiché non ho controllo su quali altre transazioni sono in arrivo verso `system_server`, per migliorare l'affidabilità dell'exploit ho creato più `LazyValue` che puntano a vari `Parcel`
Poiché ho la capacità di innescare una transazione `Binder` sincrona verso il mio processo da `system_server`, ho usato quella capacità per creare `LazyValue` a vari livelli di ricorsione tra il mio processo e `system_server` (anche se questa volta l'ho fatto senza tenere un mutex globale).
Quindi:
* Creo un `LazyValue`
* Innesco una chiamata a `system_server`; `system_server` mi richiama
* Creo un `LazyValue`
* Innesco una chiamata a `system_server`; `system_server` mi richiama
* Creo un `LazyValue`
* Innesco una chiamata a `system_server`; `system_server` mi richiama
* ...
Poi, una volta che ho abbastanza `LazyValue`, smetto di farlo, ritorno da tutte queste chiamate e tutti i `Parcel` che erano stati riservati da queste chiamate vengono riciclati con `recycle()`
Ognuno dei `LazyValue` che ho creato è avvolto in un separato [`ParceledListSlice` creato da `getQueue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804) e posso chiamare il `Binder` del `ParceledListSlice` per far sì che `system_server` lo serializzi e lo invii al mio processo
(Un modo alternativo per farlo sarebbe creare più `MediaSession`)
# Avvio del processo dell'app target
Ora abbiamo tutto il necessario per catturare `IApplicationThread` da `attachApplication()` quando avviene, ma dobbiamo ancora far sì che `attachApplication()` avvenga
In generale [esistono alcuni tipi di componenti dell'app con cui un'altra app può interagire](https://developer.android.com/guide/components/fundamentals#Components), ognuno dei quali richiede che il processo dell'app venga avviato
Volevo avviare l'app Impostazioni di sistema (che [viene eseguita con l'uid di sistema](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) e quindi ha accesso a [tutto ciò che sta dietro ai permessi Android](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b))
All'inizio ho tentato di avviarla tramite `startActivity()`, ma quando l'ho provato, il processo non veniva avviato finché non rilasciavo il lock di `ActivityTaskManagerService`. I dettagli sul perché ciò accadeva sono nella sezione "Nota aggiuntiva: chiamate `Binder` e rientranza dei mutex", ma come soluzione ho deciso di richiedere al sistema un [`ContentProvider` di quell'app](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) invece di un'`Activity`. Questo aveva il vantaggio aggiuntivo di evitare interferenze con la mia UI
Non ho usato [l'API ufficiale `ContentResolver` esposta da SDK](https://developer.android.com/reference/android/content/ContentResolver), ma invece ho usato [quella interna di sistema, perché avevo bisogno di un'API asincrona](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907), poiché il binding al `ContentProvider` non sarebbe terminato fino a `attachApplication()`, che sto bloccando, sebbene avviare un altro thread potesse essere un'alternativa
(Non importa cosa offra questo particolare `ContentProvider`, l'unica cosa rilevante è che posso stabilire una connessione con esso)
Quindi è così che avvio il processo dell'app Impostazioni. Mi assicuro che non sia già in esecuzione in primo luogo usando [il metodo ufficialmente disponibile `ActivityManager.killBackgroundProcesses()`](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))
# Mettere tutto insieme
Le primitive sono ora descritte, quindi ecco come funziona tutto insieme (questa è più o meno una trascrizione del metodo `MainActivity.doAllStuff()` di questo exploit):
1. Abilitare l'accesso alle API nascoste (le API nascoste non sono un confine di sicurezza e ci sono [già bypass pubblicamente disponibili](https://www.xda-developers.com/bypass-hidden-apis/), anche se qui ho usato un metodo basato su [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String)), che non ho visto altrove)
2. (Solo se stiamo rieseguendo l'exploit dopo il primo tentativo) Rilasciare la connessione al `ContentProvider` che abbiamo stabilito nel passaggio 6 durante l'esecuzione precedente. Dobbiamo farlo, altrimenti `ActivityManager.killBackgroundProcesses()` non considererà il processo target come "background" e non lo ucciderà.
3. Uccidere il processo dell'app vittima usando `ActivityManager.killBackgroundProcesses()`, poiché `attachApplication()` viene chiamato solo all'avvio del processo.
4. Richiedere a `system_server` di creare una serie di oggetti contenenti `LazyValue` che puntano a un `Parcel` che viene successivamente riciclato. Ottengo il riferimento al `Binder` del `ParceledListSlice` per ogni oggetto contenente `LazyValue` e posso fare una transazione `Binder` verso di esso per far sì che il sistema lo riscriva. La creazione di ogni oggetto `LazyValue` avviene a diverse profondità di chiamate [mutualmente ricorsive](https://en.wikipedia.org/wiki/Mutual_recursion) tra `system_server` e la mia app, per rendere probabile che ciascuno di questi `LazyValue` abbia un riferimento pendente a un oggetto `Parcel` diverso.
5. Blocco `ActivityTaskManagerService.mGlobalLock` effettuando una chiamata a `ActivityTaskManagerService.moveTaskToFront()` passando come argomento un `Bundle` che, alla deserializzazione, esegue una transazione `Binder` sincrona verso il mio processo. I passaggi successivi vengono eseguiti da quel callback e quindi con quel lock tenuto.
6. Richiedo ad `ActivityManagerService` una connessione con il `ContentProvider` dell'app vittima (nota: niente "`Task`" nel nome; `ActivityTaskManagerService` è la classe focalizzata principalmente sulla gestione dei componenti `Activity` delle app, mentre `ActivityManagerService` gestisce altri [componenti app](https://developer.android.com/guide/components/fundamentals#Components) (oltre all'avvio complessivo del processo); questa [separazione è avvenuta in Android 10, in precedenza sia la gestione delle `Activity` che degli altri componenti app era in `ActivityManagerService`](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc)).
7. Faccio un `sleep()` per un po' per dare al processo appena lanciato il tempo di iniziare a chiamare `attachApplication()`.
8. Mentre il lock è ancora tenuto, richiedo a tutti gli oggetti `ParceledListSlice` creati in precedenza di inviare i loro contenuti rimanenti (quelli che non erano entrati nella transazione iniziale), cioè gli oggetti contenenti `LazyValue` che puntano a un `Parcel` riciclato. Poi, da un offset hardcoded che corrisponde alla posizione di `IApplicationThread` passato a `attachApplication()`, leggo l'oggetto `Binder`. In questo momento sto solo salvando i `Binder` ricevuti in un `ArrayList` per evitare di fare troppo con il lock tenuto.
9. Questa è la fine del codice che eseguo dal callback avviato nel passaggio 5. `ActivityTaskManagerService.mGlobalLock` viene sbloccato.
10. Ho ottenuto il `Binder` di `IApplicationThread`. Ora posso semplicemente usarlo per caricare il mio codice nell'app vittima, come descritto nella sezione successiva.
# Come utilizzo `IApplicationThread`
Come notato in precedenza, il `Binder` [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) viene inviato dall'app a `system_server` all'avvio del processo dell'app, e poi `system_server` lo usa per dire all'applicazione quali componenti deve caricare.
Si presume che questo oggetto venga passato solo a `system_server` e quindi non ci sono controlli basati su `Binder.getCallingUid()` lì, quindi possiamo semplicemente chiamare direttamente i metodi offerti da quell'interfaccia.
Ho [descritto nel mio writeup precedente come ottengo l'esecuzione di codice manipolando gli argomenti di `scheduleReceiver()`](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). Ora la situazione è la stessa, tranne che questa volta sono io a chiamare `scheduleReceiver()`, mentre prima stavo alterando l'interpretazione degli argomenti di una chiamata fatta da `system_server`.
# Note aggiuntive
In questa sezione descrivo alcune cose che alla fine non si sono rivelate utili in questo caso, sebbene possano essere funzionalità di cui vale la pena essere consapevoli o potenziali bug
## Nota aggiuntiva: `Bundle.clear()`Per semplicità, ho descritto qui la `Bundle` aggiornata senza un [commit introdotto successivamente, che consente di riciclare la `Parcel` usata in `Bundle` per supportare `LazyValue`s chiamando `Bundle.clear()`](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)
Come indicato nel messaggio del commit, viene tracciato se si copia `Bundle` e in tal caso `clear()` non ricicla la `Parcel`.
Tuttavia quel commit cambia anche la semantica del parametro/variabile `recycleParcel` di [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)
In precedenza, `recycleParcel` impostato a `false` indicava che `Parcel` non doveva essere riciclata, o perché [il chiamante impostava `recycleParcel` a `false` per indicare che `Parcel` non è di proprietà di `Bundle`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1) o perché veniva [impostato a `false` in base al risultato di `parcelledData.readArrayMap()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)
Ora le ragioni per cui `recycleParcel` potrebbe essere `false` sono le stesse, tuttavia l'interpretazione è cambiata: ora ciò non significa "non riciclare questa `Parcel`", ma "rimanda il riciclo della `Parcel` fino alla chiamata di `Bundle.clear()`".
Ciò significa che se `clear()` venisse chiamato su una `Bundle` creata con `Parcel.hasReadWriteHelper()` uguale a `true`, la `Parcel` verrebbe riciclata, mentre il codice che ha richiesto la creazione di quella `Bundle` riciclerebbe anch'esso quella `Parcel`, portando a una doppia `recycle()`, che produce un comportamento simile a un double-free: le chiamate successive a `Parcel.obtain()` restituirebbero lo stesso oggetto due volte.
Tuttavia, non ho trovato un modo per far sì che `clear()` venga chiamato su una `Bundle` del genere.
Da quando ho scritto originariamente questo, [il comportamento di `recycle()` è cambiato e ora un riciclo aggiuntivo è un no-op con possibile crash tramite `Log.wtf()`](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([a seconda della configurazione, ma non causa mai il crash di `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)). Direi che il nuovo comportamento potrebbe essere ancora pericoloso, soprattutto quando abbiamo la capacità di bloccare programmaticamente la deserializzazione in corso in un altro processo, ma non esiste davvero un buon modo per gestire il doppio riciclo.
## Nota aggiuntiva: chiamate `Binder` e rientranza dei mutex
Una caratteristica non molto conosciuta di `Binder` è che supporta l'invio di chiamate ricorsive al thread originale.
Cioè, se il processo A effettua una chiamata `Binder` sincrona al processo B e poi il processo B, mentre la gestisce sullo stesso thread, effettua una chiamata `Binder` sincrona al processo A, quella chiamata nel processo A verrà smistata nello stesso thread che è in attesa del completamento della chiamata originale al processo B.
Un'altra cosa è che le sezioni `synchronized () {}` in Java sono mutex rientranti, il che significa che se vi entri due volte dallo stesso thread, ti farà entrare e non andrà in deadlock.
Questo significa che, in teoria, mentre teniamo bloccato `ActivityTaskManagerService.mGlobalLock`, potremmo comunque avviare l'app Impostazioni usando `startActivity(new Intent(Settings.ACTION_SETTINGS))` e riusciremmo a entrare nel [blocco `synchonized` che stiamo bloccando](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d), ma l'avvio di quell'`Activity` comporta anche la creazione di una `Task`, che prevede la chiamata di [`notifyTaskCreated()`, che pubblica un messaggio](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) su [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547), e [la sua gestione](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) tenta di acquisire da un altro thread il lock che stiamo bloccando. Quindi finché non rilasciamo `ActivityTaskManagerService.mGlobalLock`, il thread `DisplayThread` rimarrà bloccato. In seguito, la procedura di avvio dell'`Activity` prevede [l'invio di un messaggio allo stesso thread per avviare il processo dell'app](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c). Tutto ciò significa che in questo caso il processo dell'app non verrà avviato finché non rilasciamo il lock, e il motivo per cui tenevamo bloccato il lock in primo luogo era impedire che la transazione `attachApplication()` terminasse, così da poter estrarre gli handle da essa, ma in questo caso quella transazione non partirebbe effettivamente.
Anche se avviassimo un'`Activity` che farà parte della stessa `Task` di quella corrente (cioè avvieremmo un'`Activity` diversa dall'app Impostazioni, una che non specifica `android:launchMode="singleTask"`), quella procedura coinvolgerà comunque [`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), che qui ha lo stesso impatto di `notifyTaskCreated()`.
Quindi, mentre il mio thread poteva chiamare metodi che usano `synchronized (ActivityTaskManagerService.mGlobalLock) {}`, l'avvio di un nuovo processo dell'app dopo `startActivity()` comportava l'uso di quel lock da un thread diverso, e questo non era utile in questo caso; ho quindi optato per attivare l'avvio del processo dell'app tramite `ContentProvider`.
## Nota aggiuntiva: altri modi per usare `IApplicationThread`
`IApplicationThread` è un handle molto privilegiato, quindi considero il suo utilizzo dopo averlo ottenuto come post-exploitation.
In questo exploit l'ho usato direttamente per richiedere l'esecuzione di codice nel processo target, sfruttando il fatto che l'accesso a quell'operazione è regolato da una capability (il possesso dell'oggetto `Binder`, che qui abbiamo fatto trapelare) e non da `Binder.getCallingUid()`.
Aggiungere il controllo di `Binder.getCallingUid()` in [`ApplicationThread.scheduleReceiver()` (che abbiamo usato qui per richiedere l'esecuzione di codice)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) e in altri metodi di `ApplicationThread` (poiché `scheduleReceiver()` non è l'unico metodo in `IApplicationThread` che consente il caricamento di codice) non impedirebbe comunque di usare `IApplicationThread` per caricare codice nel processo di un'altra app, poiché un attaccante potrebbe passare l'`IApplicationThread` fatto trapelare al posto del proprio a `attachApplication()`.
Oltre a caricare codice nel processo, avere `IApplicationThread` consente di eseguire [`grantUriPermission()` usando i privilegi del processo a cui appartiene quell'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).
unparcel(boolean itemwise)sourcemParcelledDataParcelBundlerecycleParceltrueParcelBundleinitializeFromParcel chiama recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) per leggere il contenuto della mappa chiave-valore. Le chiavi sono String e i valori vengono letti usando readLazyValue(), creando oggetti LazyValue per i valori dei tipi che vengono scritti insieme al prefisso di lunghezza. readArrayMap() restituisce un valore che indica se è consentito riciclare il Parcel. Se erano presenti oggetti LazyValue, recycleParcel viene impostato a false e il Parcel a cui si riferiscono i LazyValue non verrà riciclato (c'è un'eccezione a questo, ma non è rilevante qui, la descriverò nella sezione "Nota aggiuntiva: Bundle.clear()")unparcel(), mMap viene impostato (non null) e mappa le chiavi String o ai valori effettivi se sono pronti o agli oggetti LazyValuegetValue(), che mappa la chiave (String) all'indice (int) e lo passa a getValueAt()LazyValue.apply() riavvolge il Parcel alla posizione di LazyValue.mPosition e chiama il normale Parcel.readValue() di cui ho già parlatoLazyValue viene sostituito in mMap, così che la successiva chiamata Bundle.get*() per la stessa chiave restituisca direttamente il valore e la deserializzazione di LazyValue non venga ripetuta. Quando il Bundle viene inoltrato, quel valore verrà serializzato di nuovo invece di copiare i dati originali verbatim (tuttavia, dopo che il Bundle inoltrato viene letto, quel valore sarà di nuovo un LazyValue e qualsiasi possibile disallineamento writeToParcel/createFromParcel non potrà influenzare altri valori)ParcelmaybeWriteSquashed()system_serverBundleParcel.writeParcelable()Parcelable.writeToParcelBinder, viene scritto 0 per indicare che non ci sono più elementi in questa transazione e i prossimi elementi verranno inviati in un'altra transazioneParcelableListBinder ha ricevuto il numero di elementi specificato nella prima transazione, invoca la lambda passata al suo costruttore, che in questo caso assegna la lista ricevuta a MediaSessionRecord.mQueueBinderParceledListSlice viene letto da un Parcel, legge la prima parte direttamente dal Parcel e poi se non tutti gli elementi sono stati scritti inline, chiama il Binder che è stato scritto nel Parcel per recuperare questi elementiBundleParcelParcelBinderParcel.recycle()RemoteViewsParcelmakeOwnedLeakermakeHolderLeakerRemoteViews.readActionsFromParcel()