
Exploit für CVE-2022-20452, Privilegieneskalation auf Android von einer installierten App zur System-App (oder einer anderen App) über LazyValue unter Verwendung von Parcel nach recycle()
Android 13 führt viele Verbesserungen ein, um den Parcel-Serialisierungsmechanismus abzusichern.
Das ist großartig – es beseitigt definitiv viele Schwachstellen oder macht sie nicht ausnutzbar. Außerdem beschreiben sie, wie mein früherer Exploit, der es Apps ermöglichte, ihren Code in andere Apps (einschließlich System-Apps) zu laden, geknackt wurde.
Aber jetzt bin ich mit einem neuen Exploit zurück, der dasselbe erreicht, wenn auch auf andere Weise. Er nutzt die folgenden Schwachstellen aus, die während der genannten Parcel-Härtung eingeführt wurden:
![Screenshot einer Anwendung, die Text anzeigt. Titel: LeakValue. Haupttext: Created 6 ValueLeaker-s. Locking ActivityTaskManagerService. Locked ActivityTaskManagerService. Unlocking ActivityTaskManagerService. Unlocked ActivityTaskManagerService. leakedBinders=[android.os.BinderProxy@f06702e]. Leaked interface: android.app.IApplicationThread. Requesting code execution. Shellcode has been executed in uid=1000 pid=6904 packageName=com.android.settings uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0. Am unteren Bildschirmrand befinden sich zwei Schaltflächen: START und MANUAL TESTING](Screenshot_20220723-081920.png)
(Auch logcat von der App-Ausführung, die Ausnutzung ist in den Protokollen laut)
Parcel- und Parcelable-Mismatch-FehlerDie Parcel-Klasse von Android ist die Grundlage der Kommunikation zwischen Prozessen.
Objekte können das Parcelable-Interface implementieren, um das Schreiben in eine Parcel zu ermöglichen, zum Beispiel (kopiert von 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());
} }
Beachten Sie, dass `Parcel` intern die Position speichert, an der ein Schreib- oder Lesevorgang durchgeführt wird. `readString()` parst Daten in einen String und erhöht gleichzeitig die Position. Diese Position kann manuell über [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)) abgerufen/gesetzt werden. Implementierungen des `Parcelable`-Interfaces müssen sicherstellen, dass ihr `writeToParcel` und `createFromParcel` die gleiche Datenmenge schreiben/lesen, da sonst alle nachfolgenden Lesevorgänge Daten von falschen Offsets erhalten.
[`Bundle`](https://developer.android.com/reference/android/os/Bundle) (eine Schlüssel-Wert-Zuordnung, die zwischen Prozessen gesendet werden kann) kann eine [Vielzahl von Objekten enthalten, die über `writeValue()` in `Parcel` geschrieben werden können](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937). Wenn der Inhalt eines `Bundle` aus einem `Parcel` gelesen wird, kann jede im System verfügbare `Parcelable`-Klasse dort gelesen werden.
`Bundle` verzögert die tatsächliche Analyse des Inhalts, indem die Länge der gesamten parcelled-Daten in den `Parcel` geschrieben und dann [der relevante Teil des ursprünglichen Parcel in einen sekundären Parcel kopiert wird, der in `mParcelledData` gespeichert wird](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683) (dies ermöglicht es z.B. [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)), `Parcelable`s bereitzustellen, die in `system_server` nicht verfügbar sind; das gesamte `Bundle` wird dann unverändert an `system_server` übergeben und zurück, ohne den Inhalt zu analysieren).
Sobald jedoch auf einen Wert im `Bundle` zugegriffen wurde, [wurden alle Werte im `Bundle` entpackt](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) und [jedes vorhandene Schlüssel-Wert-Paar wurde analysiert](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). Enthielt eine solche Karte ein `Parcelable` mit unausgeglichenen `writeToParcel`- und `createFromParcel`-Methoden und wurde dieses `Bundle` später an einen anderen Prozess weitergeleitet, konnte dieser andere Prozess einen anderen Inhalt des `Bundle` sehen. Dies machte alle [solcher Unstimmigkeiten in Klassen, die im System verfügbar sind, zu Schwachstellen](https://github.com/michalbednarski/ReparcelBug), da es [Stellen im System gibt, an denen `Bundle` als sicher überprüft](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) und dann an einen anderen Prozess weitergeleitet wird.
In diesem Writeup nenne ich ein solches `Bundle`, das einen Inhalt präsentiert und nach der Weiterleitung einen anderen, selbstveränderndes `Bundle`.
Ein weiterer wichtiger Punkt hier ist, dass `Parcel` neben bloßen Bytes (Strings, Zahlen, daraus zusammengesetzte Objekte) auch Dateideskriptoren und `Binder` enthalten kann. `Binder` sind Objekte, auf die man RPC-Aufrufe tätigen kann: Ein Prozess erstellt ein `Binder`-Objekt und überschreibt die [`onTransact()`-Methode](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Dann wird der `Binder` an einen anderen Prozess übergeben; im Beispielcode oben sehen Sie `read`/`writeStrongBinder()`-Aufrufe, die zum Lesen und Schreiben in den `Parcel` verwendet werden. Im anderen Prozess wird bei Verwendung von `readStrongBinder()` ein `BinderProxy`-Objekt erstellt (hinter dem [`IBinder`-Interface](https://developer.android.com/reference/android/os/IBinder) versteckt). Dieser andere Prozess kann dann [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) auf diesem Objekt aufrufen, und im ursprünglichen Objekt wird `onTransact()` ausgeführt. Normalerweise schreibt man jedoch nicht manuell `transact()`/`onTransact()`, sondern [verwendet stattdessen AIDL](https://developer.android.com/guide/components/aidl).
# `LazyValue` tritt auf den Plan: das Ende der selbstverändernden `Bundle`s
Da es in der Vergangenheit viele Fälle von Klassen mit `writeToParcel`/`createFromParcel`-Unstimmigkeiten gab, löst Android 13 das Problem, dass eine solche Klasse irgendwo im System vorhanden ist und die Konstruktion eines selbstverändernden `Bundle` ermöglicht, indem es [`LazyValue` einführt](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/).
Wenn nun `writeValue` verwendet wird, wird, falls der zu schreibende Wert kein primitiver ist, [auch die Länge des Werts in den `Parcel` geschrieben](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804).
Wenn eine normale App direkt `Parcel.readValue()` verwendet, [läuft alles wie zuvor ab, außer dass eine Warnung ausgegeben wird, wenn die aus `Parcel` gelesene `length` nicht mit der Größe der tatsächlich gelesenen Daten übereinstimmt](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804) (Beachten Sie jedoch, dass [`Slog.wtfStack` niemals wirft](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577)).
`Bundle` verwendet nun jedoch stattdessen [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804).
Schauen wir uns genauer an, wie es funktioniert: In der Klasse `LazyValue` gibt es einen [schönen Kommentar, der die Struktur der `LazyValue`-Daten innerhalb von `Parcel` erklärt](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 ist ein Verweis auf das ursprüngliche Parcel, auf dem readLazyValue() aufgerufen wurde.
mPosition und mLength beschreiben die Position der gesamten LazyValue-Daten im ursprünglichen Parcel, einschließlich type und length.
"length" (ohne "m" am Anfang) bezieht sich auf den Längenwert, wie er in das Parcel geschrieben wurde, und schließt den Header (type und length) aus.
Hier ist also, was passiert, wenn jemand (entweder System oder App) einen Wert aus einem Bundle nimmt, das aus einem Parcel gelesen wurde:
get*()-Methoden der Bundle-Klasse, z. B. neues getParcelable() mit Typargument (Der Ablauf ist für neue und alte Methoden gleich, nur stellen neue Methoden sicher, dass das clazz-Argument nicht null ist, während alte es auf null setzen).unparcel() wird aufgerufen, was prüft, ob dieses Bundle mParcelledData hat (d. h. es wurde aus einem Parcel gelesen, aber noch kein Wert abgerufen und die Schlüsselnamen wurden noch nicht entpackt; falls nicht, springe zu Schritt 5).unparcel() delegiert an , . wird auf gesetzt, eine Kopie des , die das erstellt hat, und der Parameter wird auf gesetzt, um anzuzeigen, dass das übergebene dem gehört und es in Ordnung ist, darauf aufzurufen.Wenn das Bundle weitergeleitet wird, während es noch LazyValue enthält (d. h. dieser bestimmte Wert wurde nicht abgerufen, aber ein anderer Wert aus diesem Bundle wurde abgerufen (also wurde unparcel() aufgerufen, aber LazyValue.apply() für dieses Element nicht)):
LazyValue wird von Parcel.writeValue() erkannt und das Schreiben wird an LazyValue.writeToParcel() delegiert.LazyValue.writeToParcel() verwendet out.appendFrom(source, mPosition, mLength), um die gesamten LazyValue-Daten aus dem ursprünglichen Parcel zu kopieren (wiederum enthalten mPosition und mLength den LazyValue-Header, sodass auch type und length aus dem ursprünglichen kopiert werden).Parcel.ReadWriteHelper und Parcel.readSquashed(Details davon sind für diesen Exploit nicht wichtig; hier ist nur relevant, dass diese Mechanismen existieren)
Eine weitere interessante Funktion von Parcel ist die optionale Fähigkeit, geschriebene Strings und Objekte zu deduplizieren.
Die Deduplizierung von Strings erfolgt durch Überschreiben der Parcel.ReadWriteHelper-Klasse: Parcel.readString() delegiert tatsächlich an den ReadWriteHelper, und der Standard-Helper liest direkt einen String aus dem Parcel.
Eine alternative Implementierung von Parcel.ReadWriteHelper kann readString-Aufrufe ersetzen durch vorheriges Lesen eines Pools von Strings und Verwenden von readInt, um Indizes von Strings im Pool zu erhalten; dies wird jedoch nie mit app-kontrollierten Parcels durchgeführt.
Parcel bietet die hasReadWriteHelper()-Methode, die es Aufrufern ermöglicht, das Vorhandensein eines solchen Deduplizierungsmechanismus zu erkennen und inkompatible Funktionen zu deaktivieren.
Ein weiterer in Parcel verfügbarer Deduplizierungsmechanismus ist das Squashing:
Parcel.allowSquashing() aktiviert werden.Parcel.maybeWriteSquashed(this) auf. Wenn diese Methode true zurückgibt, bedeutet dies, dass das Objekt bereits in dieses Parcel geschrieben wurde und jetzt nur noch ein Offset zum vorherigen Objektdaten in das Parcel geschrieben wird. Andernfalls (entweder ist Squashing nicht aktiviert oder das Objekt wird zum ersten Mal geschrieben) schreibt maybeWriteSquashed Null als Offset, um anzuzeigen, dass das Objekt nicht gesquasht ist, und gibt false zurück, um dem Aufrufer mitzuteilen, dass er die eigentlichen Objektdaten schreiben soll.Parcel.readSquashed aufgerufen und die eigentliche Lesefunktion wird als Lambda übergeben. readSquashed prüft, ob der von geschriebene Offset darauf hinweist, dass ein weiteres Vorkommen des Objekts zuvor gelesen wurde: falls ja, wird das zuvor gelesene Objekt zurückgegeben, andernfalls wird das bereitgestellte Lambda aufgerufen, um es jetzt zu lesen.Parcel.recycle()Auf der Java-Seite können Parcel-Objekte in einen Pool recycelt werden. Sobald man mit einem Parcel fertig ist, ruft man recycle() darauf auf, und beim nächsten Aufruf von Parcel.obtain() erhält man das zuvor recycelte Parcel. Dies reduziert die Anzahl der Objektzuweisungen und die anschließende Garbage Collection.
Andererseits bringt eine solche manuelle Speicherverwaltung die Möglichkeit von Use-After-Free-ähnlichen Fehlern in Java mit sich (obwohl mit Typensicherheit, anders als beim üblichen Use-After-Free in C).
Wie oben erwähnt, erstellt Bundle eine Kopie des Parcel und ruft Parcel.recycle() nicht auf, wenn LazyValue vorhanden ist. Dies ist jedoch nicht der Fall, wenn Parcel.hasReadWriteHelper() true ist. In diesem Fall:
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle); wird aufgerufen. Dies bedeutet, dass Bundle das Parcel nicht recycelt, da es noch dem Aufrufer gehört. Allerdings werden dabei LazyValues erstellt, die auf das ursprüngliche Parcel verweisen und dessen Lebensdauer überdauern könnten.unparcel(/* itemwise */ true) aufgerufen, was getValueAt() auf allen Elementen verwendet, um alle in Bundle vorhandenen LazyValues durch tatsächliche Werte zu ersetzen.Können wir diese LazyValues in Schritt 2 überleben lassen und dieses Verhalten in Use-After-Recycle umwandeln?
Wenn die Deserialisierung fehlschlägt (z. B. die im Parcel angegebene Klasse konnte nicht gefunden werden), wird eine BadParcelableException ausgelöst und dann von getValueAt() abgefangen. Wenn das statische Feld BaseBundle.sShouldDefuse true ist, wird keine Ausnahme ausgelöst und die Ausführung fährt fort, wobei das Bundle einen LazyValue enthält, der auf das ursprüngliche Parcel verweist. sShouldDefuse gibt an, dass nicht verfügbare Werte aus Bundle in einem bestimmten Prozess keine Ausnahmen verursachen sollen, und wird in system_server auf gesetzt.
Wenn das ursprüngliche Parcel recycelt wird und danach das daraus gelesene Bundle in ein anderes Parcel geschrieben wird, werden die Inhalte des ursprünglichen Parcel in das Ziel-Parcel kopiert. Zu diesem Zeitpunkt könnte das ursprüngliche Parcel jedoch für etwas anderes wiederverwendet werden und Daten aus einem nicht verwandten IPC-Vorgang könnten kopiert werden.
Okay, aber wie erreichen wir, dass Parcel.hasReadWriteHelper() true ist, während das von uns bereitgestellte Bundle deserialisiert wird?
Es stellt sich heraus, dass die RemoteViews-Klasse (normalerweise z. B. zum Übergeben von Widgets an den Startbildschirm verwendet) explizit einen ReadWriteHelper setzt, wenn darin verschachtelte Bundles gelesen werden. Dieser ReadWriteHelper führt keine String-Deduplizierung durch und ist nur vorhanden, um zu veranlassen, dass Bundle das Kopieren von Daten in ein sekundäres Parcel überspringt. Der Grund dafür ist, dass RemoteViews Squashing aktiviert, um darin verschachtelte ApplicationInfo-Objekte zu deduplizieren. Dies könnte jedoch auch dazu führen, dass ApplicationInfo-Objekte, die sich innerhalb eines befinden, gesquasht werden. Daher kann das Lesen dieses nicht aufgeschoben werden, da diese gesquashten Objekte dann nicht entsquasht werden könnten.
Parcelables in system_server ablegen und zurückerhaltenWir möchten nun, dass system_server unsere RemoteViews liest, die ein Bundle mit einem LazyValue enthalten, das nicht deserialisiert werden kann, und später (in einer anderen Binder-IPC-Transaktion) dieses Objekt an uns zurücksendet.
Dies könnte wahrscheinlich auf legitime Weise geschehen, z. B. durch Registrierung als App Widget Host (dies würde jedoch eine Benutzerinteraktion zur Erteilung der Berechtigung erfordern) oder durch Posten einer Notification mit gesetzter contentView (dies würde jedoch eine Interaktion mit anderen Prozessen verursachen und/oder für den Benutzer sichtbar sein; ich wollte beides vermeiden).
Ich habe mich stattdessen dafür entschieden, eine MediaSession zu erstellen und setQueue(List<MediaSession.QueueItem> queue) darauf aufzurufen, um ein Objekt an system_server zu senden, und es später durch die List<MediaSession.QueueItem> getQueue()-Methode von MediaController zurückzuerhalten (die über MediaSession.getController() abgerufen werden kann). Obwohl diese Methoden nicht so aussehen, als könnten sie RemoteViews akzeptieren, tun sie dies tatsächlich dank Java Type Erasure und der Tatsache, dass sie unter der Haube mit generischen Serialisierungsoperationen auf List implementiert sind.
Ich verwende diese SDK-Methoden jedoch nicht; ich schreibe manuell Daten für die zugrunde liegenden Binder-Transaktionen (weil ich fehlerhafte serialisierte Daten schreiben und später lesen muss). Sehen wir uns daher an, wie diese Methoden funktionieren.
Beide Methoden mussten berücksichtigen, dass die Gesamtgröße der Warteschlange die maximale Größe einer Binder-Transaktion überschreiten könnte, sodass die Übertragung auf mehrere Transaktionen aufgeteilt werden kann.
Das Senden der "Warteschlange" an system_server läuft normalerweise wie folgt ab:
MediaSession.setQueue() ruft zuerst ISession.getBinderForSetQueue() auf.system_server-Seite konstruiert und gibt diese Methode ein ParcelableListBinder-Objekt zurück](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1026;drc=03c34f57c05feecfb090de3917787f049cb5f804).MediaSession.setQueue() ParcelableListBinder.send() auf, das die Listeninhalte an den bereitgestellten Binder sendet, möglicherweise über mehrere Transaktionen:
Das Abrufen der "Warteschlange" hingegen läuft etwas anders ab:
MediaController.getQueue() ruft einfach ISessionController.getQueue() auf und entpackt den empfangenen ParceledListSlice.system_server-Seite packt getQueue() einfach mQueue in einen ParceledListSlice und gibt es zurück.ParceledListSlice.writeToParcel() und createFromParcel(). Insbesondere writeToParcel() schreibt beim Erreichen der sicheren Größenbeschränkung ein -Objekt, mit dem die nächsten Blöcke abgerufen werden können.Warum diese unterschiedlich sind: Es gibt eine laufende Bemühung sicherzustellen, dass system_server keine ausgehenden synchronen Binder-Aufrufe an andere Apps tätigt, da dies das gesamte system_server aufhängen könnte, wenn diese Aufrufe hängen bleiben. Dies bedeutet, dass system_server keine ParceledListSlices empfangen sollte. Obwohl es Code gibt, der vor ausgehenden synchronen Transaktionen von system_server warnt, konnte er noch nicht durchgesetzt werden, da es immer noch Fälle gibt, in denen system_server solche Aufrufe tätigt, z. B. durch tatsächlichen Empfang von ParceledListSlice.
Wir haben nun die notwendigen Primitive, um system_server dazu zu bringen, parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size) auszuführen.
Wir könnten entweder zufällig versuchen, Parcel-Daten aus dem System zu ziehen, oder die Dinge so arrangieren, dass wir etwas Bestimmtes erhalten.
Es gibt folgende Überlegungen:* Wenn Parcel.recycle() aufgerufen wird, wird der Inhalt dieser Parcel gelöscht. Das bedeutet, dass die Parcel, aus der wir Daten kopieren möchten, nicht recycle()-t werden darf, was ungefähr bedeutet, dass wir keine Daten aus einer abgeschlossenen Binder-Transaktion entnehmen können.
Parcel eines Bundles im System entnehmen (dies umfasst Intent-Extras und savedInstanceState von Activity). Diese werden normalerweise überhaupt nicht recycle()-t (sie werden vom Garbage Collector bereinigt und kehren nicht in den Pool zurück; wenn der Pool erschöpft ist, erzeugt Parcel.obtain() neue Parcel-Objekte. Natürlich werden die Parcels, auf die wir eine Referenz halten, nicht vom GC erfasst, selbst wenn das System keine andere Verwendung für sie hat).Parcels, die für eingehende Binder-Transaktionen verwendet werden, verwenden einen anderen Pool als andere Parcels im System. Wenn eine ausgehende Binder-Transaktion durchgeführt wird, kopiert Daten in eine sekundäre , oder eine App verwendet für eigene Zwecke, dann wird . Wenn hingegen eine eingehende -Transaktion vorliegt, , das . In beiden Fällen wird anschließend aufgerufen und kümmert sich darum, . Das bedeutet, dass der Exploit aus einer lesen lassen muss, die dem gleichen Pool angehört, aus dem wir Daten abziehen möchten. Bevor ich mich für eine bestimmte Variante entschieden habe, habe ich beide geschrieben, daher finden Sie sowohl die Methoden als auch in meiner Letztendlich habe ich beschlossen, zu versuchen, den IApplicationThread-Binder zu schnappen, der von der App an den system_server gesendet wird, wenn der App-Prozess startet, und den der system_server verwendet, um der Anwendung mitzuteilen, welche Komponenten sie laden soll.
Wenn der Anwendungsprozess ursprünglich startet, ist eine der ersten Aktionen das Senden des IApplicationThread an den system_server durch Aufruf von attachApplication(), und dies ist die Transaktion, aus der ich diesen Binder herausziehen werde. Es gibt andere Stellen, an denen IApplicationThread in eine Parcel gelegt wird, z. B. zur Identifizierung des Aufrufers durch das System beim Starten einer Activity (aber ich hatte nicht viel Kontrolle darüber, wann die Ziel-App das tut) oder vom System an die Anwendung gesendet als Teil des Activity-Lebenszyklus-Managements (dies geschieht jedoch in einer Oneway-Transaktion, die vom system_server ausgeht, und die Chancen, das Rennen gegen Parcel.recycle() zu gewinnen, wären gering).
Abgesehen davon ist es auch nicht trivial, den Binder zu schnappen, der vom system_server während der attachApplication()-Transaktion empfangen wird, und es gab einige Probleme zu überwinden.
ParcelDas erste Problem beim Herausziehen des IApplicationThread-Binder aus der Parcel, aus der die Daten für attachApplication() empfangen werden, ist, dass dieser Binder sich an einer ziemlich frühen/niedrigen dataPosition() befindet, viel niedriger, als es unser LazyValue im Bundle in den RemoteViews sein könnte.
Die Daten für die attachApplication()-Transaktion bestehen nur aus RPC-Header, gefolgt vom IApplicationThread-Binder. Der RPC-Header (geschrieben durch Parcel.writeInterfaceToken()) besteht aus einigen ints und dem Namen der Schnittstelle, in diesem Fall "android.app.IActivityManager".
Um das in RemoteViews eingebettete Bundle zu lesen, müssten wir mindestens (einige kleinere Punkte werden übersprungen) über folgende Punkte hinwegkommen:
readParcelable zu startenParcelable: "android.view.RemoteViews"ApplicationInfo-Objekt, das in RemoteViews vorhanden ist (es muss auch nicht null sein und einen nicht-null packageName haben, sonst schlägt RemoteViews.writeToParcel() fehl, wenn wir versuchen, dieses Objekt zurückzusenden)Nun müssen wir im Bundle nur einen String-Schlüssel setzen, und das Lesen von LazyValue beginnt; die Position in der Parcel wird gemerkt, aber zu diesem Zeitpunkt ist sie weit über der Position, an der sich der IApplicationThread-Binder befinden würde.
Können wir vielleicht bei Erreichen dieses Punktes die Position in der Parcel zurückspulen? Mit anderen Worten, könnten wir Parcel.setDataPosition() mit einem Wert aufrufen, der auf eine frühere Position als die aktuelle zeigt?
Es stellt sich heraus, dass wir das können, dank eines weiteren Fehlers in LazyValue. Dies ist der Code, der zum Lesen verwendet wird:```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);
}
}
([Original in AOSP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804), `LazyValue`-Konstruktor weist Parameter nur Feldern zu)
Die Sache ist, dass `MathUtils.addOrThrow()` auf Überlauf prüft, [aber mit negativen Werten völlig zufrieden ist](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)
Wenn wir versuchen würden, `Parcel.writeValue()` auf `LazyValue` mit negativem `mLength` (gefüllt vom `valueLength`-Parameter) aufzurufen, dann [würde das in `appendFrom()` einen Fehler werfen](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804), aber da wir uns während des Lesens eines `Bundle` mit `Parcel.hasReadWriteHelper()` = `true` befinden, werden alle `LazyValue`s nach dem Lesen entpackt, und wir mussten absichtlich ein fehlerhaftes `Parcelable` darin platzieren, damit es als `LazyValue` bestehen bleibt. Wenn wir gültige geparcele Daten an der Position platzieren, an der sich `LazyValue` befindet, wird es entpackt, und wie bereits erwähnt, führt eine nicht übereinstimmende Länge nur zu einer Nachricht im `logcat`. Dieser spezielle Exploit setzt den Typ auf `VAL_MAP` und die Anzahl der Schlüssel-Wert-Paare auf Null. Im `logcat` sehen wir beim Lesen dieses Werts folgende Nachricht: "`E Parcel : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP consumed 4 bytes, but -540 expected.`"
(Auch `LazyValue` mit negativ angegebener Länge kann verwendet werden (ohne andere in diesem Writeup beschriebene Bugs) um ein sich selbst veränderndes `Bundle` zu erstellen, das Ding, das `LazyValue` beseitigen sollte. Aber das ist eine andere Geschichte (und wurde separat an Google gemeldet), bei diesem Exploit ziele ich auf mehr ab)
Wie weit wollen wir zurückspulen?
Nach dem Aufruf von `setDataPosition()` wird das Lesen zum nächsten Schlüssel-Wert-Paar im `Bundle` fortgesetzt, also müssen wir eine Position wählen, an der wir Folgendes haben:
1. Bundle-Schlüssel, gelesen mit `Parcel.readString()`, kann so ziemlich alles sein, einschließlich einer ungültigen Länge (negativ oder die Gesamtgröße des `Parcel` überschreitend), in diesem Fall würde `readString()` `null` zurückgeben, was ein gültiger Schlüssel im `Bundle` ist
2. Werttyp, dies muss einer der [Typen sein, für die `isLengthPrefixed()` `true` zurückgibt](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804)
3. Wertlänge, auch dies muss ein von uns kontrollierter Wert sein, `Parcel.appendFrom()` schlägt fehl, wenn die Länge nicht ausgerichtet ist oder die Gesamtgröße des Quell-`Parcel` überschreitet
Welche Position im `Parcel` könnte das also sein, wenn man bedenkt, dass dieselben Daten bereits gelesen wurden und notwendig sind, um diesen Punkt zu erreichen:
* Nicht vor dem Namen des `Parcelable` (`"android.view.RemoteViews"`), weil nicht genug Platz ist
* Nicht im Namen des `Parcelable`, weil wir keinen Typ und keine Länge setzen können
* Nicht direkt nach dem Namen des `Parcelable`, weil das Erste in `RemoteViews` `mode` ist, das wir [auf `MODE_NORMAL` setzen müssen, um unseren Code zu erreichen](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* Nicht danach, weil das hinter dem Punkt liegt, an dem sich der `IApplicationThread` `Binder` befindet
Hmm, es gibt keinen guten Platz, wenn `RemoteViews` das äußerste Objekt in den geparcelten Daten ist
Wir müssen ein anderes `Parcelable` finden, das:
1. Am Anfang oder in der Nähe des Anfangs eine Stelle hat, an der wir beliebige Daten platzieren können (z.B. `int`s oder `String`s, die nur Daten sind und den Serialisierungsprozess nicht beeinflussen)
2. `RemoteViews` enthalten kann (entweder direkt oder über beliebiges `readParcelable`)
3. Einen nicht zu langen vollqualifizierten Klassennamen hat, da wir immer noch durch die Position, an der sich `IApplicationThread` im Ziel-`Parcel` befindet, in der Größe eingeschränkt sind
Also habe ich die Liste der `Parcelable`-Klassen im System genommen, nach aufsteigender Länge des vollqualifizierten Klassennamens sortiert und begann, Elemente auf dieser Liste zu überprüfen, ob sie Bedingung 2 erfüllen
Auf diese Weise bin ich zu [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216) gelangt, was dieser Exploit verwendet. Der Prozess des Lesens unseres vorbereiteten Objekts aus dem `Parcel` läuft nun wie folgt ab:
* [Flag für Vorhandensein eines Elements](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) um `readParcelable` zu starten
* [Name des `Parcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804): `"android.os.Message"`
* Einige [`int`s, die wir auf beliebige Werte setzen können](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216) werden in Felder eingelesen
* Wir erreichen den `readParcelable()`-Aufruf, der den gesamten oben beschriebenen Weg durch `RemoteViews` geht und beginnt, ein `Bundle` zu lesen, wobei `Parcel.hasReadWriteHelper` `true` ist
* Dieses `Bundle` gibt an, zwei Schlüssel-Wert-Paare zu haben. Im ersten Wert haben wir ein `LazyValue` mit negativer Länge, das `Parcel.setDataPosition()` auf die Position auslöst, an der der `"android.os.Message"` `String` ist
* Das Lesen fährt mit dem zweiten Schlüssel-Wert-Paar fort, der Schlüssel ist `"android.os.Message"` und Typ, Länge und Daten des `LazyValue` werden aus den im dritten Aufzählungspunkt beschriebenen `int`s übernommen. Ich habe ein `LazyValue` mit den gewünschten `mPosition` und `mLength` erhalten. Hurra!
* Nachdem die `LazyValue`s gelesen wurden, werden sie entpackt. Das mit negativer Größe wird erfolgreich entpackt und durch eine leere `Map` ersetzt, während das andere bei der Deserialisierung fehlschlägt, aber diese Ausnahme wird abgefangen und `LazyValue` bleibt einfach im `Bundle`
* `readParcelable()` endet, aber das ist nicht das Ende der `Message`-Daten. `Message.readFromParcel()` liest nun weiter Daten nach dem Zurückspulen und sieht Daten, die ursprünglich als Teil von `RemoteViews` geschrieben wurden. Wenn an diesem Punkt irgendetwas eine Ausnahme auslöst, wird der gesamte Plan vereitelt
* Erste mögliche Ausnahme: [es gibt einen `readBundle()`-Aufruf](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216). [`Bundle` hat einen magischen Wert und wenn dieser falsch ist, wird eine Ausnahme ausgelöst](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). Dieser magische Wert ist jedoch nicht vorhanden, wenn die Länge [Null](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91) oder [negativ](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804) ist, und das war der Fall, als die Länge der `LazyValue`-Daten auf den Wert gesetzt wurde, den ich benötigte, um `IApplicationThread` zu greifen. Da hatte ich einfach Glück
* Nächstes mögliches Problem könnte der [`Messenger.readMessengerOrNullFromParcel()`-Aufruf](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216) sein. Dies ist tatsächlich ein umschlossenes `Binder`-Objekt. Das Lesen dieses `Binder` schlägt fehl, weil `Binder` ein spezielles Objekt im `Parcel` ist und out-of-band annotiert werden muss, um gelesen zu werden. Dieses Problem wird [von `Parcel` auf nativer Seite erkannt und protokolliert, jedoch nicht als Fehler weitergegeben und es wird einfach `null` zurückgegeben](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)
# Verzögern von `attachApplication()`
Okay, im vorherigen Schritt haben wir erfolgreich ein Objekt erstellt, das es uns ermöglicht, das `IApplicationThread`-Objekt zu greifen, während die Methode `attachApplication()` ausgeführt wird
Die Sache ist, dass diese Methode schnell abgeschlossen wird und unsere Chancen in einem fairen Wettlauf gegen ihren Abschluss eher gering wären
Diese Methode erwirbt jedoch einige Mutexe (durch Verwendung von Java's `synchronized () {}`-Blöcken), wenn wir einen solchen Mutex erwerben und dort anhalten, würde auch diese Methode anhalten
Kommen wir nun zu einigen Dingen zurück, die bereits in diesem Writeup erwähnt wurden und für diesen Zweck nützlich sein werden:
* `Bundle` führt eine Deserialisierung seiner Werte durch, wenn auf diese Werte zugegriffen wird
* Es gibt die Klasse `ParceledListSlice`, die während der Deserialisierung einen blockierenden ausgehenden `Binder`-Aufruf an das in den serialisierten Daten angegebene Objekt tätigt
Wenn wir all diese Dinge zusammennehmen: Wenn wir im `system_server` eine Stelle finden, an der auf Inhalte eines von der App bereitgestellten `Bundle` unter einem Mutex zugegriffen wird, der auch von `attachApplication()` verwendet wird, können wir `attachApplication()` anhalten, bis die `Binder`-Transaktion zu unserem Prozess abgeschlossen ist
[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) ist eine Klasse, die verschiedene Parameter im Zusammenhang mit dem Start einer `Activity` beschreibt (z.B. Animation). Im Gegensatz zu anderen Klassen, die Parameter beschreiben, die an `system_server` übergeben werden, implementiert diese kein `Parcelable`, sondern stellt stattdessen eine Methode zur Konvertierung in ein `Bundle` bereit
Auf der `system_server`-Seite wird dieses [`Bundle` zurück in `ActivityOptions` konvertiert, was die Deserialisierung auslöst](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). Ich habe eine Stelle gefunden, an der [diese Operation durchgeführt wird, während der Mutex `ActivityTaskManagerService.mGlobalLock` in `ActivityTaskManagerService.moveTaskToFront()` gehalten wird](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)
Also rufe ich [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle)) auf und übergebe ein `Bundle`, das ein `ParceledListSlice` anstelle eines Werts mit dem erwarteten Typ enthält. Dieses [`ParceledListSlice` tätigt einen `Binder`-Aufruf zu meinem Prozess](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577) und bis ich von diesem Aufruf zurückkehre, bleibt der Mutex `ActivityTaskManagerService.mGlobalLock` gesperrt
# Erstellen mehrerer `LazyValue`s, die auf verschiedene `Parcel`s zeigen
`Parcel.recycle()` und `Parcel.obtain()` arbeiten nach dem [Last-In-First-Out-Prinzip](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))
Das bedeutet, dass ich, wenn ich ein manipuliertes `LazyValue` erstelle, während keine andere `Binder`-Transaktion zu `system_server` läuft, ein `LazyValue` erhalte, das auf das `Parcel` zeigt, das immer verwendet wird, wenn nur eine eingehende Transaktion zu `system_server` vorhanden ist (bis es passiert, dass zwei gleichzeitige eingehende Transaktionen zu `system_server` in einer nicht-Stapel-Reihenfolge starten und enden)
Da ich keine Kontrolle darüber habe, welche anderen Transaktionen zu `system_server` eingehen, habe ich zur Verbesserung der Exploit-Zuverlässigkeit mehrere `LazyValue`s erstellt, die auf verschiedene `Parcel`s zeigen
Da ich die Möglichkeit habe, eine synchrone `Binder`-Transaktion zu meinem Prozess vom `system_server` aus auszulösen, habe ich diese Fähigkeit genutzt, um `LazyValue` auf verschiedenen Rekursionsebenen zwischen meinem Prozess und dem `system_server` zu erstellen (obwohl ich diesmal ohne das Halten eines globalen Mutex tat)
Also:
* Ich erstelle ein `LazyValue`
* Ich löse einen Aufruf an `system_server` aus, `system_server` ruft mich zurück
* Ich erstelle ein `LazyValue`
* Ich löse einen Aufruf an `system_server` aus, `system_server` ruft mich zurück
* Ich erstelle ein `LazyValue`
* Ich löse einen Aufruf an `system_server` aus, `system_server` ruft mich zurück
* ...
Dann, sobald ich genug `LazyValue`s habe, beende ich dies, kehre von all diesen Aufrufen zurück und alle `Parcel`s, die durch diese Aufrufe reserviert wurden, werden `recycle()`d
Jedes von mir erstellte `LazyValue` ist in ein separates [`ParceledListSlice`, erstellt von `getQueue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804), eingewickelt und ich kann den `Binder` des `ParceledListSlice` aufrufen, um den `system_server` zu veranlassen, es zu serialisieren und an meinen Prozess zu senden
(Eine alternative Möglichkeit wäre, mehrere `MediaSession`s zu erstellen)
# Starten des Ziel-App-Prozesses
Jetzt haben wir alles, was wir brauchen, um `IApplicationThread` von `attachApplication()` zu erfassen, wenn es passiert, aber wir müssen `attachApplication()` noch auslösen
Im Allgemeinen [gibt es einige Arten von App-Komponenten, mit denen andere Apps interagieren können](https://developer.android.com/guide/components/fundamentals#Components), von denen jede den Start eines App-Prozesses erfordert
Ich wollte die System-Settings-App starten (die [unter der System-UID läuft](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) und daher Zugriff auf [alles hinter Android-Berechtigungen](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b) hat)
Zunächst habe ich versucht, sie über `startActivity()` zu starten, aber als ich das versuchte, wurde der Prozess erst gestartet, nachdem ich die `ActivityTaskManagerService`-Sperre freigegeben hatte. Details, warum das der Fall war, finden Sie im Abschnitt "Zusätzliche Anmerkung: `Binder`-Aufrufe und Mutex-Wiedereintritt", aber als Lösung habe ich beschlossen, das System um einen [`ContentProvider` dieser App](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) anstelle einer `Activity` zu bitten. Dies hatte den zusätzlichen Vorteil, dass eine Störung meiner Benutzeroberfläche vermieden wurde
Ich habe nicht die [offizielle `ContentResolver`-API verwendet, die vom SDK bereitgestellt wird](https://developer.android.com/reference/android/content/ContentResolver), sondern stattdessen die [systeminterne, da ich eine asynchrone API benötigte](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907), weil das Binden an den `ContentProvider` nicht abgeschlossen wäre, bis `attachApplication()`, das ich aufhänge, beendet ist, obwohl das Starten eines anderen Threads eine Alternative gewesen wäre
(Es spielt keine Rolle, was dieser bestimmte `ContentProvider` bietet, das Einzige, was relevant ist, ist, dass ich eine Verbindung zu ihm herstellen kann)
So starte ich den Prozess der Settings-App. Ich stelle zunächst sicher, dass er nicht bereits läuft, indem ich die [offiziell verfügbare Methode `ActivityManager.killBackgroundProcesses()`](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String)) verwende
# Alles zusammensetzen
Primitive sind jetzt beschrieben, also hier ist, wie alles zusammen funktioniert (dies ist so ziemlich die Transkription der Methode `MainActivity.doAllStuff()` dieses Exploits):
1. Zugriff auf verborgene APIs aktivieren (verborgene APIs sind keine Sicherheitsgrenze und es gibt [bereits öffentlich verfügbare Workarounds](https://www.xda-developers.com/bypass-hidden-apis/), obwohl ich hier eine Methode basierend auf [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String)) verwendet habe, die ich anderswo nicht gesehen habe)
2. (Nur wenn wir den Exploit nach dem ersten Versuch erneut ausführen) Verbindung zum `ContentProvider` freigeben, den wir in Schritt 6 während der vorherigen Ausführung hergestellt haben. Wir müssen dies tun, da sonst `ActivityManager.killBackgroundProcesses()` den Zielprozess nicht als "Hintergrund" betrachtet und ihn nicht beendet
3. Opfer-App-Prozess mit `ActivityManager.killBackgroundProcesses()` beenden, da `attachApplication()` nur beim Start des Prozesses aufgerufen wird
4. `system_server` auffordern, eine Reihe von Objekten zu erstellen, die `LazyValue` enthalten, das auf ein später recyceltes `Parcel` zeigt. Ich erhalte eine `ParceledListSlice`-`Binder`-Referenz für jedes Objekt, das `LazyValue` enthält, und ich kann eine `Binder`-Transaktion dazu durchführen, um das System zu veranlassen, es zurückzuschreiben. Jede Erstellung eines `LazyValue`-Objekts erfolgt in unterschiedlichen Tiefen von [gegenseitig rekursiven](https://en.wikipedia.org/wiki/Mutual_recursion) Aufrufen zwischen `system_server` und meiner App, um wahrscheinlich zu machen, dass jedes dieser `LazyValue`s eine verwaiste Referenz auf ein anderes `Parcel`-Objekt hat
5. Ich sperre `ActivityTaskManagerService.mGlobalLock`, indem ich einen Aufruf an `ActivityTaskManagerService.moveTaskToFront()` tätige, dem ich ein `Bundle` als Argument übergebe, das bei der Deserialisierung eine synchrone `Binder`-Transaktion zu meinem Prozess durchführt. Die nächsten Schritte werden von diesem Callback aus ausgeführt und daher mit gehaltener Sperre durchgeführt
6. Ich fordere von `ActivityManagerService` eine Verbindung zum `ContentProvider` der Opfer-App an (beachten Sie, dass kein "`Task`" im Namen vorkommt, `ActivityTaskManagerService` ist eine Klasse, die sich hauptsächlich auf die Handhabung von `Activity`-Komponenten von Apps konzentriert, während `ActivityManagerService` andere [App-Komponenten](https://developer.android.com/guide/components/fundamentals#Components) (sowie den gesamten Prozessstart) behandelt, diese [Aufteilung erfolgte in Android 10; zuvor war die Handhabung von `Activity` und anderen App-Komponenten in `ActivityManagerService`](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. Ich `sleep()` ein wenig, um dem neu gestarteten Prozess Zeit zu geben, `attachApplication()` aufzurufen
8. Während die Sperre noch gehalten wird, fordere ich alle zuvor erstellten `ParceledListSlice`-Objekte auf, ihre verbleibenden Inhalte zu senden (die nicht in die erste Transaktion passten), d.h. Objekte, die `LazyValue` enthalten, das auf recyceltes `Parcel` zeigt. Dann lese ich von einem fest codierten Offset, der der Position von `IApplicationThread` entspricht, die an `attachApplication()` übergeben wurde, das `Binder`-Objekt. Im Moment speichere ich nur die empfangenen `Binder`s in einer `ArrayList`, um nicht zu viel mit gehaltener Sperre zu tun
9. Dies ist das Ende des Codes, den ich vom in Schritt 5 gestarteten Callback aus ausführe. `ActivityTaskManagerService.mGlobalLock` wird entsperrt
10. Ich habe den `IApplicationThread` `Binder`. Jetzt kann ich ihn einfach verwenden, um meinen Code in die Opfer-App zu laden, wie im nächsten Abschnitt beschrieben
# Wie verwende ich `IApplicationThread`
Wie bereits erwähnt, wird der [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) `Binder` von der App an `system_server` gesendet, wenn der App-Prozess startet, und dann verwendet ihn `system_server`, um der App mitzuteilen, welche Komponenten sie laden soll
Es wird angenommen, dass dieses Objekt nur an `system_server` übergeben wird, daher gibt es dort keine `Binder.getCallingUid()`-basierten Prüfungen, so dass wir einfach direkt die von diesem Interface angebotenen Methoden aufrufen können
Ich habe [in meinem vorherigen Writeup beschrieben, wie ich durch Manipulation der `scheduleReceiver()`-Argumente Codeausführung erreiche](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). Jetzt ist die Situation dieselbe, nur dass ich diesmal `scheduleReceiver()` selbst aufrufe, während ich damals die Interpretation der Argumente des von `system_server` getätigten Aufrufs manipulierte
# Zusätzliche Anmerkungen
In diesem Abschnitt beschreibe ich einige Dinge, die sich letztendlich als nicht nützlich in diesem Fall erwiesen haben, obwohl sie Funktionen sein könnten, die es wert sind, bekannt zu sein, oder potenzielle Bugs darstellen
## Zusätzliche Anmerkung: `Bundle.clear()`Der Einfachheit halber habe ich hier das aktualisierte `Bundle` ohne [einen später eingeführten Commit beschrieben, der es erlaubt, dass `Parcel`, das in `Bundle` zur Sicherung von `LazyValue`s verwendet wird, durch Aufruf von `Bundle.clear()` recycelt werden kann](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/).
Wie in der Commit-Nachricht erwähnt, wird nachverfolgt, ob `Bundle` kopiert wurde, und in diesem Fall wird `clear()` das `Parcel` nicht recyceln.
Dieser Commit ändert jedoch auch die Semantik des Parameters/der Variable `recycleParcel` von [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1).
Bisher bedeutete `recycleParcel = false`, dass das `Parcel` nicht recycelt werden sollte, entweder weil der [Aufrufer `recycleParcel` auf `false` setzte, um anzuzeigen, dass das `Parcel` nicht im Besitz von `Bundle` ist](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1) oder weil es [basierend auf dem Ergebnis von `parcelledData.readArrayMap()` auf `false` gesetzt wurde](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1).
Jetzt sind die Gründe, warum `recycleParcel` `false` sein könnte, dieselben, aber die Interpretation hat sich geändert: Nun bedeutet das nicht "dieses `Parcel` nicht recyceln", sondern "das Recyceln des `Parcel`s bis zum Aufruf von `Bundle.clear()` aufschieben".
Das bedeutet, dass, wenn `clear()` auf einem `Bundle` aufgerufen würde, das mit `Parcel.hasReadWriteHelper() = true` erstellt wurde, dies dazu führen würde, dass das `Parcel` recycelt wird, während der Code, der die Erstellung dieses `Bundle`s aufruft, ebenfalls dieses `Parcel` recyceln würde, was zu einem doppelten `recycle()` führt. Dies verhält sich ähnlich wie ein Double-Free: Nachfolgende Aufrufe von `Parcel.obtain()` würden dasselbe Objekt zweimal zurückgeben.
Allerdings habe ich keinen Weg gefunden, `clear()` auf einem solchen `Bundle` aufzurufen.
Da ich dies ursprünglich geschrieben habe, wurde [das Verhalten von `recycle()` geändert, und nun ist ein zusätzliches Recyceln ein No-op mit möglichem Absturz durch `Log.wtf()`](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([abhängig von der Konfiguration, aber `system_server` stürzt nie ab](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056)). Ich würde sagen, dass das neue Verhalten immer noch gefährlich sein kann, insbesondere wenn wir die Möglichkeit haben, die Deserialisierung, die in einem anderen Prozess stattfindet, programmatisch zu blockieren, aber es gibt wirklich keinen guten Weg, um mit doppeltem Recyceln umzugehen.
## Zusätzliche Anmerkung: `Binder`-Aufrufe und Wiedereintritt von Mutexen
Eine nicht sehr bekannte Eigenschaft von `Binder` ist, dass es das Senden rekursiver Aufrufe an den ursprünglichen Thread unterstützt.
Das heißt, wenn Prozess A einen synchronen `Binder`-Aufruf an Prozess B tätigt und Prozess B dann während der Bearbeitung im selben Thread einen synchronen `Binder`-Aufruf an Prozess A tätigt, wird dieser Aufruf in Prozess A im selben Thread ausgeführt, der auf den ursprünglichen Aufruf an Prozess B wartet.
Eine andere Sache ist, dass `synchronized () {}`-Blöcke in Java wiedereintrittsfähige Mutexe sind, was bedeutet, dass, wenn Sie sie zweimal vom selben Thread betreten, dies zugelassen wird und kein Deadlock entsteht.
Das bedeutet theoretisch, dass wir, während wir `ActivityTaskManagerService.mGlobalLock` gesperrt halten, dennoch die Settings-App mit `startActivity(new Intent(Settings.ACTION_SETTINGS))` starten könnten, und wir würden erfolgreich in den [`synchronized`-Block eintreten, den wir blockieren](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d). Das Starten dieser `Activity` beinhaltet jedoch auch die Erstellung einer `Task`, was den Aufruf von [`notifyTaskCreated()` beinhaltet, der eine Nachricht sendet](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) an [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547), und [die Bearbeitung dieser Nachricht](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) versucht, [die von uns blockierte Sperre von einem anderen Thread zu erlangen](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f). Bis zur Freigabe von `ActivityTaskManagerService.mGlobalLock` bleibt der `DisplayThread`-Thread also blockiert. Später beinhaltet das Verfahren zum Starten einer `Activity` das [Senden einer Nachricht an denselben Thread, um den App-Prozess zu starten](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c). All das bedeutet, dass in diesem Fall der App-Prozess erst gestartet wird, wenn wir die Sperre freigeben, und der Grund, warum wir die Sperre überhaupt hielten, war, die `attachApplication()`-Transaktion am Abschluss zu hindern, um Handle daraus zu extrahieren. In diesem Fall würde diese Transaktion jedoch gar nicht erst starten.
Selbst wenn wir eine `Activity` starten, die Teil derselben `Task` wie die aktuelle ist (d.h., wir starten eine andere `Activity` aus der Settings-App, eine, die kein `android:launchMode="singleTask"` angibt), beinhaltet dieses Verfahren trotzdem [`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), was hier die gleiche Auswirkung wie `notifyTaskCreated()` hat.
Während mein Thread also in Methoden aufrufen könnte, die `synchronized (ActivityTaskManagerService.mGlobalLock) {}` verwenden, beinhaltete das Starten eines neuen App-Prozesses nach `startActivity()` die Nutzung dieser Sperre von einem anderen Thread, und das war in diesem Fall nicht nützlich, daher habe ich mich dafür entschieden, den Start des App-Prozesses stattdessen über einen `ContentProvider` auszulösen.
## Zusätzliche Anmerkung: Andere Möglichkeiten, `IApplicationThread` zu verwenden
`IApplicationThread` ist ein sehr privilegiertes Handle, daher betrachte ich die Nutzung nach dessen Erlangung als Post-Exploitation.
In diesem Exploit habe ich es direkt verwendet, um die Codeausführung im Zielprozess anzufordern, wobei ich ausgenutzt habe, dass der Zugriff auf diese Operation durch die Fähigkeit (Besitz des `Binder`-Objekts, das wir hier geleakt haben) und nicht durch `Binder.getCallingUid()` geschützt ist.
Das Hinzufügen einer `Binder.getCallingUid()`-Prüfung in [`ApplicationThread.scheduleReceiver()` (die wir hier zur Anforderung der Codeausführung verwendet haben)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) und anderen Methoden von `ApplicationThread` (da `scheduleReceiver()` nicht die einzige Methode in `IApplicationThread` ist, die das Laden von Code erlaubt) würde dennoch nicht verhindern, dass `IApplicationThread` zum Laden von Code in den Prozess einer anderen App verwendet wird, da ein Angreifer das geleakte `IApplicationThread` anstelle des eigenen an `attachApplication()` übergeben könnte.
Neben dem Laden von Code in einen Prozess ermöglicht der Besitz von `IApplicationThread` das Durchführen von [`grantUriPermission()` unter Verwendung der Privilegien des Prozesses, zu dem dieses Handle gehört](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 ruft recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) auf, um die Key-Value-Map-Inhalte zu lesen. Schlüssel sind Strings und Werte werden mit readLazyValue() gelesen, wobei LazyValue-Objekte für Werttypen erstellt werden, die mit einem Längenpräfix geschrieben werden. readArrayMap() gibt einen Wert zurück, der angibt, ob es in Ordnung ist, das Parcel zu recyceln. Wenn LazyValue-Objekte vorhanden waren, wird recycleParcel auf false gesetzt und das Parcel, auf das sich die LazyValues beziehen, wird nicht recycelt (es gibt eine Ausnahme, die aber hier nicht relevant ist; ich werde sie im Abschnitt "Zusätzliche Anmerkung: Bundle.clear()" beschreiben).unparcel() abgeschlossen ist, wird mMap gesetzt (nicht null) und bildet String-Schlüssel entweder auf tatsächliche Werte ab (falls diese bereits bereit sind) oder auf LazyValue-Objekte.getValue() aufgerufen, das den Schlüssel (String) auf einen Index (int) abbildet und ihn an getValueAt() übergibt.LazyValue.apply() setzt das Parcel auf die Position von LazyValue.mPosition zurück und ruft das normale Parcel.readValue() auf, worüber ich bereits gesprochen habe.LazyValue in mMap ersetzt, so dass der nächste Bundle.get*()-Aufruf für denselben Schlüssel direkt den Wert zurückgibt und die LazyValue-Deserialisierung nicht wiederholt wird. Wenn das Bundle weitergeleitet wird, wird dieser Wert erneut serialisiert, anstatt die Originaldaten wortwörtlich zu kopieren (allerdings wird dieser Wert nach dem Lesen des weitergeleiteten Bundle wieder ein LazyValue sein, und eventuelle writeToParcel/createFromParcel-Inkonsistenzen können andere Werte nicht beeinträchtigen).ParcelmaybeWriteSquashed()trueBundleBundle1Parcel.writeParcelable()Parcelable.writeToParcelBinder-Transaktionsgröße nähern, wird 0 geschrieben, um anzuzeigen, dass es keine weiteren Elemente in dieser Transaktion gibt und die nächsten Elemente in einer anderen Transaktion gesendet werden.ParcelableListBinder die Anzahl der Elemente erhalten hat, die in der ersten Transaktion angegeben wurde, ruft es das an seinen Konstruktor übergebene Lambda auf, das in diesem Fall die abgerufene Liste MediaSessionRecord.mQueue zuweist.BinderParceledListSlice aus Parcel gelesen wird, liest es den ersten Teil direkt aus Parcel und ruft dann, wenn nicht alle Elemente inline geschrieben wurden, den Binder auf, der in das Parcel geschrieben wurde, um diese Elemente abzurufen.BundleParcelParcelBinderParcel.recycle()RemoteViewsParcelmakeOwnedLeakermakeHolderLeakerRemoteViews.readActionsFromParcel()