
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
Mon idée initiale était d'avoir le chemin « take possession » fermer les descripteurs de fichier, après quoi à la fin de la transaction les mêmes descripteurs seraient fermés à nouveau, mais entre ces événements, je placerais un autre descripteur de fichier dans `system_server` dans une autre transaction et à un moment ultérieur récupérerais mon descripteur de fichier, car ce FD se réfère à ce moment-là à un fichier différent
Cela a fonctionné sur mon émulateur utilisant une ancienne version AOSP, mais lorsque j'ai essayé avec une version plus récente, ce plan a été stoppé par [File Descriptor Sanitizer (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md)
En particulier, [dans `android-14.0.0_r29`, la couverture de FDSan a été étendue pour couvrir les FD dans `Parcel`](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)
En fait, après que FDSan a couvert Parcel, je n'ai même pas pu atteindre l'appel `closeFileDescriptors()` lorsque le `Parcel` contenait des FD. Avant cet appel, il y a un appel à `acquireObjects();`, qui acquiert des références aux handles `Binder` (qui sont libérés par l'appel `mOwner()` plus tard dans cette fonction), cependant `acquireObjects()` va également [définir des tags FDSan pour les FD](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe):```cpp
case BINDER_TYPE_FD:
if (obj.cookie != 0) { // owned
FdTag(obj.handle, nullptr, who);
}
Le fait est que nous avons déjà tagué les FDs reçus du noyau```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }
Ainsi, double-`FdTag` (sans fermer ni changer le tag tout en spécifiant l'ancien tag attendu) conduit à une erreur FDSan, qui interrompt le processus, et nous n'avons même pas encore atteint l'appel à `closeFileDescriptors()`. Puisque le chemin "prendre possession" est du code mort en usage normal, de tels problèmes pourraient passer inaperçus
Cependant, nous pouvons voir la condition `if (obj.cookie != 0)`. Si elle est fausse, cela signifie que le FD présent dans `Parcel` n'est pas réellement possédé par ce `Parcel` et qu'il est de la responsabilité de l'utilisateur de `Parcel` de les garder ouverts tant que ce `Parcel` existe. Cependant, comme ce `Parcel` vient juste du noyau, les valeurs de `cookie` proviennent en réalité du processus d'origine et sont considérées comme non pertinentes lorsque `Parcel` a effectivement `mOwner`. Mais le chemin "prendre possession" ne tient pas compte de cela et se contente de copier les valeurs de `cookie`
En combinant tout cela, en mettant les valeurs de `cookie` à zéro du côté de l'expéditeur, nous pouvons obtenir un `Parcel` qui référence des descripteurs de fichier qui ont été fermés, mais qui ne les considère pas comme les possédant, ce qui signifie qu'il ne les fermera pas à nouveau. Cela nous permet d'éviter de déclencher FDSan, mais cela élimine également toute voie d'exploitation par double-fermeture.
# Astuces côté Java de Parcel
Ces FD pourraient encore être passés à un autre `Parcel` (et ensuite à un autre processus), mais notre façon de déclencher la création de tels FD pendants implique la construction de `PooledStringWriter` par réflexion, après quoi une `ClassCastException` est levée lorsque nous essayons de l'`add()` à `ArrayList<IntentInfo>`
Nous aurions besoin de :
* Être à la fin du `Parcel` qui a été passé comme argument `data` à `onTransact()`
* Effectuer la construction de `PooledStringWriter`, après quoi le Parcel aura des FD pendants, mais aussi une `ClassCastException` est levée
* Attendre que les FD que nous voulons fuir soient alloués dans `system_server`
* Faire en sorte que les FD de ce `Parcel` soient copiés vers un autre `Parcel` qui serait envoyé à notre processus
Afin de remplir toutes ces exigences, je vais devoir utiliser à la fois des astuces anciennes et nouvelles.
## Astuces anciennes
Commençons par revisiter les astuces anciennes, dont la plupart ont déjà été décrites dans mon exploit [`LazyValue`-using-`Parcel`-after-`recycle()`](https://github.com/michalbednarski/LeakValue)
1. La classe `RemoteViews` effectue la désérialisation du `Bundle` contenu avec `Parcel.ReadWriteHelper` défini](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4). [Lorsque `ReadWriteHelper` est défini, les `Bundle` ne sont pas désérialisés de manière immédiate mais paresseusement](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545). Il est également bon de noter à ce stade que tous les `Bundle` contenus dans ce `Bundle` sous `RemoteViews` seront également désérialisés de manière immédiate, pas seulement celui qui est directement à l'intérieur de `RemoteViews`.
2. [Si une `BadParcelableException` est levée lors de la désérialisation d'un `Bundle` dans `system_server`, cette `BadParcelableException` sera silencieusement attrapée et le contenu du `Bundle` sera effacé](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e). Notez cependant que notre déclencheur d'écriture dans `createFromParcel` lance une `ClassCastException` qui ne sera pas attrapée ici.
3. [La classe `ParceledListSlice` lors de la désérialisation effectue un appel Binder sortant bloquant vers l'objet spécifié dans les données sérialisées](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364), que nous pouvons utiliser pour bloquer l'exécution de la désérialisation.
Tous ces éléments seront nécessaires maintenant, mais ce n'est pas tout.
## Nouvelles astuces
[AIDL est un outil pour générer une implémentation d'interface RPC](https://developer.android.com/guide/components/aidl), cependant en plus de cela, il est capable de générer des implémentations de structure `Parcelable`.
Ces structures sont préfixées par leur longueur, donc différentes versions de la même structure sont compatibles dans le système tant qu'aucun champ n'est ajouté au milieu (c'est-à-dire que les versions sont compatibles si une version est un préfixe de l'autre).
Regardons ce que le code AIDL a généré pour la structure [`ReceiverInfo`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl) :```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
int _aidl_start_pos = _aidl_parcel.dataPosition();
int _aidl_parcelable_size = _aidl_parcel.readInt();
try {
if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
data = _aidl_parcel.readString();
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
// SNIP: Other fields
} finally {
if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
throw new android.os.BadParcelableException("Overflow in the size of parcelable");
}
_aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
}
}
Ce code nous permettra de réaliser les deux dernières astuces, toutes deux nécessaires pour effectuer d’autres actions après avoir déclenché le chemin « prendre possession », qui nous oblige à être à la fin de Parcel et lance une ClassCastException
D’abord, nous lisons la longueur depuis Parcel, l’utilisons pour ignorer les champs absents dans la version écrite, puis à la fin nous ajustons la position dans Parcel en fonction de cette longueur. Il ne nous est pas permis de nous déplacer avant la position de ce Parcelable AIDL (se déplacer après la fin de Parcel est possible, mais cela n’a rien de dangereux). Cependant, nous pouvons nous déplacer au milieu d’un objet déjà lu, donc lors de la première lecture nous pourrions atteindre la fin de Parcel, faire construire PooledStringWriter, puis revenir en arrière au milieu de quelque chose qui se trouve à l’intérieur de cet objet ReceiverInfo
Cela règle le problème « être à la fin de Parcel », il reste le problème de ClassCastException. Mais nous avons un bloc finally qui, avant d’appeler setDataPosition(), vérifie s’il n’y a pas de débordement. En cas de débordement, une BadParcelableException sera levée. Que se passe-t-il si le bloc finally lève une Exception alors qu’une autre est en attente ? L’exception levée dans finally prend le dessus, l’exception précédente disparaît silencieusement. Maintenant, au lieu d’une ClassCastException nous avons une BadParcelableException, que Bundle ignore volontairement pour éviter les Exception dans system_server
ParcelableListBinderNous utilisons ParcelableListBinder de MediaSession pour placer un Parcelable arbitraire dans system_server et récupérer plus tard cet objet
Récemment, il y a eu un correctif de ParcelableListBinder qui interdit exactement cela
Du point de vue de cette exploitation, ce correctif ne m’arrête pas vraiment, mais je dois gérer sa présence ou son absence différemment
Si ce correctif est présent, les éléments non-QueueItem sont silencieusement supprimés de la liste reçue par system_server (ils ne provoquent pas d’Exception). Comme nous avons besoin d’un élément non-QueueItem pour les effets secondaires, nous pouvons placer un élément non-QueueItem en premier, puis le faire suivre d’un vrai QueueItem, ce qui ne nous permet pas d’effectuer une désérialisation arbitraire de Parcelable, mais contient un Bundle qui peut contenir des descripteurs de fichiers (que nous souhaitons transmettre à notre processus)
Si ce correctif est absent, nous n’avons pas cet obstacle, mais nous ne pouvons pas non plus utiliser le même flux. Car dans le cas précédent, nous aurions une liste contenant à la fois RemoteViews et QueueItem, donc ParceledListSlice refuserait le transfert d’une liste de types mixtes. Dans ce cas, je dois placer nos descripteurs de fichiers divulgués à l’intérieur de l’instance RemoteViews
Ce qui est plus intéressant à propos de ce correctif, c’est la raison pour laquelle il a été mis en place. Le message de validation mentionne « permettre aux applications de démarrer depuis l’arrière-plan » et bien que le bulletin de sécurité Android n’en dise pas beaucoup, nous pouvons trouver des informations utiles dans l’entrée CVE, qui fait référence au champ Notification.mAllowlistToken, qui pendant la lecture peut être pris depuis un champ statique, qui à l’intérieur de system_server est un jeton autorisant le démarrage d’activités en arrière-plan et plus tard ce jeton serait écrit dans Notification.writeToParcel(). Cela signifie-t-il que désormais tous les cas où system_server désérialise un Parcelable arbitraire et le renvoie à l’application sont des vulnérabilités ? Quoi qu’il en soit, pour l’instant ce n’est qu’une pensée, dans cette exploitation je veux faire plus de toute façon
Je pense que cette exploitation présente la chaîne de gadgets Parcelable la plus complexe que j’aie jamais faite :
RemoteViews (1)
ReflectionAction (2)
Bundle
Parcelable[] (3)
ReceiverInfo (pour seek, 4 et 10)
Intent
ComponentName (segment A, 5)
ParceledListSlice (11)ParcelableParcel ou QueueItem (12)Bundle (pour catch, segment B, 6)
ReceiverInfo (pour rethrow, 7)
Bundle (8)
Les annotations « segment A » et « B » dans la liste ci-dessus font référence aux blocs entre les commentaires « START A »/« END A »/« START B »/« END B » dans ma classe FdLeaker.java, les nombres renvoient aux points de la liste ci-dessous
L’arbre ci-dessus décrit la hiérarchie du point de vue de l’expéditeur, du point de vue du destinataire cela semble un peu différent cependant :
ParcelableListBinder, l’objet le plus externe est RemoteViewsRemoteViews se trouve un Bundle imbriqué. RemoteViews va définir Parcel.ReadWriteHelper donc ce Bundle et tous les Bundle qu’il contient seront lus avec empressement. C’est nécessaire car sinon nous ne pourrions pas exécuter readParcelable arbitraire depuis ReceiverInfo.readFromParcel()Parcelable[] est simplement un emballage pratique ici pour regrouper tous les Parcelable que je place à l’intérieur de Regardons l’attaque ci-dessus d’un point de vue général :
system_server, une référence est conservée et le FD est fermésystem_server pour ouvrir un descripteur de fichier différentIl y a une limitation importante de cette attaque : nous ne pouvons pas saisir les descripteurs de fichiers qui ont été ouverts avant le début de notre attaque
Il y a cependant encore quelques choses utiles que nous pourrions faire
InputChannelPour être honnête, c’est la seule variante d’exploitation que j’ai réussi à faire fonctionner sans hypothèses supplémentaires
Les événements d’entrée, c’est-à-dire les événements de l’écran tactile et du clavier, sont reçus par l’application via une socket UNIX depuis system_server. Lorsqu’une Activity démarre ou ajoute une nouvelle fenêtre au système, un nouveau InputChannel est créé, ce qui signifie qu’une paire de sockets UNIX est créée et l’une des extrémités est envoyée à l’application tandis que l’autre est utilisée par system_server pour envoyer les événements
La disposition des structures envoyées sur ces sockets est bien définie (car elles doivent être compatibles entre processus 32 bits et 64 bits) et il semble que rien ne soit perturbé par des numéros de séquence inattendus. De plus, InputChannel semble être la seule socket allouée à l’intérieur de system_server après l’appel startActivity(), donc il peut être facilement déterminé quel FD est la socket côté serveur de InputChannel

