
Exploit pour CVE-2022-20452, élévation de privilèges sur Android depuis une application installée vers une application système (ou une autre application) via LazyValue utilisant Parcel après recycle()
Android 13 introduit de nombreuses améliorations afin de renforcer le mécanisme de sérialisation Parcel
Voici la présentation de l'équipe Android Security and Privacy sur les améliorations apportées
C'est excellent, cela élimine ou rend inexploitables de nombreuses vulnérabilités. Ils décrivent également comment casser mon précédent exploit, qui permettait aux applications de charger leur code dans d'autres applications (y compris celles du système)
Mais je reviens avec un nouvel exploit qui atteint le même résultat, bien que d'une manière différente. Il repose sur les vulnérabilités suivantes qui ont été introduites lors du durcissement susmentionné de Parcel:
![Capture d'écran de l'application affichant du texte. Titre: LeakValue. Texte principal: Créé 6 ValueLeaker-s. Verrouillage d'ActivityTaskManagerService. ActivityTaskManagerService verrouillé. Déverrouillage d'ActivityTaskManagerService. ActivityTaskManagerService déverrouillé. leakedBinders=[android.os.BinderProxy@f06702e]. Interface divulguée: android.app.IApplicationThread. Demande d'exécution de code. Le shellcode a été exécuté dans 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. En bas de l'écran se trouvent deux boutons: START et MANUAL TESTING](Screenshot_20220723-081920.png)
(Également logcat de l'exécution de l'application, l'exploitation est bruyante dans les journaux)
Parcel et ParcelableLa classe Parcel d'Android est la base de la communication entre les processus
Les objets peuvent implémenter l'interface Parcelable afin de pouvoir les écrire dans un Parcel, par exemple (copié depuis 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());
} }
Notez que `Parcel` stocke en interne la position à laquelle l'écriture ou la lecture est effectuée ; `readString()` analyse les données en une chaîne et avance également la position. Cette position peut être obtenue/définie manuellement via [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)). Les implémentations de l'interface `Parcelable` doivent garantir que leurs méthodes `writeToParcel` et `createFromParcel` écrivent/lisent la même quantité de données, sinon toutes les lectures suivantes obtiendront des données à partir de décalages incorrects
[`Bundle`](https://developer.android.com/reference/android/os/Bundle) (une carte clé-valeur pouvant être envoyée entre processus) peut contenir [une variété d'objets pouvant être écrits dans `Parcel` via `writeValue()`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937). Lorsque le contenu d'un `Bundle` est lu depuis `Parcel`, toute classe `Parcelable` disponible dans le système peut être lue
`Bundle` diffère l'analyse réelle de son contenu : il écrit dans `Parcel` la longueur de l'intégralité des données sérialisées, puis [copie simplement la partie correspondante de la `Parcel` d'origine vers une `Parcel` secondaire stockée dans `mParcelledData`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683) (cela permet par exemple à [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)) de fournir des `Parcelable` qui ne sont pas disponibles dans `system_server` ; le `Bundle` entier est ensuite transmis à `system_server` puis renvoyé tel quel sans analyse du contenu)
Cependant, dès qu'une valeur du `Bundle` était accédée, toutes les valeurs du `Bundle` étaient [désérialisées](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) et [chaque paire clé-valeur présente était analysée](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). Si une telle carte contenait un `Parcelable` dont les méthodes `writeToParcel` et `createFromParcel` étaient déséquilibrées, et que ce `Bundle` était ensuite transmis à un autre processus, cet autre processus pouvait voir un contenu différent du `Bundle`. Cela a transformé tous ces [écarts dans les classes disponibles dans le système en vulnérabilités](https://github.com/michalbednarski/ReparcelBug), car il existe des [endroits dans le système où le `Bundle` est inspecté pour être sûr](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) puis transmis à un autre processus
Dans cet article, j'appelle un tel `Bundle`, qui présente un contenu puis un autre après avoir été transmis, un `Bundle` auto-modifiable
Un autre point important ici est qu'en plus de simples octets (chaînes, nombres, objets composés de ce qui précède), `Parcel` peut également contenir des descripteurs de fichiers et des `Binder`. Les `Binder` sont des objets sur lesquels on peut effectuer un appel RPC : un processus crée un objet `Binder` et redéfinit la [méthode `onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Ensuite, le `Binder` est transmis à un autre processus ; dans l'exemple de code ci-dessus, vous pouvez voir les appels `read`/`writeStrongBinder()` utilisés pour le lire et l'écrire dans `Parcel`. Dans l'autre processus, lorsque `readStrongBinder()` est utilisé, un objet `BinderProxy` est créé (caché derrière l'[interface `IBinder`](https://developer.android.com/reference/android/os/IBinder)). Ce processus peut alors appeler [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) sur cet objet et, dans l'objet d'origine, `onTransact()` sera exécuté. En général, on n'écrit pas manuellement `transact()`/`onTransact()`, mais on [utilise AIDL à la place](https://developer.android.com/guide/components/aidl)
# Voici `LazyValue`, la fin des `Bundle` auto-modifiables
Comme par le passé il y a eu de nombreux cas de classes avec une inadéquation entre `writeToParcel` et `createFromParcel`, Android 13 résout le problème de toute classe de ce type présente n'importe où dans le système qui permettrait la construction d'un `Bundle` auto-modifiable en [introduisant `LazyValue`](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/)
Désormais, lorsque `writeValue` est utilisé, si la valeur écrite n'est pas primitive, [la longueur de la valeur est également écrite dans `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804)
Lorsqu'une application normale utilise directement `Parcel.readValue()`, [tout se passe comme avant, à l'exception d'un avertissement affiché si la `length` lue depuis `Parcel` ne correspond pas à la taille des données réellement lues](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804) (Notez cependant que [`Slog.wtfStack` ne lève jamais d'exception](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577))
`Bundle`, en revanche, utilise désormais [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804) à la place
Examinons de plus près son fonctionnement : dans la classe `LazyValue`, on trouve [un bon commentaire expliquant la structure des données `LazyValue` dans `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4392-4399;drc=03c34f57c05feecfb090de3917787f049cb5f804):```
| 4B | 4B |
mSource = Parcel{... | type | length | object | ...}
a b c d
length = d - c
mPosition = a
mLength = d - a
mSource est une référence au Parcel original sur lequel readLazyValue() a été appelé
mPosition et mLength décrivent l'emplacement de l'intégralité des données LazyValue dans le Parcel original, y compris type et length
« length » (sans « m » au début) fait référence à la valeur de longueur telle qu'elle est écrite dans le Parcel et exclut l'en-tête (type et length)
Voici donc ce qui se passe lorsque quelqu'un (système ou application) prend une valeur d'un Bundle qui a été lu depuis un Parcel :
get*() de la classe Bundle, par exemple nouvelle getParcelable() avec argument de type (le flux sera le même pour les nouvelles et les anciennes méthodes, les nouvelles méthodes garantissent simplement que l'argument clazz n'est pas null alors que les anciennes le définissent à null)unparcel() est appelé, ce qui vérifie si ce Bundle possède mParcelledData (ce qui signifie qu'il a été lu depuis un Parcel mais qu'aucune valeur n'a encore été accédée et que les noms de clés n'ont pas encore été déballés ; si ce n'est pas le cas, passer à l'étape 5.)unparcel() délègue à , , est défini sur , une copie du que le a faite, et le paramètre est défini sur pour indiquer que le passé appartient au et qu'il est acceptable d'appeler dessusSi le Bundle est transmis alors qu'il contient encore des LazyValue (ce qui signifie que cette valeur particulière n'a pas été accédée, mais qu'une autre valeur de ce Bundle l'a été (c'est-à-dire que unparcel() a été appelé, mais LazyValue.apply() pour cet élément ne l'a pas été)) :
LazyValue est détecté par Parcel.writeValue() et l'écriture est déléguée à LazyValue.writeToParcel()LazyValue.writeToParcel() utilise out.appendFrom(source, mPosition, mLength) pour copier toutes les données LazyValue du Parcel original (encore une fois, mPosition et mLength incluent l'en-tête LazyValue, donc cela copie aussi type et length du original)Parcel.ReadWriteHelper et Parcel.readSquashed(Les détails de ces mécanismes ne sont pas importants pour cette exploitation ; la seule chose pertinente ici est que ces mécanismes existent)
Une autre caractéristique intéressante de Parcel est la possibilité optionnelle de dédupliquer les String et les objets écrits
La déduplication des String est effectuée en redéfinissant la classe Parcel.ReadWriteHelper : Parcel.readString() délègue en réalité à ReadWriteHelper et le helper par défaut lit directement une String depuis Parcel
Une implémentation alternative de Parcel.ReadWriteHelper peut remplacer les appels à readString par la lecture préalable d'un pool de String et l'utilisation de readInt pour obtenir les index des String dans le pool ; cela n'est cependant jamais fait avec des Parcel contrôlés par une application
Parcel offre néanmoins une méthode hasReadWriteHelper(), qui permet aux appelants de détecter la présence d'un tel mécanisme de déduplication actif et de désactiver les fonctionnalités incompatibles avec celui-ci
L'autre mécanisme de déduplication disponible dans Parcel est le squashing :
Parcel.allowSquashing()Parcel.maybeWriteSquashed(this). Si cette méthode renvoie true, cela signifie que l'objet a déjà été écrit dans ce Parcel et que seul l'offset vers les données de l'objet précédent a été écrit dans le Parcel. Sinon (soit le squashing n'est pas activé, soit c'est la première fois que cet objet est écrit), maybeWriteSquashed écrit zéro comme offset pour indiquer que l'objet n'est pas squashé et renvoie false pour indiquer à l'appelant qu'il doit écrire les données réelles de l'objetParcel.readSquashed est appelé et la fonction de lecture réelle lui est passée en tant que lambda. readSquashed vérifie si l'offset écrit par indique qu'une autre occurrence de l'objet a été lue plus tôt : si c'est le cas, l'objet précédemment lu est renvoyé, sinon la lambda fournie est appelée pour le lire maintenantParcel.recycle()Côté Java, les objets Parcel peuvent être recyclés dans un pool : une fois que vous avez fini avec un Parcel, vous appelez recycle() dessus et la prochaine fois que quelqu'un appelle Parcel.obtain(), il obtiendra un Parcel précédemment recyclé. Cela permet de réduire le nombre d'allocations d'objets et le Garbage Collection ultérieur
D'un autre côté, une telle gestion manuelle de la mémoire introduit en Java la possibilité de bugs de type Use-After-Free (bien qu'avec la sécurité des types, contrairement aux Use-After-Free habituels en C)
Comme indiqué ci-dessus, Bundle crée une copie du Parcel et n'appellera pas Parcel.recycle() si un LazyValue est présent ; ce n'est cependant pas le cas si Parcel.hasReadWriteHelper() est true. Dans ce cas :
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle); est appelé, ce qui signifie que Bundle ne recyclera pas le Parcel car il appartient toujours à l'appelant ; cela crée cependant des LazyValue qui se réfèrent au Parcel original et pourraient survivre à la durée de vie du Parcel originalunparcel(/* itemwise */ true), qui utilisera getValueAt() sur tous les éléments pour remplacer tous les LazyValue présents dans le Bundle par des valeurs réellesMaintenant, pouvons-nous faire survivre ces LazyValue à l'étape 2 et transformer ce comportement en Use-After-Recycle ?
Si la désérialisation échoue (par exemple, la classe dont le nom est spécifié dans le Parcel ne peut pas être trouvée), une BadParcelableException est levée puis attrapée par getValueAt(). Si le champ statique BaseBundle.sShouldDefuse est true, aucune exception n'est levée et l'exécution continue, laissant le Bundle contenir un LazyValue se référant au Parcel original. sShouldDefuse indique que les valeurs indisponibles d'un Bundle ne doivent pas provoquer d'exceptions dans un processus particulier et est défini à true dans
Si le Parcel original est recyclé et qu'ensuite le Bundle qui en a été lu est écrit dans un autre Parcel, le contenu du Parcel original sera copié vers le Parcel de destination, mais à ce moment-là, le Parcel original pourrait avoir été réutilisé pour autre chose et des données provenant d'une opération IPC sans rapport pourraient être copiées
D'accord, mais comment faire en sorte que Parcel.hasReadWriteHelper() soit true pendant que le Bundle que nous fournissons est désérialisé ?
Il s'avère que la classe RemoteViews (normalement utilisée par exemple pour transmettre des widgets à l'écran d'accueil) définit explicitement ReadWriteHelper lors de la lecture des Bundle imbriqués en elle. Ce ReadWriteHelper ne fait pas de déduplication des String et n'est présent que pour amener le Bundle à ignorer la copie des données vers un Parcel secondaire. La raison de cette approche est que RemoteViews active le squashing afin de dédupliquer les objets ApplicationInfo imbriqués, mais cela pourrait également provoquer le squashing des objets ApplicationInfo présents dans le Bundle ; la lecture de ce ne peut donc pas être différée, car ces objets squashés ne pourraient pas être désquashés
Parcelable dans system_server et les récupérerNous voulons donc maintenant que system_server lise notre RemoteViews contenant un Bundle contenant un LazyValue qui échoue à se désérialiser, puis plus tard (dans une autre transaction IPC Binder) nous renvoie cet objet
Cela pourrait probablement être fait par des moyens légitimes, comme nous enregistrer comme hôte de widgets d'application (mais cela nécessiterait une interaction de l'utilisateur pour nous accorder la permission) ou publier une Notification avec contentView définie (mais cela provoquerait une interaction avec d'autres processus et/ou serait visible pour l'utilisateur, et j'ai préféré éviter ces deux approches)
J'ai plutôt décidé de créer une MediaSession et d'appeler setQueue(List<MediaSession.QueueItem> queue) sur celle-ci pour envoyer l'objet à system_server, puis de le récupérer grâce à la méthode List<MediaSession.QueueItem> getQueue() de MediaController (qui peut être obtenue via MediaSession.getController()). Bien que ces méthodes ne semblent pas pouvoir accepter des RemoteViews, elles le peuvent en réalité grâce à l'effacement de type en Java et au fait qu'elles sont implémentées sous le capot à l'aide d'opérations de sérialisation génériques sur List
Cependant, je n'utilise pas ces méthodes SDK ; j'écris manuellement les données des transactions Binder sous-jacentes (car j'ai besoin d'écrire puis de lire des données sérialisées malformées), alors examinons comment ces méthodes fonctionnent
Ces deux méthodes doivent tenir compte du fait que la taille totale de la file d'attente peut dépasser la taille maximale d'une transaction Binder, de sorte que le transfert peut être divisé en plusieurs transactions
L'envoi de la « file d'attente » à system_server se déroule normalement comme suit :
MediaSession.setQueue() appelle d'abord ISession.getBinderForSetQueue()system_server, cette méthode construit et renvoie un objet ParcelableListBinderMediaSession.setQueue() appelle ParcelableListBinder.send(), qui envoie le contenu de la liste dans le Binder fourni, éventuellement sur plusieurs transactions :
1 est écrit et l'élément réel est écrit via (qui écrit le nom de la classe envoyée puis appelle pour envoyer les données)La récupération de la « file d'attente », en revanche, se fait un peu différemment :
MediaController.getQueue() se contente d'appeler ISessionController.getQueue() et de déballer le ParceledListSlice reçusystem_server, getQueue() se contente d'envelopper mQueue dans un ParceledListSlice et de le renvoyerParceledListSlice.writeToParcel() et createFromParcel() ; en particulier, writeToParcel(), lorsqu'il atteint la limite de taille sûre, écrit un objet qui permet de récupérer les morceaux suivantsQuant à la raison de cette différence : des efforts sont en cours pour s'assurer que system_server n'effectue pas d'appels Binder synchrones sortants vers d'autres applications, car si ces appels bloquaient, cela pourrait bloquer tout system_server. Cela signifie que system_server ne devrait pas recevoir de ParceledListSlice. Bien qu'il existe du code qui avertit des transactions synchrones sortantes depuis system_server, il ne pouvait pas encore être rendu contraignant car il existe encore des cas où system_server effectue de tels appels, par exemple en recevant réellement un ParceledListSlice
Nous avons donc maintenant les primitives nécessaires pour faire exécuter à system_server parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size)
Nous pourrions soit tenter de tirer aléatoirement des données Parcel du système, soit organiser les choses pour prendre quelque chose de spécifique
Les considérations sont les suivantes :* Lorsque Parcel.recycle() est appelé, le contenu de cette Parcel est effacé. Cela signifie que la Parcel depuis laquelle nous voudrions copier des données ne doit pas être recycle()ée, ce qui signifie approximativement que nous ne pouvons pas prendre des données d'une transaction Binder qui est terminée
Parcel d'un Bundle présent dans le système (cela inclut les extras Intent et le savedInstanceState d'Activity). Celles-ci ne sont généralement pas recycle()ées du tout (elles sont nettoyées par le Garbage Collector et ne retournent pas au pool ; lorsque le pool est épuisé, Parcel.obtain() crée de nouveaux objets Parcel. Bien sûr, les Parcel auxquelles nous conservons une référence ne seront pas GCées, même si le système n'a plus d'autre utilité pour elles)Parcel utilisées pour les transactions Binder entrantes utilisent un pool séparé des autres Parcel du système. Lorsqu'une transaction Binder sortante est en cours, copie les données dans une secondaire, ou une application utilise à ses propres fins, elle appelle . D'autre part, lorsqu'il y a une transaction entrante, , qui . Dans les deux cas, est utilisé ensuite et se charge de . Cela signifie que l'exploit doit faire lire à partir d'une appartenant au même pool que celui d'où nous souhaitons fuiter des données. Avant de décider de la variante particulière, j'ai écrit les deux, vous trouverez donc les méthodes et dans ma Au final, j'ai décidé de tenter de récupérer le Binder IApplicationThread, qui est envoyé par l'application à system_server lorsque le processus applicatif démarre, et que system_server utilise pour indiquer à l'application quels composants elle doit charger
Lorsque le processus applicatif démarre initialement, l'une des premières choses qu'il fait est d'envoyer IApplicationThread à system_server via un appel à attachApplication(), et c'est la transaction à partir de laquelle je récupérerai ce Binder. Il existe d'autres endroits où IApplicationThread est placé dans une Parcel, comme lorsqu'il est passé pour l'identification de l'appelant par le système lors du démarrage d'une activité (mais je n'avais pas beaucoup de contrôle sur le moment où l'application cible fait cela) ou lorsqu'il est envoyé par le système à l'application en tant que partie de la gestion du cycle de vie de l'Activity (mais cela se fait dans une transaction oneway sortante de system_server et les chances de gagner la course contre seraient minces)
Cela dit, récupérer le Binder qui est reçu par system_server pendant la transaction attachApplication() est également non trivial et il y avait quelques problèmes à surmonter
ParcelLe premier problème pour récupérer le Binder IApplicationThread depuis la Parcel à partir de laquelle les données pour attachApplication() sont reçues est que ce Binder est à une position dataPosition() assez précoce/basse, bien plus basse que celle où notre LazyValue dans le Bundle du RemoteViews pourrait se trouver
Les données pour la transaction attachApplication() consistent simplement en un en-tête RPC suivi du Binder IApplicationThread. L'en-tête RPC (écrit via Parcel.writeInterfaceToken()) consiste en quelques int et le nom de l'interface, dans ce cas "android.app.IActivityManager"
Pendant ce temps, pour lire le Bundle intégré dans RemoteViews, nous aurions besoin de dépasser au moins (quelques éléments mineurs sont sautés) :
readParcelableParcelable : "android.view.RemoteViews"ApplicationInfo assez volumineux présent dans RemoteViews (il doit également ne pas être null et avoir un packageName non null sinon RemoteViews.writeToParcel() échouera lorsque nous essaierons de renvoyer cet objet)Maintenant, dans le Bundle, il suffit de placer une clé String et la lecture de LazyValue commence, la position dans la Parcel est mémorisée, mais à ce stade elle est bien au-delà de la position où se trouverait le Binder IApplicationThread
Pouvons-nous peut-être, une fois ce point atteint, rembobiner la position dans la Parcel ? En d'autres termes, pourrions-nous faire appel à Parcel.setDataPosition() avec une valeur pointant vers une position antérieure à la position actuelle ?
Il s'avère que nous le pouvons, grâce à un autre bug dans LazyValue. Voici le code utilisé pour le lire :```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 dans AOSP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804), le constructeur `LazyValue` se contente d'assigner les paramètres aux champs)
Le truc, c'est que `MathUtils.addOrThrow()` vérifie les dépassements d'entier, [mais accepte parfaitement les valeurs négatives](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)
Si on essayait d'appeler `Parcel.writeValue()` sur un `LazyValue` avec un `mLength` négatif (rempli à partir du paramètre `valueLength`), [cela lèverait une exception dans `appendFrom()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804). Cependant, comme nous sommes en train de lire un `Bundle` avec `Parcel.hasReadWriteHelper()` à `true`, tous les `LazyValue` sont déparcellés après la lecture et nous devions placer intentionnellement un `Parcelable` défectueux à l'intérieur pour qu'il reste sous forme de `LazyValue`. Si nous plaçons des données parcellées valides à la position où se trouve le `LazyValue`, il sera déparcellé et, comme indiqué précédemment, la longueur incohérente ne déclenchera qu'un message dans `logcat`. Cette exploitation particulière définit le type sur `VAL_MAP` et le nombre de paires clé-valeur à zéro. Dans `logcat`, à la lecture de cette valeur, on peut voir le message suivant : "`E Parcel : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP consumed 4 bytes, but -540 expected.`"
(On peut aussi utiliser un `LazyValue` avec une longueur négative spécifiée (sans utiliser les autres bugs décrits dans ce writeup) pour créer un `Bundle` auto-modifiable, la chose même que `LazyValue` avait été créé pour éliminer. Mais c'est une autre histoire (et rapportée séparément à Google) ; dans cette exploitation, je vise plus loin)
Alors, de combien voulons-nous reculer ?
Après l'appel à `setDataPosition()`, la lecture passera à la paire clé-valeur suivante dans le `Bundle`, nous devons donc choisir une position où nous aurons :
1. La clé du `Bundle`, lue via `Parcel.readString()`, peut être à peu près n'importe quoi, y compris pointer vers une longueur invalide (négative ou dépassant la taille totale du `Parcel`) ; dans ce cas, `readString()` renverrait `null`, ce qui est une clé valide dans un `Bundle`
2. Le type de valeur, qui doit être l'un des [types pour lesquels `isLengthPrefixed()` renvoie `true`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804)
3. La longueur de la valeur, qui doit également être une valeur que nous contrôlons ; `Parcel.appendFrom()` échouera si la longueur n'est pas alignée ou dépasse la taille totale du `Parcel` source
Alors, quelle position dans le `Parcel` cela pourrait-il être, sachant que les mêmes données ont déjà été lues et sont nécessaires pour atteindre ce point :
* Pas avant le nom du `Parcelable` (`"android.view.RemoteViews"`), car il n'y a pas assez d'espace
* Pas à l'intérieur du nom du `Parcelable`, car nous sommes incapables de définir le type et la longueur
* Pas directement après le nom du `Parcelable`, car la première chose dans `RemoteViews` est `mode`, que nous [devons définir sur `MODE_NORMAL` pour atteindre notre code](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* Pas après cela, car c'est au-delà du point où se trouve le `Binder` `IApplicationThread`
Hmm, il n'y a pas de bon endroit quand `RemoteViews` est l'objet le plus externe dans les données parcellées
Nous devons trouver un autre `Parcelable` qui :
1. Possède, au début ou à proximité, un endroit où nous pouvons mettre des données arbitraires (par exemple des `int` ou des `String` qui ne sont que des données et n'affectent pas le processus de sérialisation)
2. Peut contenir un `RemoteViews` (directement ou via un `readParcelable` arbitraire)
3. N'a pas un nom de classe complet trop long, car nous sommes toujours limités en taille par la position à laquelle `IApplicationThread` se trouve dans le `Parcel` cible
J'ai donc pris la liste des classes `Parcelable` du système, je l'ai triée par longueur croissante du nom de classe complet et j'ai commencé à vérifier les éléments de cette liste pour voir s'ils remplissent la condition 2
C'est ainsi que je suis arrivé à [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216), qui est ce que cette exploitation utilise. Le processus de lecture de notre objet préparé depuis le `Parcel` se déroule alors comme suit :
* [Drapeau de présence de l'élément](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) pour démarrer `readParcelable`
* [Nom du `Parcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804) : `"android.os.Message"`
* Quelques [`int` que nous pouvons définir aux valeurs que nous voulons](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216) sont lus dans les champs
* Nous atteignons l'appel `readParcelable()`, qui parcourt tout le chemin décrit ci-dessus à travers `RemoteViews` et commence à lire un `Bundle` avec `Parcel.hasReadWriteHelper` à `true`
* Ce `Bundle` déclare avoir deux paires clé-valeur. Dans la première valeur, nous avons un `LazyValue` avec une longueur négative, qui déclenche `Parcel.setDataPosition()` vers la position où se trouve la `String` `"android.os.Message"`
* La lecture passe à la deuxième paire clé-valeur ; la clé est `"android.os.Message"` et le type, la longueur et les données du `LazyValue` sont pris à partir des `int` décrits dans le troisième point. J'ai obtenu un `LazyValue` avec le `mPosition` et le `mLength` que je voulais. Hourra !
* Après la lecture, les `LazyValue` sont déparcellés. Celui avec la taille négative est déparcellé avec succès et remplacé par une `Map` vide, tandis que l'autre échoue à la désérialisation, mais cette exception est attrapée et le `LazyValue` reste simplement dans le `Bundle`
* `readParcelable()` se termine, mais ce n'est pas la fin des données de `Message`. `Message.readFromParcel()` continue maintenant à lire les données après le retour en arrière et voit les données qui ont été initialement écrites comme faisant partie de `RemoteViews`. Si quoi que ce soit lève une exception à ce stade, tout le plan est compromis
* Première exception possible : [il y a un appel à `readBundle()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216). [`Bundle` a une valeur magique et si elle est incorrecte, une exception sera levée](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). Cette valeur magique n'est cependant pas présente si la longueur [est zéro](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91) ou [négative](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804), et c'était justement le cas lorsque la longueur des données du `LazyValue` a été définie à la valeur dont j'avais besoin pour saisir `IApplicationThread`. J'ai donc simplement eu de la chance ici
* Le problème suivant pourrait être [l'appel à `Messenger.readMessengerOrNullFromParcel()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216). Il s'agit en fait d'un objet `Binder` enveloppé. La lecture de ce `Binder` échoue, car un `Binder` est un objet spécial dans un `Parcel` et doit être annoté hors bande pour être lu. Ce problème est [détecté et journalisé par `Parcel` côté natif, mais il n'est pas propagé comme une erreur et `null` est simplement renvoyé](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)
# Bloquer `attachApplication()`
Bon, à l'étape précédente, nous avons réussi à créer un objet qui nous permettra de saisir l'objet `IApplicationThread` pendant que la méthode `attachApplication()` s'exécute
Le problème, c'est que cette méthode se termine rapidement et que nos chances dans une course équitable contre sa fin seraient plutôt minces
Cette méthode acquiert cependant quelques mutex (via l'utilisation de blocs `synchronized () {}` en Java) ; si nous parvenons à acquérir l'un de ces mutex et à y rester bloqués, cette méthode se bloquera également
Revenons maintenant à quelques éléments déjà mentionnés dans ce writeup et qui seront utiles à cette fin :
* `Bundle` effectue la désérialisation des valeurs qu'il contient lorsque ces valeurs sont accédées
* Il existe la classe `ParceledListSlice` qui, lors de la désérialisation, effectuera un appel `Binder` sortant bloquant vers l'objet spécifié dans les données sérialisées
En combinant tout cela : si nous trouvons dans `system_server` un endroit où le contenu d'un `Bundle` fourni par l'application est accédé sous un mutex également utilisé par `attachApplication()`, nous pourrons bloquer `attachApplication()` jusqu'à ce que la transaction `Binder` faite vers notre processus se termine
[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) est une classe décrivant divers paramètres liés au démarrage d'une `Activity` (par exemple l'animation). Contrairement à d'autres classes décrivant les paramètres passés à `system_server`, celle-ci n'implémente pas `Parcelable` mais fournit plutôt une méthode pour la convertir en `Bundle`
Côté `system_server`, ce [`Bundle` est reconverti en `ActivityOptions`, ce qui déclenche la désérialisation](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). J'ai trouvé un endroit où [cette opération est effectuée alors que le mutex `ActivityTaskManagerService.mGlobalLock` est détenu dans `ActivityTaskManagerService.moveTaskToFront()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)
J'appelle donc [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle)), en passant un `Bundle` qui contient un `ParceledListSlice` à la place d'une valeur du type attendu. Ce [`ParceledListSlice` effectue un appel `Binder` vers mon processus](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577) et tant que je ne reviens pas de cet appel, le mutex `ActivityTaskManagerService.mGlobalLock` restera verrouillé
# Créer plusieurs `LazyValue` pointant vers différents `Parcel`
`Parcel.recycle()` et `Parcel.obtain()` fonctionnent de manière [dernier-entré, premier-sorti](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))
Cela signifie que si je crée un `LazyValue` truqué alors qu'aucune autre transaction `Binder` vers `system_server` n'est en cours, j'obtiendrai un `LazyValue` qui pointera vers le `Parcel` toujours utilisé lorsqu'il n'y a qu'une seule transaction entrante vers `system_server` (jusqu'à ce qu'il arrive que deux transactions concurrentes entrantes vers `system_server` commencent et se terminent dans un ordre non-empilé)
Comme je n'ai pas de contrôle sur les autres transactions entrantes vers `system_server`, afin d'améliorer la fiabilité de l'exploitation, j'ai créé plusieurs `LazyValue` pointant vers différents `Parcel`
Puisque j'ai la capacité de déclencher une transaction `Binder` synchrone vers mon processus depuis `system_server`, j'ai utilisé cette capacité pour créer des `LazyValue` à différents niveaux de récursion entre mon processus et `system_server` (bien que cette fois je l'aie fait sans détenir de mutex global)
Donc :
* Je crée un `LazyValue`
* Je déclenche un appel vers `system_server`, `system_server` me rappelle
* Je crée un `LazyValue`
* Je déclenche un appel vers `system_server`, `system_server` me rappelle
* Je crée un `LazyValue`
* Je déclenche un appel vers `system_server`, `system_server` me rappelle
* ...
Ensuite, une fois que j'ai assez de `LazyValue`, j'arrête de faire cela, je reviens de tous ces appels et tous les `Parcel` qui ont été réservés par ces appels sont `recycle()`d
Chacun des `LazyValue` que j'ai créés est enveloppé dans un [`ParceledListSlice` distinct créé par `getQueue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804) et je peux appeler le `Binder` du `ParceledListSlice` pour que `system_server` le sérialise et l'envoie à mon processus
(Une autre façon de faire serait de créer plusieurs `MediaSession`)
# Démarrer le processus de l'application cible
Maintenant, nous avons tout ce qu'il faut pour capturer `IApplicationThread` depuis `attachApplication()` lorsque cela se produit, mais il nous faut encore faire en sorte que `attachApplication()` se produise
En général, [il existe quelques types de composants d'application avec lesquels une autre application peut interagir](https://developer.android.com/guide/components/fundamentals#Components), chacun nécessitant le démarrage du processus de l'application
Je voulais démarrer l'application système Settings (qui [s'exécute sous l'uid système](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) et a donc accès à [tout ce qui se trouve derrière les permissions Android](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b))
Au départ, j'ai tenté de la lancer via `startActivity()`, mais quand j'ai essayé, le processus n'a pas été démarré avant que j'aie libéré le verrou `ActivityTaskManagerService`. Les détails sur la raison de ce comportement se trouvent dans la section « Note supplémentaire : appels `Binder` et réentrance des mutex », mais comme solution, j'ai décidé de demander au système un [`ContentProvider` de cette application](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) plutôt qu'une `Activity`. Cela avait l'avantage supplémentaire d'éviter toute interférence avec mon interface utilisateur
Je n'ai pas utilisé [l'API officielle `ContentResolver` exposée par le SDK](https://developer.android.com/reference/android/content/ContentResolver), mais j'ai utilisé [l'API interne du système, car j'avais besoin d'une API asynchrone](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907) car la liaison au `ContentProvider` ne se terminerait pas avant `attachApplication()`, que je bloque en suspens, bien que démarrer un autre thread aurait pu être une alternative
(Peu importe ce que propose ce `ContentProvider` particulier ; seule compte la possibilité d'établir une connexion avec lui)
C'est donc ainsi que je démarre le processus de l'application Settings. Je m'assure d'abord qu'elle n'est pas déjà en cours d'exécution en utilisant [la méthode officiellement disponible `ActivityManager.killBackgroundProcesses()`](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))
# Tout assembler
Les primitives sont maintenant décrites, voici donc comment tout fonctionne ensemble (c'est à peu près la transcription de la méthode `MainActivity.doAllStuff()` de cette exploitation) :
1. Activer l'accès aux API cachées (les API cachées ne sont pas une frontière de sécurité et il existe [des contournements publiquement disponibles](https://www.xda-developers.com/bypass-hidden-apis/) ; ici j'ai cependant utilisé une méthode basée sur [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String)), que je n'ai vue nulle part ailleurs)
2. (Uniquement si nous réexécutons l'exploitation après une première tentative) Libérer la connexion au `ContentProvider` que nous avons établie à l'étape 6 lors de l'exécution précédente. Nous devons le faire, sinon `ActivityManager.killBackgroundProcesses()` ne considérera pas le processus cible comme « en arrière-plan » et ne le tuera pas
3. Tuer le processus de l'application victime avec `ActivityManager.killBackgroundProcesses()`, car `attachApplication()` n'est appelé qu'au démarrage du processus
4. Demander à `system_server` de créer un tas d'objets contenant des `LazyValue` pointant vers un `Parcel` qui est ensuite recyclé. J'obtiens une référence `Binder` `ParceledListSlice` pour chaque objet contenant un `LazyValue` et je peux effectuer une transaction `Binder` vers elle pour déclencher son renvoi par le système. Chaque création d'objet `LazyValue` est faite à différentes profondeurs d'appels [mutuellement récursifs](https://en.wikipedia.org/wiki/Mutual_recursion) entre `system_server` et mon application afin qu'il soit probable que chacun de ces `LazyValue` ait une référence pendante vers un objet `Parcel` différent
5. Je verrouille `ActivityTaskManagerService.mGlobalLock` en effectuant un appel à `ActivityTaskManagerService.moveTaskToFront()` en passant en argument un `Bundle` qui, lors de la désérialisation, effectue une transaction `Binder` synchrone vers mon processus. Les étapes suivantes sont effectuées depuis ce rappel et sont donc réalisées avec ce verrou détenu
6. Je demande à `ActivityManagerService` une connexion avec le `ContentProvider` de l'application victime (à noter l'absence de « `Task` » dans le nom ; `ActivityTaskManagerService` est une classe axée principalement sur la gestion des composants `Activity` des applications, tandis que `ActivityManagerService` gère les autres [composants d'application](https://developer.android.com/guide/components/fundamentals#Components) (ainsi que le démarrage global des processus) ; cette [division a eu lieu dans Android 10, auparavant la gestion des `Activity` et des autres composants d'application était dans `ActivityManagerService`](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. Je fais un `sleep()` un petit moment pour laisser au processus nouvellement lancé le temps de commencer à appeler `attachApplication()`
8. Pendant que le verrou est toujours détenu, je demande à tous les objets `ParceledListSlice` précédemment créés d'envoyer leur contenu restant (celui qui ne tenait pas dans la transaction initiale), c'est-à-dire les objets contenant des `LazyValue` pointant vers un `Parcel` recyclé. Ensuite, à partir d'un décalage codé en dur correspondant à la position de `IApplicationThread` passé à `attachApplication()`, je lis l'objet `Binder`. Pour l'instant, je me contente de sauvegarder les `Binder` reçus dans une `ArrayList` pour éviter d'en faire trop avec le verrou détenu
9. C'est la fin du code que j'exécute depuis le rappel démarré à l'étape 5. `ActivityTaskManagerService.mGlobalLock` est déverrouillé
10. J'ai le `Binder` `IApplicationThread`. Maintenant, je peux simplement l'utiliser pour charger mon code dans l'application victime comme décrit dans la section suivante
# Comment j'utilise `IApplicationThread`
Comme indiqué précédemment, le `Binder` [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) est envoyé par l'application à `system_server` lorsque le processus de l'application démarre, puis `system_server` l'utilise pour indiquer à l'application quels composants elle doit charger
Il est supposé que cet objet n'est transmis qu'à `system_server` et qu'il n'y a donc pas de vérifications basées sur `Binder.getCallingUid()` là-bas ; nous pouvons donc simplement appeler directement les méthodes offertes par cette interface
J'ai [décrit dans mon writeup précédent comment j'obtiens l'exécution de code en manipulant les arguments de `scheduleReceiver()`](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). Maintenant, la situation est la même, sauf que cette fois c'est moi qui appelle `scheduleReceiver()` alors qu'à l'époque je falsifiais l'interprétation des arguments d'un appel effectué par `system_server`
# Notes supplémentaires
Dans ces sections, je décris quelques éléments qui, en fin de compte, ne se sont pas avérés utiles dans ce cas, même s'ils pourraient être des fonctionnalités intéressantes à connaître ou des bugs potentiels
## Note supplémentaire : `Bundle.clear()`Par souci de simplicité, j'ai ici décrit le `Bundle` mis à jour sans un [commit introduit plus tard, qui permet au `Parcel` utilisé dans `Bundle` pour sauvegarder les `LazyValue`s d'être recyclé en appelant `Bundle.clear()`](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)
Comme indiqué dans le message du commit, le fait que `Bundle` soit copié est suivi, et dans ce cas `clear()` ne recyclera pas le `Parcel`.
Cependant, ce commit modifie également la sémantique du paramètre/variable `recycleParcel` de [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)
Auparavant, le fait que `recycleParcel` soit `false` indiquait que `Parcel` ne devait pas être recyclé, soit parce que [l'appelant définissait `recycleParcel` sur `false` pour indiquer que `Parcel` n'est pas possédé par `Bundle`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1), soit parce qu'il était [défini sur `false` en fonction du résultat de `parcelledData.readArrayMap()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1).
Maintenant, les raisons pour lesquelles `recycleParcel` peut être `false` sont les mêmes, mais l'interprétation a changé : cela ne signifie plus « ne pas recycler ce `Parcel` », mais « différer le recyclage du `Parcel` jusqu'à l'appel à `Bundle.clear()` ».
Cela signifie que si `clear()` était appelé sur un `Bundle` créé alors que `Parcel.hasReadWriteHelper()` retourne `true`, cela mènerait au recyclage du `Parcel`, tandis que le code ayant déclenché la création de ce `Bundle` recyclerait également ce `Parcel`, conduisant à un double-`recycle()`, ce qui produit un comportement similaire à un double-free : les appels suivants à `Parcel.obtain()` retourneraient le même objet deux fois.
Cependant, je n'ai pas trouvé de moyen de faire appeler `clear()` sur un tel `Bundle`.
Depuis que j'ai écrit cela à l'origine, [le comportement de `recycle()` a changé et désormais un recyclage supplémentaire est une opération sans effet, avec un éventuel crash via `Log.wtf()`](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([selon la configuration, mais ne faisant jamais crasher `system_server`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056)). Je dirais que ce nouveau comportement peut encore être dangereux, surtout lorsque nous avons la capacité de bloquer par programmation la désérialisation se déroulant dans un autre processus, mais il n'existe pas vraiment de bonne façon de gérer le double recyclage.
## Note supplémentaire : appels `Binder` et réentrance des mutex
Une fonctionnalité peu connue de `Binder` est qu'il prend en charge l'envoi d'appels récursifs vers le thread d'origine.
C'est-à-dire que si le processus A effectue un appel `Binder` synchrone vers le processus B, puis que le processus B, tout en le traitant sur le même thread, effectue un appel `Binder` synchrone vers le processus A, cet appel dans le processus A sera distribué sur le même thread qui attend que l'appel d'origine vers le processus B se termine.
Autre chose : les sections `synchronized () {}` en Java sont des mutex réentrants, ce qui signifie que si vous y entrez deux fois depuis le même thread, il vous laissera entrer et ne provoquera pas d'interblocage.
Cela signifie qu'en théorie, tant que nous maintenons `ActivityTaskManagerService.mGlobalLock` verrouillé, nous pourrions toujours démarrer l'application Settings en utilisant `startActivity(new Intent(Settings.ACTION_SETTINGS))` et nous entrerions avec succès dans [le bloc `synchonized` que nous bloquons](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d). Cependant, démarrer cette `Activity` implique aussi la création d'une `Task`, ce qui implique d'appeler [`notifyTaskCreated()`, qui publie un message](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) sur [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547), et [le traitement de ce message](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) tente [d'acquérir le verrou que nous bloquons depuis un autre thread](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f). Ainsi, tant que nous ne libérons pas `ActivityTaskManagerService.mGlobalLock`, le thread `DisplayThread` restera bloqué. Ensuite, la procédure de démarrage de l'`Activity` implique [l'envoi d'un message au même thread afin de démarrer le processus de l'application](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c). Tout cela signifie que, dans ce cas, le processus de l'application ne sera pas démarré tant que nous n'aurons pas libéré le verrou, et la raison pour laquelle nous détenions ce verrou en premier lieu était d'empêcher la transaction `attachApplication()` de se terminer afin de pouvoir en récupérer les handles, mais dans ce cas, cette transaction ne démarrerait pas réellement.
Même si nous lançons une `Activity` qui fera partie de la même `Task` que celle en cours (c'est-à-dire que nous lancerions une `Activity` différente de l'application Settings, une qui ne spécifie pas `android:launchMode="singleTask"`), cette procédure impliquera toujours [`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), ce qui a ici le même impact que `notifyTaskCreated()`.
Ainsi, même si mon thread pouvait appeler des méthodes qui utilisent `synchronized (ActivityTaskManagerService.mGlobalLock) {}`, le démarrage d'un nouveau processus d'application après `startActivity()` impliquait l'utilisation de ce verrou depuis un autre thread, ce qui n'était pas utile dans ce cas ; j'ai donc choisi de déclencher le démarrage du processus d'application via `ContentProvider` à la place.
## Note supplémentaire : Autres façons d'utiliser `IApplicationThread`
`IApplicationThread` est un handle très privilégié, donc je considère que l'utiliser après l'avoir obtenu relève de la post-exploitation.
Dans cette exploitation, je l'ai utilisé directement pour demander l'exécution de code dans le processus cible, en tirant parti du fait que l'accès à cette opération est contrôlé par une capacité (la possession de l'objet `Binder`, que nous avons ici exfiltré) et non par `Binder.getCallingUid()`.
Ajouter un contrôle `Binder.getCallingUid()` dans [`ApplicationThread.scheduleReceiver()` (que nous avons utilisé ici pour demander l'exécution de code)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) et d'autres méthodes de `ApplicationThread` (car `scheduleReceiver()` n'est pas la seule méthode de `IApplicationThread` permettant le chargement de code) n'empêcherait toujours pas d'utiliser `IApplicationThread` pour charger du code dans le processus d'une autre application, car un attaquant pourrait passer l'`IApplicationThread` exfiltré à la place du sien à `attachApplication()`.
Outre le chargement de code dans un processus, le fait de posséder `IApplicationThread` permet d'effectuer [`grantUriPermission()` en utilisant les privilèges du processus auquel ce handle appartient](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 appelle recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) afin de lire le contenu de la map clé-valeur. Les clés sont des String et les valeurs sont lues à l'aide de readLazyValue(), créant des objets LazyValue pour les valeurs de types qui sont écrites avec un préfixe de longueur. readArrayMap() renvoie une valeur indiquant s'il est acceptable de recycler le Parcel. S'il y avait des objets LazyValue présents, recycleParcel est défini sur false et le Parcel auquel les LazyValue se réfèrent ne sera pas recyclé (il y a une exception à cela, mais elle n'est pas pertinente ici ; je la décrirai dans la section « Note complémentaire : Bundle.clear() »)unparcel() terminé, mMap est défini (non null) et mappe les clés String soit vers les valeurs réelles si elles sont prêtes, soit vers des objets LazyValuegetValue() est appelé, ce qui mappe la clé (String) à un index (int) et le passe à getValueAt()LazyValue.apply() rembobine le Parcel à la position de LazyValue.mPosition et appelle le Parcel.readValue() normal, dont j'ai déjà parléLazyValue est remplacé dans mMap, de sorte que le prochain appel Bundle.get*() pour la même clé renverra directement la valeur et la désérialisation de LazyValue ne sera pas répétée. Lorsque le Bundle est transmis, cette valeur sera sérialisée à nouveau au lieu que les données originales soient copiées telles quelles (cependant, une fois le Bundle transmis lu, cette valeur redeviendra un LazyValue et d'éventuelles discordances writeToParcel/createFromParcel ne pourront pas affecter les autres valeurs)ParcelmaybeWriteSquashed()system_serverBundleParcel.writeParcelable()Parcelable.writeToParcelBinder, un 0 est écrit pour indiquer qu'il n'y a plus d'éléments dans cette transaction et que les éléments suivants seront envoyés dans une autre transactionParcelableListBinder a reçu le nombre d'éléments spécifié dans la première transaction, il invoque la lambda passée à son constructeur, qui dans ce cas assigne la liste récupérée à MediaSessionRecord.mQueueBinderParceledListSlice est lu depuis un Parcel, il lit d'abord la première partie directement depuis le Parcel, puis si tous les éléments n'ont pas été écrits directement, il appelle le Binder qui a été écrit dans le Parcel afin de récupérer ces élémentsBundleParcelParcelBinderParcel.recycle()RemoteViewsParcelmakeOwnedLeakermakeHolderLeakerParcel.recycle()RemoteViews.readActionsFromParcel()