Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
ReparcelBug2 — Writeup et exploit pour une application installée afin d'élever les privilèges système sur Android 12 Beta via CVE-2021-0928, une inadéquation de sérialisation `writeToParcel`/`createFromParcel` dans `OutputConfiguration` | Kitploit
Outils/GitHubGitHub/michalbednarski/reparcelbug2
Sécurité AndroidEscalade de PrivilègesAnalyse des VulnérabilitésExploitationSécurité MobileAnalyse de Binaires
GitHubmichalbednarski/reparcelbug2

ReparcelBug2

Writeup et exploit pour une application installée afin d'élever les privilèges système sur Android 12 Beta via CVE-2021-0928, une inadéquation de sérialisation `writeToParcel`/`createFromParcel` dans `OutputConfiguration`

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
Voir le dépôt
12323il y a 4 ansVérifié par Kitploit

CVE-2021-0928, incompatibilité de sérialisation writeToParcel/createFromParcel dans android.hardware.camera2.params.OutputConfiguration

Il s'agit d'un exploit utilisant cette vulnérabilité pour une élévation de privilèges depuis une application Android installée vers l'application Paramètres Android (ou toute autre application installée à laquelle l'application pourrait envoyer un <receiver> déclaré dans AndroidManifest.xml ; l'élévation de privilèges en envoyant vers un <activity> était également possible, bien que non présentée ici)

J'ai découvert ce problème à l'origine sur Android 12 Developer Preview 3

La version de l'exploit présente dans ce dépôt fonctionne sur Android 12 Beta 2 et 3

La vulnérabilité a été corrigée dans la première version officielle d'Android 12

Le writeup ci-dessous a été rédigé à l'origine pour Google afin que ce rapport soit considéré comme une chaîne d'exploitation complète

Au moment de la rédaction, Android 12 n'était pas disponible dans l'AOSP (les versions Android Developer Preview/Beta ne sont pas open source)

Capture d'écran de la notification Android de l'application Paramètres : Hello from 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

Introduction à Parcel

La plupart des IPC sur Android sont effectués via la classe Parcel

L'utilisation de base de Parcel est la suivante :```java Parcel p = Parcel.obtain(); p.writeInt(1); p.writeString("Hello");

root@kitploit:~
Ensuite, `Parcel` est envoyé à un autre processus [via `Binder`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Alternativement, pour les tests, on peut appeler [`p.setDataPosition(0)`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)) pour rembobiner le parcel à sa position initiale et commencer la lecture :```java
int a = p.readInt(); // a = 1
String b = p.readString(); // b = "Hello"

Il convient de noter que Parcel conserve en interne la position à partir de laquelle les lectures sont effectuées. Il est de la responsabilité de l'utilisateur de la classe Parcel de s'assurer que les méthodes read* correspondent aux méthodes write* utilisées précédemment, sinon les lectures suivantes seront effectuées à partir de positions incorrectes dans le tampon.

Parcel offre également la possibilité d'écrire des objets personnalisés, la méthode privilégiée pour ce faire est d'implémenter l'interface Parcelable

Voici un exemple d'implémentation de l'interface Parcelable (code non pertinent supprimé, la classe WindowContainerTransaction est utilisée dans l'exploit dans le cadre de la chaîne de gadgets, mais il n'y a rien de mal à cela)```java package android.window; public final class WindowContainerTransaction implements Parcelable { private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>(); private final ArrayList mHierarchyOps = new ArrayList<>();

root@kitploit:~
private WindowContainerTransaction(Parcel in) {
    in.readMap(mChanges, null /* loader */);
    in.readList(mHierarchyOps, null /* loader */);
}

@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
    dest.writeMap(mChanges);
    dest.writeList(mHierarchyOps);
}

@NonNull
public static final Creator<WindowContainerTransaction> CREATOR =
        new Creator<WindowContainerTransaction>() {
            @Override
            public WindowContainerTransaction createFromParcel(Parcel in) {
                return new WindowContainerTransaction(in);
            }
        };

}