Bien que ce soit un exemple jouet, nous pourrions également approuver des invites d’autorisation ou l’installation d’applications, activer la Projection multimédia ou le Service d’accessibilité
zygote pendant le démarrageCelle-ci est surtout théorique, j’ai réussi à effectuer cette attaque sur un émulateur lent, mais sur un vrai appareil la fenêtre de course était trop petite
system_server ouvre une connexion à /dev/socket/zygote une seule fois au démarrage, après quoi toutes les requêtes sont envoyées via cette connexion
Pendant le démarrage de system_server, MediaSessionService (qui est utilisé pour envoyer et recevoir des Parcelable vers/depuis system_server) est publié dans servicemanager avant que la connexion à zygote ne soit établie
Donc, théoriquement, il est possible pour une application de lancer un processus secondaire, de faire planter system_server, puis à partir de ce processus en arrière-plan d’effectuer l’attaque pendant le démarrage de system_server
zygote via SensorServiceIl y a donc un autre bug que j’ai trouvé, voici la méthode SensorService::createSensorDirectConnection()```cpp
sp SensorService::createSensorDirectConnection(
const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format,
const native_handle *resource) {
// SNIP: Reject direct connections when sensor privacy is enabled
// SNIP: Irrelevant parameter checks
// check specific to memory type
switch(type) {
case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
if (resource->numFds < 1) {
ALOGE("Ashmem direct channel requires a memory region to be supplied");
android_errorWriteLog(0x534e4554, "70986337"); // SafetyNet
return nullptr;
}
// SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
}
case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
// no specific checks for gralloc
break;
default:
ALOGE("Unknown direct connection memory type %d", type);
return nullptr;
}
native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
return nullptr;
}
native_handle_set_fdsan_tag(clone);
sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
// SNIP: usual case where sensor belong to this device (not app streaming)
} else {
auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
} else {
int fd = dup(clone->data[0]);
channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
}
}
// SNIP: Return connection
}
Nous avons l'appel `dup(clone->data[0])`. `clone` est un `native_handle_t` reçu depuis un processus distant. Le native handle contient un certain nombre de FD et un certain nombre d'entiers simples dans `data` ; l'utilisateur du native handle doit vérifier les quantités respectives en consultant [`numFds` et `numInts`](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e) avant d'accéder à `data`.
Ici, nous avons même une section « vérification spécifique au type de mémoire » qui vérifie que, pour `SENSOR_DIRECT_MEM_TYPE_ASHMEM`, cela est vérifié ici ; pour `SENSOR_DIRECT_MEM_TYPE_GRALLOC`, le format du native handle est spécifique à l'appareil et ne peut pas être validé ici. Le fait est que pour les « capteurs d'exécution », le type est toujours traité comme `SENSOR_DIRECT_MEM_TYPE_ASHMEM`, mais nous pouvons spécifier `SENSOR_DIRECT_MEM_TYPE_GRALLOC` pour contourner la validation dans ce cas.
Cependant, ce code n'est accessible que si les « capteurs d'exécution » sont présents. Je ne suis pas sûr dans quel cas cela se produit réellement ; je pense que c'est lorsque l'utilisateur utilise « streaming d'applications à proximité » (Nearby app streaming) (?)
Pour les tests, j'ajoute néanmoins une petite classe qui permet l'enregistrement de [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary), vous pouvez l'utiliser via```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'
Après cela, il est possible d'exécuter du code en tant qu'uid system sur un appareil de production

