
Analyse détaillée et exploitation pour CVE-2024-49746 : Parcel::continueWrite d'Android ferme des descripteurs de fichiers qui sont ensuite utilisés
Le correctif pour ce problème est apparu sous le nom CVE-2024-49746 : bulletin, correctif
Le titre ci-dessus est le commentaire de la méthode Parcel::continueWrite, qui est en réalité responsable du redimensionnement des objets Parcel, soit lorsqu'il est explicitement demandé par l'utilisateur (par exemple via setDataSize()) ou lors de l'appel à l'une des méthodes write lorsque la capacité actuelle des données est trop faible```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;
}
[Lorsque ce commentaire a été introduit](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/), l'appel à `closeFileDescriptors()` a été déplacé de `IPCThreadState::freeBuffer()` (qui est appelé dans le code ci-dessus via le pointeur de fonction `mOwner()`) vers la méthode `continueWrite()`, mais la logique est restée la même. Après tout, `Parcel` est une partie centrale de l'IPC Android et si l'IPC central fermait des descripteurs de fichiers qu'il ne devrait pas, ce serait un problème évident
Ce qui nous amène à la partie importante : quand le code ci-dessus est-il utilisé ? Il est utilisé lorsque la classe Parcel transfère la propriété des données reçues du pilote Binder (qui se trouvent à ce moment-là dans le [`mmap` de `/dev/binder`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) et ne peuvent pas être écrites (toute tentative d'écriture de cette mémoire entraînerait un `SIGSEGV`)), c'est-à-dire que `Parcel` est soit des données de transaction entrantes (l'argument `data` passé à [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))) soit une réponse entrante (c'est-à-dire l'objet `Parcel` qui a été passé à l'appel [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) comme argument `reply`, `transact()` définit une référence dans cet objet `Parcel`)
En pratique, le seul cas où l'on entre dans le bloc `if (mOwner)` est lorsque le système [appelle `setDataSize(0)` pour libérer les données de transaction](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567), mais dans ce cas, on entre aussi dans `if (desired == 0)` qui effectue un retour prématuré. Lors d'une utilisation légitime du système, il n'y a aucun cas où l'on emprunterait le chemin « S'il y a un propriétaire différent, nous devons prendre possession ».
# Déclencher le chemin « prendre possession »
Dans l'un de mes exploits précédents, j'ai montré [un cas où `createFromParcel()` peut en fait appeler `writeInt(0)` sur un `Parcel` qu'il est censé lire](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects). Bien que le correctif ait empêché l'exécution de toute méthode `createFromParcel()` non-`Intent` dans `AccountManagerService`, le chemin de `createFromParcel()` vers `writeInt(0)` a été conservé intact.
Pour récapituler, [dans `PackageParser`, nous avons le code suivant](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));
}
Par conséquent, nous pouvons avoir un objet Parcel qui a été passé à createFromParcel passé à tout constructeur public disponible dans le système qui accepte un seul argument Parcel
Et ailleurs nous avons le code suivant:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }
Par conséquent, pour déclencher le chemin « take possession », nous devons avoir un appel arbitraire à `readParcelable` sur le `Parcel` qui a été passé à `onTransact()` en tant que `data`. Dans cet exploit, j'utilise pour cela le [même chemin que j'ai précédemment utilisé dans un autre](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them). J'ai l'appel `readParcelable` à `PackageParser$Activity.CREATOR.createFromParcel()`, qui à son tour lit le nom de `PooledStringWriter` et appelle son constructeur, et après cela les données du `Parcel` se terminent, donc `writeInt()` doit réallouer le Parcel, entrant ainsi dans notre chemin « take possession »
À noter ici, s'il n'y avait pas de fin des données `Parcel` à ce moment-là, `writeInt()` tenterait de réécrire les données sur place, ce qui, dans le cas de données soutenues par `/dev/binder` `mmap`, mènerait à un `SIGSEGV`
# File Descriptor Sanitizer