
Writeup und Exploit für CVE-2024-49746: Androids Parcel::continueWrite schließt Dateideskriptoren, die später verwendet werden
Der Fix für dieses Problem erschien als CVE-2024-49746: Bulletin, Patch
Der obige Titel ist der Kommentar aus der Methode Parcel::continueWrite, die tatsächlich für die Größenänderung von Parcel-Objekten verantwortlich ist – entweder wenn sie explizit vom Benutzer angefordert wird (zum Beispiel über setDataSize()) oder wenn eine der write-Methoden aufgerufen wird, während die aktuelle Datenkapazität zu klein ist```cpp
status_t Parcel::continueWrite(size_t desired)
{
// SNIP: Validate desired size
// SNIP: Assign kernelFields & rpcFields from variant member of this class
// SNIP: Count number of objects (Binder handles and File Descriptors)
// that will be present after resize and assign to objectsSize
if (mOwner) {
// If the size is going to zero, just release the owner's data.
if (desired == 0) {
freeData();
return NO_ERROR;
}
// If there is a different owner, we need to take
// posession.
uint8_t* data = (uint8_t*)malloc(desired);
// SNIP: Check if malloc succeeded
binder_size_t* objects = nullptr;
if (kernelFields && objectsSize) {
objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
// SNIP: Check if calloc succeeded
// Little hack to only acquire references on objects
// we will be keeping.
size_t oldObjectsSize = kernelFields->mObjectsSize;
kernelFields->mObjectsSize = objectsSize;
acquireObjects();
kernelFields->mObjectsSize = oldObjectsSize;
}
// SNIP: rpcFields handling for non-/dev/binder Parcels
if (mData) {
memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
}
if (objects && kernelFields && kernelFields->mObjects) {
memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
}
// ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
if (kernelFields) {
// TODO(b/239222407): This seems wrong. We should only free FDs when
// they are in a truncated section of the parcel.
closeFileDescriptors();
}
mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
kernelFields ? kernelFields->mObjectsSize : 0);
mOwner = nullptr;
// SNIP: Allocation count tracking
// SNIP: Assign data and objects to this object
} else if (mData) {
// SNIP: Resize data owned by this instance of Parcel
} else {
// SNIP: Allocate initial data for currently empty Parcel
}
return NO_ERROR;
}
[Als dieser Kommentar eingeführt wurde](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/), wurde der Aufruf von `closeFileDescriptors()` aus der Methode `IPCThreadState::freeBuffer()` (die im obigen Code über den Funktionszeiger `mOwner()` aufgerufen wird) in die Methode `continueWrite()` verschoben, die Logik blieb jedoch dieselbe wie zuvor. Schließlich ist `Parcel` ein zentraler Bestandteil des Android-IPC, und wenn das Kern-IPC Dateideskriptoren schließen würde, die es nicht schließen sollte, wäre das ein offensichtliches Problem.
Was uns zum wichtigen Punkt bringt: Wann wird der obige Code verwendet? Er wird verwendet, wenn die Klasse `Parcel` den Besitz an Daten übernimmt, die vom Binder-Treiber empfangen wurden (die sich zu diesem Zeitpunkt im [`/dev/binder`-`mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) befinden und nicht beschrieben werden können (jeder Versuch, diesen Speicher zu beschreiben, würde zu `SIGSEGV` führen)), d. h. wenn `Parcel` entweder eingehende Transaktionsdaten ist (`data`-Argument, das an [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) übergeben wird) oder eine eingehende Antwort (d. h. das `Parcel`-Objekt, das an den [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))-Aufruf als `reply`-Argument übergeben wurde; `transact()` setzt eine Referenz innerhalb dieses `Parcel`-Objekts).
In der Praxis ist der einzige Fall, in dem wir den Block `if (mOwner)` betreten, wenn das System [`setDataSize(0)` aufruft, um Transaktionsdaten freizugeben](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567), aber in diesem Fall würden wir auch `if (desired == 0)` betreten, was eine vorzeitige Rückkehr auslöst. Bei legitimer Systemnutzung gibt es allerdings keinen Fall, in dem wir den Pfad „Wenn es einen anderen Besitzer gibt, müssen wir Besitz ergreifen" betreten.
# Den Pfad „Besitz ergreifen" auslösen
In einem meiner früheren Exploits habe ich [einen Fall gezeigt, in dem `createFromParcel()` tatsächlich `writeInt(0)` auf dem `Parcel` aufrufen kann, aus dem es lesen sollte](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects). Während der Fix dort die Ausführung aller Nicht-`Intent`-`createFromParcel()`-Methoden innerhalb des `AccountManagerService` verhinderte, blieb der Pfad von `createFromParcel()` zu `writeInt(0)` intakt.
Zur Erinnerung: [im `PackageParser` haben wir den folgenden Code](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N);
for (int i = 0; i < N; ++i) {
intentsList.add(cons.newInstance(in));
}
Daher können wir ein Parcel-Objekt, das an createFromParcel übergeben wurde, an jeden im System verfügbaren public-Konstruktor übergeben, der ein einzelnes Parcel-Argument akzeptiert.
Und anderswo haben wir folgenden Code:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }
Daher müssen wir, um den „take possession“-Pfad auszulösen, über einen beliebigen `readParcelable`-Aufruf auf dem `Parcel` verfügen, das als `data` an `onTransact()` übergeben wurde. In diesem Exploit verwende ich dafür [denselben Pfad, den ich zuvor in einem anderen verwendet habe](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them). Ich veranlasse, dass `readParcelable` `PackageParser$Activity.CREATOR.createFromParcel()` aufruft, das wiederum den Namen von `PooledStringWriter` liest und dessen Konstruktor aufruft; danach enden die `Parcel`-Daten, sodass `writeInt()` das Parcel neu allozieren muss und damit in unseren „take possession“-Pfad eintritt.