PackageParser$Activity (9)
PooledStringWriterRemoteViewsReceiverInfo le plus externe a une longueur définie qui tombe au milieu de ses données, mais lors de la lecture cela n’est pas encore connu et Intent est lu normalement à partir de celui-cireadString fait par static ComponentName.readFromParcel() (en utilisant ComponentName car il utilise readString en UTF-16 quelle que soit la version Android, ce readString retournera null car cette chaîne chevauche des objets Binder, ainsi, bien que la construction de ComponentName ait sauté les données cachées de ce passage, l’objet ComponentName n’est pas crééBundle imbriqué, à la fin de la désérialisation de ce Bundle une BadParcelableException sera attrapéeReceiverInfo. Ce ReceiverInfo a une longueur spécifiée comme Integer.MAX_VALUE et donc lèvera une BadParcelableException dans le bloc finally, écartant silencieusement la ClassCastExceptionBundle. C’est juste pour pouvoir atteindre un readParcelable arbitraire depuis ReceiverInfo, ce que nous pouvons maintenant faire car nous sommes à l’intérieur de RemoteViews donc Bundle est lu avec empressementPackageParser$Activity + PooledStringWriter déclenche un appel writeInt(0) sur le Parcel en cours de lecture. Comme nous étions à la fin de Parcel, writeInt() doit étendre la capacité de Parcel, déclenchant le chemin « prendre possession ». Cette combinaison mène également à une ClassCastException, qui est avalée comme décrit aux étapes 7 et 6 ci-dessusReceiverInfo externe, ReceiverInfo se positionne selon la longueur de son en-tête et tombe dans les données qui étaient auparavant à l’intérieur de ComponentName, qui sont lues comme les éléments suivants dans Parcelable[]ParceledListSlice qui effectue une transaction Binder bloquante vers mon processus. À ce stade, les descripteurs de fichiers définis dans ce Parcel ont été fermés, mais rien d’intéressant n’a encore pris leur place. Pendant que cette désérialisation attend le retour de cet appel, je peux faire ouvrir par le système quelques descripteurs de fichiers intéressants qui me seront ensuite envoyésParcelableListBinder filtre les éléments ou non :
a. Si ParcelableListBinder ne filtre pas les éléments, les FD sont sauvegardés à l’intérieur de ParcelableParcel, qui, comme Bundle, copie les données Parcel textuellement avec Parcel.appendFrom(), mais n’a pas la logique spéciale hasReadWriteHelper() donc il le fait malgré être sous le Bundle de RemoteViews
b. Si ParcelableListBinder filtre les éléments, c’est la fin de RemoteViews, l’objet RemoteViews est rejeté par ParcelableListBinder, mais cela me convient car les effets secondaires ont déjà eu lieu. L’élément suivant reçu par ParcelableListBinder est QueueItem, qui contient MediaDescription, qui à son tour contient Bundle, c’est là que mes FD divulgués sont conservés