root@kitploit:~
Comme on peut le voir ci-dessus, la méthode `writeToParcel()` est utilisée lors de l'écriture. Ensuite, lors de la lecture, la méthode de fabrique `CREATOR.createFromParcel()` est appelée. Il est de la responsabilité de l'implémentation de `Parcelable` de garantir que `createFromParcel` lit la même quantité de données que celle écrite par `writeToParcel`, sinon toutes les lectures ultérieures de ce `Parcel` liront des données à partir d'un décalage incorrect.

Une telle classe peut être écrite dans/lue depuis un `Parcel` via :

* Appel direct de `obj.writeToParcel(parcel, 0)` / `obj = WindowContainerTransaction.CREATOR.createFromParcel()`, ce qui est souvent utilisé lorsque le type de la classe est connu, par exemple lorsqu'un `Parcelable` possède un champ avec un autre `Parcelable` ou dans le code généré par AIDL lorsqu'une méthode RPC définie a un `Parcelable` comme argument
* Via `Parcel.writeParcelable`/`readParcelable`. [`writeParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=1909;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) écrit d'abord le nom de la classe puis appelle la méthode `writeToParcel` de l'interface `Parcelable`. [`readParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3282;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) lit le nom de classe écrit, trouve la classe portant ce nom dans le `ClassLoader` fourni ou dans `BOOTCLASSPATH` si null a été fourni. Une fois la classe trouvée, son champ statique `CREATOR` est utilisé pour obtenir une instance de [`Parcelable.Creator`](https://developer.android.com/reference/android/os/Parcelable.Creator), qui est la fabrique utilisée pour lire cette classe. Il est important de noter que lorsque la méthode `readParcelable` est utilisée, elle peut lire n'importe quel `Parcelable` disponible dans le classpath, car le nom de l'objet à créer est lu depuis le même `Parcel`
* `readParcelable` est utilisé par de nombreuses autres méthodes de `Parcel`, par exemple `readList` vu dans l'exemple ci-dessus lit les éléments via `readValue`, qui est la méthode la plus générique pour transférer des objets dans un `Parcel`, et l'une des façons qu'elle utilise passe par `readParcelable`. De plus, dans l'exemple ci-dessus, en raison de l'effacement de type de Java, le champ `ArrayList<HierarchyOp> mHierarchyOps` peut en réalité contenir n'importe quels objets pris en charge par Parcel, pas seulement ceux compatibles avec le type spécifié dans la déclaration du type générique

# Incohérences `writeToParcel`/`createFromParcel`

Comme indiqué ci-dessus, il est de la responsabilité de l'implémentation de l'interface `Parcelable` de garantir que `createFromParcel` lit la même quantité de données depuis le `Parcel` que celle écrite précédemment par le `writeToParcel` correspondant. Chaque fois qu'il existe dans `BOOTCLASSPATH` un `Parcelable` pouvant violer ce contrat, cela crée une vulnérabilité car cela permet le scénario suivant :

1. Une application malveillante envoie à `system_server` un `Bundle` OU un `Parcelable` contenant une instance `Parcelable` défectueuse ainsi que des données spécialement construites qui seront réellement lues à l'étape 3 mais transmises telles quelles à l'étape 2
2. `system_server` vérifie que le `Bundle` est sûr puis le transmet OU `system_server` transmet le `Parcelable` fourni à une méthode AIDL qui possède également des données critiques transmises dans le paramètre suivant (si les données reçues dans ce paramètre pouvaient être modifiées, cela provoquerait un problème de sécurité)
3. Une autre application reçoit des données de `system_server` et leur fait confiance, mais en raison de la sérialisation défectueuse, les données qu'elle voit réellement diffèrent de celles que `system_server` avait l'intention d'envoyer

J'ai utilisé « OU » dans les étapes ci-dessus car elles décrivent à la fois une [ancienne variante d'exploit qui conduit au lancement d'une Activity arbitraire que j'ai publiée en 2017](https://github.com/michalbednarski/ReparcelBug) (à gauche du « OU ») et une nouvelle variante que je décrirai ici dans la section suivante

# Comment `BroadcastReceiver` est exécuté dans l'application

Du point de vue du développeur d'application utilisant les API disponibles dans le SDK Android, la façon dont [`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) fonctionne est qu'une application appelle [`sendBroadcast`](https://developer.android.com/reference/android/content/Context#sendBroadcast(android.content.Intent)) (bien que souvent les applications veuillent recevoir des Broadcasts du système, pas d'une application), puis l'Intent diffusé est mis en correspondance avec le `<receiver>` défini dans `AndroidManifest.xml` ; lorsque cela se produit, le système démarre le processus de l'application réceptrice, instancie la sous-classe `BroadcastReceiver` telle que définie dans l'attribut `<receiver android:name>` puis appelle la méthode [`onReceive`](https://developer.android.com/reference/android/content/BroadcastReceiver#onReceive(android.content.Context,%20android.content.Intent))

Examinons la communication avec `system_server` se produisant dans le processus recevant le broadcast :

* Lorsque le processus d'application est initialement démarré, il [appelle `IActivityManager.attachApplication()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7340;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c), en faisant cela il transmet le handle [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl) qui est utilisé par le système pour indiquer au processus d'application quoi faire
* Lorsque le système veut exécuter un `BroadcastReceiver` enregistré dans le manifeste dans le processus d'application, il appelle la méthode [`scheduleReceiver`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=950;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c) en utilisant `IApplicationThread` décrit au point précédent. Cette méthode a plusieurs arguments mais ici les plus importants sont les deux premiers :
  1. `Intent intent`, qui a été précédemment transmis au système lorsque `sendBroadcast()` a été appelé
  2. `ActivityInfo info`, qui contient des informations sur le composant qui doit être exécuté. La valeur de ce paramètre est prise par le système depuis le Package Manager Service. Le plus important est que les données transmises dans ce paramètre incluent le chemin du fichier à partir duquel la classe Java gérant le broadcast reçu sera chargée

À ce stade, vous pouvez probablement deviner quel est ce nouveau chemin d'exploit : appeler `sendBroadcast()` en transmettant un `Intent` qui fera que, lorsque le système tentera d'appeler `scheduleReceiver`, l'application dans laquelle `scheduleReceiver` est invoqué verra un `ActivityInfo` falsifié

Il convient de noter que ce nouveau chemin d'exploit est devenu viable dans Android 12, car auparavant il n'y avait aucun moyen de placer des `Parcelable` arbitraires dans un `Intent` (les [extras d'Intent](https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable)) ne comptent pas car ils sont placés dans un `Bundle` dont la longueur totale est écrite dans le Parcel et qui est [lu comme un bloc unique](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1671-1681;drc=5d123b67756dffcfdebdb936ab2de2b29c799321), donc les extras ne peuvent pas provoquer une mauvaise interprétation de l'objet `Intent` qui les contient)

# Déclenchement de l'incohérence `writeToParcel`/`createFromParcel`

La plupart du temps, les incohérences `writeToParcel`/`createFromParcel` sont des cas où, dans l'une de ces méthodes, l'un des champs est oublié ou écrit deux fois ; dans ce cas, l'envoi d'un tel objet déclenchera toujours l'incohérence. (Cela se produit la plupart du temps lorsque l'objet, bien que `Parcelable`, n'est pas réellement utilisé entre les processus, sinon cela serait rapidement remarqué lors d'une utilisation normale)

Cette fois, cependant, ce n'était pas le cas et le déclenchement de l'incohérence n'est pas évident

Examinons la classe vulnérable ([l'original était ici](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/hardware/camera2/params/OutputConfiguration.java;drc=46c390a1c695e2dc458cb889e40559f259f60aed), les lignes marquées `// New in Android 12` ont été ajoutées manuellement car elles n'étaient pas présentes dans AOSP au moment de la rédaction) ([Voici le commit ayant initialement introduit la vulnérabilité](https://android.googlesource.com/platform/frameworks/base/+/a69b1bc58b0838e06deefb190e226774a34671e6%5E%21/#F10), cependant il a été publié après la sortie d'Android 12)```java
package android.hardware.camera2.params;

public final class OutputConfiguration implements Parcelable {
    private OutputConfiguration(@NonNull Parcel source) {
        int rotation = source.readInt();
        int surfaceSetId = source.readInt();
        int surfaceType = source.readInt();
        int width = source.readInt();
        int height = source.readInt();
        boolean isDeferred = source.readInt() == 1;
        boolean isShared = source.readInt() == 1;
        ArrayList<Surface> surfaces = new ArrayList<Surface>();
        source.readTypedList(surfaces, Surface.CREATOR);
        String physicalCameraId = source.readString();
        boolean isMultiResolution = source.readInt() == 1; // New in Android 12
        ArrayList<Integer> sensorPixelModesUsed = new ArrayList<Integer>(); // New in Android 12
        source.readList(sensorPixelModesUsed, Integer.class.getClassLoader()); // New in Android 12

		// SNIP: copy values from variables set above to fields of this class
    }

    public static final @android.annotation.NonNull Parcelable.Creator<OutputConfiguration> CREATOR =
            new Parcelable.Creator<OutputConfiguration>() {
        @Override
        public OutputConfiguration createFromParcel(Parcel source) {
            try {
                OutputConfiguration outputConfiguration = new OutputConfiguration(source);
                return outputConfiguration;
            } catch (Exception e) {
                Log.e(TAG, "Exception creating OutputConfiguration from parcel", e);
                return null;
            }
        }

        @Override
        public OutputConfiguration[] newArray(int size) {
            return new OutputConfiguration[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mRotation);
        dest.writeInt(mSurfaceGroupId);
        dest.writeInt(mSurfaceType);
        dest.writeInt(mConfiguredSize.getWidth());
        dest.writeInt(mConfiguredSize.getHeight());
        dest.writeInt(mIsDeferredConfig ? 1 : 0);
        dest.writeInt(mIsShared ? 1 : 0);
        dest.writeTypedList(mSurfaces);
        dest.writeString(mPhysicalCameraId);
        dest.writeInt(mIsMultiResolution ? 1 : 0); // New in Android 12
        dest.writeList(mSensorPixelModesUsed); // New in Android 12
    }

    private ArrayList<Surface> mSurfaces;
    private final int mRotation;
    private final int mSurfaceGroupId;
    private final int mSurfaceType;
    private final Size mConfiguredSize;
    private final int mConfiguredFormat;
    private final int mConfiguredDataspace;
    private final int mConfiguredGenerationId;
    private final boolean mIsDeferredConfig;
    private boolean mIsShared;
    private String mPhysicalCameraId;
    private boolean mIsMultiResolution; // New in Android 12
    private ArrayList<Integer> mSensorPixelModesUsed; // New in Android 12
}

Alors, quel est le problème ici et pourquoi ce changement introduit-il une vulnérabilité ? Comme je l'ai dit en discutant de l'exemple Parcelable WindowContainerTransaction, readList peut en réalité remplir la liste avec n'importe quel objet pris en charge par Parcel, pas seulement ceux correspondant à la déclaration générique (ArrayList<Integer>). Cependant, comme nous utilisons cette classe uniquement comme partie d'une chaîne de gadgets de sérialisation et que nous ne l'utilisons pas réellement, ce champ ne sera utilisé que pour lire et écrire dans Parcel (et toute tentative d'utiliser des éléments d'un ArrayList contenant des éléments ne correspondant pas à sa déclaration générique ne mènerait de toute façon qu'à une ClassCastException), ce n'est donc pas un problème en soi.

Dans cette classe, il y a aussi un try-catch dans createFromParcel, ce qui signifie que si une Exception est levée pendant la lecture, la lecture de OutputConfiguration sera arrêtée et la lecture de l'objet contenant OutputConfiguration se poursuivra. Lorsque cela se produit, tout OutputConfiguration sera écrit dans le Parcel, mais il ne sera lu que jusqu'au point où l'Exception s'est produite. Cela crée une inadéquation car les données non consommées écrites dans OutputConfiguration.writeToParcel seront en réalité lues par l'objet qui appelait OutputConfiguration.CREATOR.createFromParcel.

Maintenant, la combinaison de ces deux éléments (permettre l'imbrication d'objets arbitraires pris en charge par Parcel et les envelopper dans un try-catch sans relancer) donne la capacité de construire un Parcelable qui peut être écrit par system_server puis lu d'une manière contrôlée par l'application qui a initialement construit le Parcelable transmis par system_server.

Bon, pour construire un tel Parcelable, nous devons maintenant trouver quelque chose à mettre dans mSensorPixelModesUsed qui sera lu avec succès dans system_server (car cet objet est reçu de l'application attaquante via Parcel), écrit avec succès par system_server, puis échouera à être désérialisé et lèvera une Exception dans l'application victime.

Une des façons de faire est d'utiliser une classe présente dans system_server mais pas dans les applications, de sorte que toute tentative de désérialisation mène à une ClassNotFoundException. Je ne peux cependant pas choisir un Parcelable de system_server car readParcelable sans ClassLoader explicitement spécifié ne recherchera que dans BOOTCLASSPATH, qui ne contiendra pas les classes spécifiques à system_server. La solution à ce problème est d'utiliser l'une des classes Serializable, car ObjectInputStream choisira le ClassLoader à partir de la première méthode non-BOOTCLASSPATH dans la trace de la pile.

J'ai choisi PackageManagerException, mais avant de l'utiliser, il y a encore une chose à faire. Dans le constructeur de OutputConfiguration, lorsque readList est appelé, l'argument loader est explicitement défini sur Integer.class.getClassLoader(). Cette loader est propagée à readValue(), puis à readSerializable() et dans readSerializable(), si le paramètre loader n'est pas nul, il est utilisé à la place de de (ce contrôle ne fait rien car lorsque ne trouve pas la classe, il lève une exception au lieu de retourner null). La solution est cependant assez simple : il suffit d'envelopper dans un qui fait sans spécifier de . C'est là qu'intervient la classe décrite ci-dessus.

Donc, à ce stade, nous avons l'objet suivant :

  • OutputConfiguration
    • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
      • mHierarchyOps.get(0) = PackageManagerException

Un tel objet peut maintenant être désérialisé avec succès dans system_server : lorsque WindowContainerTransaction appelle readList, il essaiera de trouver la classe PackageManagerException en utilisant le ClassLoader du serveur système (pas BootClassLoader), car il peut la trouver dans la trace de la pile. Ce chargeur de classe se trouve dans la trace de la pile car, bien que toutes les méthodes suivantes ne proviennent pas du chemin de classe du serveur système : Binder#execTransact(), IActivityManager$Stub#onTransact() généré par AIDL et les méthodes de toutes les classes Parcelable utilisées, il y avait une méthode déclarée dans le serveur système dans la trace de la pile : un onTransact surchargé dans ActivityManagerService. Par conséquent, peut lire puis écrire un tel objet dans un , et lorsque l'application cible tente de le lire, la classe ne sera pas disponible et donc une sera levée, puis .

Nous avons donc déclenché cette inadéquation. Enfin, pas vraiment encore à ce stade, car l'exception a été attrapée alors qu'aucune donnée non lue n'était laissée par OutputConfiguration.writeToParcel, mais nous pouvons facilement ajouter un autre élément à la List mSensorPixelModesUsed et cet élément sera écrit via Parcel.writeValue et laissé non lu après la lecture de OutputConfiguration.

Mettre cela dans un Intent

Comme indiqué ci-dessus, je veux déclencher l'inadéquation à partir d'un objet Intent, car il sera transmis par system_server à une méthode AIDL qui a Intent en premier paramètre et les informations d'exécution en deuxième paramètre, de sorte que la sérialisation/désérialisation de l'Intent passé en premier paramètre mène à la modification de la valeur du deuxième paramètre.

Dans Intent.readFromParcel(), toutes les valeurs sont lues via des méthodes typées dédiées, donc nous ne pouvons pas y spécifier une classe Parcelable personnalisée.

Cependant, dans Intent, il y a un ClipData imbriqué et depuis Android 12, dans ClipData$Item, il y a un nouveau champ ActivityInfo mActivityInfo (absent de l'AOSP au moment de la rédaction initiale, voici le commit introduisant ce champ, ce champ est lu via in.readTypedObject(ActivityInfo.CREATOR) dans le constructeur ClipData(Parcel in)).

Ensuite, dans le constructeur ActivityInfo(Parcel source), il n'y a encore aucun moyen de mettre un Parcelable personnalisé, mais comme ActivityInfo étend ComponentInfo, il a un champ applicationInfo.

Enfin, dans ApplicationInfo, il y a un champ SparseArray<int[]> splitDependencies, qui est lu via readSparseArray, qui à son tour utilise readValue pour lire les éléments du SparseArray.

À ce stade, nous pourrions placer OutputConfiguration dans splitDependencies, cependant la lecture de splitDependencies est suivie de quelques appels readString8() et il serait préférable d'avoir un contrôle total sur les données non consommées après l'inadéquation afin de pouvoir y placer directement des chaînes vides et ne pas nous soucier d'une interprétation différente des données non consommées.

Pour ce faire, nous devons d'abord placer un conteneur de données brutes dans OutputConfiguration.mSensorPixelModesUsed qui sera écrit via writeValue. J'ai choisi Bundle. De cette façon, dans les données non consommées, il restera :

  1. Le tag VAL_BUNDLE de writeValue
  2. La longueur des données brutes (ce lien s'applique également aux éléments restants de cette liste)
  3. BUNDLE_MAGIC
  4. Les données brutes transmises textuellement via Parcel.appendFrom

Nous avons donc trois éléments Parcel.writeInt non consommés ; nous pouvons nous en débarrasser en enveloppant OutputConfiguration dans un Parcelable qui, lors de la lecture, lit une valeur Parcelable arbitraire suivie de trois entiers. J'ai trouvé cela dans ZenPolicy CREATOR.

Pour résumer, nous avons la hiérarchie d'objets suivante (présente dans system_server et qu'il tente de transmettre à scheduleReceiver) :

  • Intent
    • mClipData = ClipData
      • mItems.get(0).mActivityInfo = ActivityInfo
        • applicationInfo = ApplicationInfo
          • splitDependencies.get(0) = ZenPolicy
            • mVisualEffects.get(0) = OutputConfiguration
              • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
                • mHierarchyOps.get(0) = PackageManagerException
              • mSensorPixelModesUsed.get(1) = Bundle

Cela est écrit par system_server. Ensuite, l'application réceptrice lit tout normalement jusqu'aux données readSerializable de PackageManagerException incluses. Cependant, après la lecture des données Serializable pour PackageManagerException, une exception est levée et la lecture de tout ce qui se trouve sous OutputConfiguration est annulée, laissant Bundle non lu. La lecture se poursuit vers ZenPolicy, qui consomme les trois entiers qui précèdent les données brutes dans Bundle. Ensuite, la lecture de ApplicationInfo se poursuit avec la lecture des données qui étaient auparavant des données brutes transmises textuellement dans Bundle. La lecture de ces données brutes continuera avec les objets restants de cette pile (ApplicationInfo, ActivityInfo, et ), puis ces données brutes seront utilisées pour lire le paramètre suivant de la méthode .

Ce qui se passe ensuite dans handleReceiver

Comme je viens de le dire, les paramètres restants de scheduleReceiver sont maintenant lus depuis le tampon contrôlé par l'attaquant.

Examinons ce qui se passe une fois cette méthode invoquée.

D'abord, scheduleReceiver regroupe les valeurs de tous les arguments et utilise sendMessage() pour transmettre l'exécution au thread principal.

Ensuite, sur le thread principal, handleReceiver est appelé.

handleReceiver appelle getPackageInfoNoCheck, en lui passant l'ApplicationInfo qu'il a reçu dans le cadre de l'ActivityInfo transmis à l'argument scheduleReceiver.

getPackageInfo vérifie si le package portant le nom donné est déjà présent dans le cache et, sinon, il construit une nouvelle instance LoadedApk, en lui passant l'objet ApplicationInfo reçu précédemment (comme l'attaquant veut provoquer la construction d'un nouveau LoadedApk, un packageName d'un package non vu auparavant dans ce processus est utilisé).

Ensuite, la méthode ContextImpl.getClassLoader() est utilisée, qui, lors de la première exécution, délègue à mPackageInfo.getClassLoader(), mPackageInfo étant un LoadedApk construit au paragraphe précédent.

Ensuite, il y a createOrUpdateClassLoaderLocked, qui appelle makePaths pour remplir zipPaths avec les chemins à utiliser dans le ClassLoader, puis ils sont joints et assignés à la variable zip et cela est transmis à createClassLoader.

makePaths remplit zipPaths en utilisant les informations de ApplicationInfo, notamment sourceDir. L'application attaquante fabrique un ApplicationInfo injecté avec sourceDir défini sur le chemin de son propre apk ; par conséquent, la classe du récepteur sera réellement chargée depuis l'apk de l'attaquant. Cela mène directement à l'exécution de code contrôlé par l'attaquant dans l'application recevant la diffusion.

Note sur les contrôles d'API cachées

Il y avait encore une chose à contourner : les contrôles d'API cachées. Ceux-ci n'ont jamais été destinés à être une frontière de sécurité (car une application peut toujours utiliser le NDK et appeler directement le syscall sous-jacent), mais dans ce cas, ils ont été contournés en construisant un ClipData falsifié en écrivant manuellement des données dans un Parcel puis en utilisant readParcelable. Un tel ClipData pouvait ensuite être normalement attaché à un Intent puis transmis à sendBroadcast(), de sorte que l'envoi de la diffusion lui-même était effectué uniquement avec des API publiques.

Correctifs

Le rapport ci-dessus a été initialement envoyé à Google et il semble qu'ils en aient tiré parti, car il existe plusieurs correctifs qui en résultent (je pense, sans preuve de causalité directe).

Publiés avec Android 12 :

  • La suppression d'exception de OutputConfiguration et des classes associées a été retirée
  • OutputConfiguration#mSensorPixelModesUsed n'est plus écrit via writeValue
  • ClipData#mActivityInfo n'est plus écrit dans le Parcel sauf s'il est explicitement demandé lors de l'écriture (donc Intent ne peut plus contenir de Parcelable arbitraires de BOOTCLASSPATH, éliminant cette technique d'exploitation)

Présents uniquement sur la branche master au moment de la rédaction, pas dans les versions publiées, probablement présents dans Android 13 (pas dans 12L) :

  • Il existe de nouvelles méthodes de lecture de List sur Parcel qui vérifient le type des éléments et les versions non typées ont été marquées comme dépréciées
  • Il existe une nouvelle méthode Parcel#enforceNoDataAvail() qui vérifie qu'il ne reste aucune donnée non lue dans le Parcel, qui sera apparemment utilisée par AIDL après la lecture des arguments d'appel RPC. Habituellement, mes exploits reposaient sur le fait qu'après la lecture de toutes les données du Parcel, tout le reste était ignoré ; cela ne sera plus le cas, bien que je pense que dans de nombreux cas, on pourrait construire des données qui provoquent à la fin un déplacement vers la position finale, donc ce n'est pas une atténuation très forte. Quoi qu'il en soit, parfois de tels problèmes se produisent naturellement sans être détectés, donc cela permettrait de les attraper. Une discussion plus approfondie à ce sujet se trouve dans le problème #3
  • Chaque élément d'un Bundle aura sa longueur enregistrée séparément. Cela tue à peu près toute la classe de bugs que je signalais en privé à Google depuis 2014, description publiée en 2017 et . Si je compte correctement, cela fera 8 ans de vie pour cette classe de bugs (honnêtement, je n'ai aucune idée si c'est beaucoup, bien qu'il puisse encore exister des variantes d'exploitation non-, comme celle-ci précisément (bien que celle-ci précisément ait déjà été corrigée)).
Télécharger l’outil
resolveClass
ObjectInputStream
c != null
Class.forName
PackageManagerException
Parcelable
readList
ClassLoader
WindowContainerTransaction
system_server
Parcel
PackageManagerException
ClassNotFoundException
enveloppée dans une RuntimeException
attrapée par le CREATOR de OutputConfiguration
ClipData
Intent
handleReceiver
le code lui-même environ un an plus tard
Bundle