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
TheLastBundleMismatch — Writeup und Exploit für CVE-2023-45777, Umgehung der Intent-Validierung innerhalb des AccountManagerService auf Android 13 trotz der „Lazy Bundle“-Abschwächung | Kitploit
Tools/GitHubGitHub/michalbednarski/thelastbundlemismatch
Android-SicherheitSchwachstellenanalyseExploitationBinäranalysePapers & Forschung
GitHubmichalbednarski/thelastbundlemismatch

TheLastBundleMismatch

Writeup und Exploit für CVE-2023-45777, Umgehung der Intent-Validierung innerhalb des AccountManagerService auf Android 13 trotz der „Lazy Bundle“-Abschwächung

Repository anzeigen

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
10114vor 2 JahrenVon Kitploit geprüft

Mysteriöser Patch

Beginnen wir diesmal mit dem Patch, der als Fix für CVE-2023-45777 im Android Security Bulletin erschien:```diff diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 7a19d034c2c8..5238595fe2a2 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4923,7 +4923,7 @@ public class AccountManagerService p.setDataPosition(0); Bundle simulateBundle = p.readBundle(); p.recycle();

  • root@kitploit:~
           Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
    
  • root@kitploit:~
           Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
           if (intent != null && intent.getClass() != Intent.class) {
               return false;
           }
    
root@kitploit:~
Wenige Leute waren neugierig genug, um mich zu fragen; zuvor habe ich ihnen mit einigen Hinweisen geantwortet, und jetzt veröffentliche ich den vollständigen Writeup zu diesem Problem

Aber zuerst etwas Kontext darüber, was in diesem Patch vor sich geht

Dies ist eine Änderung in der [`checkKeyIntent()`-Methode](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4938-4954;drc=47de64a38aa1799cb41f41b2ea0c539ee61de64d). Diese Methode führt mehrere Prüfungen durch, um sicherzustellen, dass die von der Anwendung bereitgestellte `Intent` für das System sicher zu starten ist (unter Verwendung der Privilegien des Systems)

Zuerst verwendet diese Methode `checkKeyIntentParceledCorrectly()`, die ein `Bundle`, das wir prüfen, serialisiert und wieder deserialisiert und prüft, ob die `Intent`, die vorher aus dem `Bundle` entnommen wurde, mit der `Intent` aus dem `Bundle` nach einem solchen Zyklus übereinstimmt. Da der Start der `Intent` in anderen System-App-Prozessen erfolgt als in dem, der die Validierung durchführt, war es zuvor [möglich, `Bundle`s zu konstruieren, die während der Validierung innerhalb von `AccountManagerService` sicher erschienen, aber eine andere `Intent` enthielten, nachdem sie an den nächsten Prozess gesendet wurden](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). Dies simuliert das Senden des `Bundle`s an den nächsten Prozess, um solche Situationen zu erkennen.

Nach `checkKeyIntentParceledCorrectly()` haben wir einen `bundle.getParcelable()`-Aufruf, den dieser Patch von der veralteten Version, die beliebige Objekte konstruieren konnte, auf eine Version umstellt, die validiert, dass das Objekt, das deserialisiert werden soll, vom Typ ist, der im zweiten Parameter angegeben wurde

Diese Version mit Typparameter wurde in Android 13 als Teil der umfassenderen `Parcel`/`Bundle`-Härtung eingeführt. Insbesondere vor Android 13 behielt ein `Bundle`, wenn es zwischen Prozessen gesendet wurde, eine rohe Kopie der gesamten serialisierten Daten, bis auf ein Element zugegriffen wurde, woraufhin jeder Wert deserialisiert wurde. Jetzt, wenn auf einen Wert zum ersten Mal zugegriffen wird, nachdem das `Bundle` empfangen wurde, werden nur die `String`-Schlüssel und die Werte primitiver Typen deserialisiert, während nicht-primitive Werte als `LazyValue`s belassen werden, deren Länge als Teil der serialisierten Daten gespeichert ist, um sicherzustellen, dass selbst bei einer Nichtübereinstimmung der Serialisierungs-/Deserialisierungslogik solche Nichtübereinstimmungen keine anderen Einträge beeinträchtigen

Bevor wir eintauchen, werfen wir einen Blick auf `LazyValue`: In seinem Quellcode [haben wir einen schönen Kommentar, der seine Datenstruktur erklärt](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

mPosition und mLength beschreiben den Speicherort der gesamten LazyValue-Daten im ursprünglichen Parcel, einschließlich type und length. „length“ (ohne „m“ am Anfang) bezieht sich auf den Längenwert, wie er in den Parcel geschrieben wurde, und schließt den Header (type und length) aus.

Wenn ein Bundle, das eine LazyValue enthält, an einen anderen Prozess weitergeleitet wird, wird die gesamte LazyValue einschließlich der Felder type und length unverändert von Bundle.mParcelledData in den Ziel-Parcel kopiert.

Wenn auf das durch LazyValue dargestellte Bundle-Element zugegriffen wird, wird Parcel auf mPosition zurückgespult und readValue() aufgerufen. Wenn ein Typargument an bundle.getParcelable() übergeben wird, wird es an readValue() weitergegeben, das sowohl sicherstellt, dass der Typ, der entpackt werden soll, der erwartete ist, als auch nach dem Entpacken verifiziert, dass der entpackte Werttyp der erwartete ist. Nach dem Entpacken wird LazyValue ersetzt, sodass der Wert beim nächsten Schreiben des Bundle in den Parcel erneut über writeValue() serialisiert wird.

Die Verwendung des typisierten Parameters Bundle.get*()/Parcel.read*() ist hauptsächlich für Methoden wie Parcel.readParcelableList() relevant, die eine ArrayList zurückgeben. Aufgrund der Java-Typ-Erasure wird der Teil <SomeParcelableType> selbst bei etwas wie List<SomeParcelableType> field = parcel.readParcelableList(); zur Laufzeit nicht erzwungen, und eine solche List könnte beliebige im System verfügbare Parcelable-Klassen enthalten. Daher könnten alle im System verfügbaren createFromParcel/writeToParcel als Teil der Serialisierung/Deserialisierung des Typs verwendet werden, der eine solche List enthält.

Möglicherweise möchten Sie sich auch die Präsentation des Android Security and Privacy Teams zur Einführung dieser Mechanismen ansehen (Folien, Video).

Hier scheint die Verwendung der typisierten Version jedoch redundant zu sein, da wir den Typ des zurückgegebenen Objekts auch explizit prüfen. Was ist also los und welche Schwachstelle wird hier behoben?

Nebenwirkungen

Werfen Sie noch einmal einen Blick auf den Patch von Anfang an

  • Wenn der unter dem Schlüssel "intent" deserialisierte Wert ein Intent ist
    • Er wird validiert, um auf eine Komponente zu verweisen, deren Start für das System sicher ist
    • Wenn eine Abweichung von einem Intent-Objekt ausgelöst werden könnte, hätten wir ein viel größeres Problem
  • Wenn der deserialisierte Wert kein Intent ist
    • Um etwas Schädliches zu tun, müssten wir ein Intent im Bundle haben, nachdem es an einen anderen Prozess gesendet wurde, aber der Typ des Parcelable wird an einem früheren Offset als jede mögliche Abweichung gespeichert, und die Längenpräfixierung von LazyValue verhindert, dass wir bei einer writeToParcel/createFromParcel-Abweichung nachfolgende Schlüssel-Wert-Paare ändern.

Was könnte der Aufruf von bundle.getParcelable(AccountManager.KEY_INTENT) ohne Typargument hier also Gefährliches tun?

[Antwort im nächsten Absatz, versuchen Sie zu raten, bevor Sie weiterlesen. Wenn ich eine Fursona hätte, wäre dies der Ort für etwas Kunst]

Die Antwort ist der Aufruf eines nicht verwandten createFromParcel(), das tatsächlich die Rohdaten der LazyValue modifiziert, die unter einem anderen Schlüssel gespeichert ist und unverändert an den nächsten Prozess weitergegeben wird.

Wir haben eine createFromParcel()-Implementierung, die tatsächlich writeInt() auf dem bereitgestellten Parcel aufrufen kann.

Aber nicht, weil writeInt versehentlich platziert wurde, sondern aufgrund von uneingeschränkter Reflexion. Insbesondere innerhalb von PackageParser haben wir den folgenden Code:```java final Class cls = (Class) Class.forName(componentName); final Constructor cons = cls.getConstructor(Parcel.class);

intentsList = new ArrayList<>(N); for (int i = 0; i < N; ++i) { intentsList.add(cons.newInstance(in)); }

root@kitploit:~
We can have `Parcel` object which was passed to `createFromParcel` passed to any available in system `public` constructor that accepts single `Parcel` argument

And then [we have following code](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=51-56;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb):```java
public PooledStringWriter(Parcel out) {
    mOut = out;
    mPool = new HashMap<>();
    mStart = out.dataPosition();
    out.writeInt(0); // reserve space for final pool size.
}

Wir haben einen Konstruktor, der writeInt(0) auf dem bereitgestellten Parcel aufruft, aber es gibt ein paar Dinge, die die Ausnutzung erschweren

Zunächst einmal, obwohl es im Quellcode nicht direkt sichtbar ist, wird unmittelbar nach dem Aufruf von newInstance() eine Typumwandlung durchgeführt und eine ClassCastException ausgelöst

Die Exception verschlucken

Ich brauchte etwas, das während createFromParcel den Aufruf von createFromParcel einer anderen Klasse innerhalb eines try-Blocks durchführt und dann fehlschlägt, um die abgefangene Exception weiterzugeben

Dies ist der Teil, in dem der Exploit auf reinem AOSP tatsächlich nicht funktioniert; ich habe eine Samsung-spezifische Klasse verwendet

Ich habe eine Kopie der relevanten Teile dieser Klasse in diesem Repository beigefügt

Dieses Repository enthält auch ein Skript, das sie in AOSP integriert, sodass du es zum Testen ausführen kannst (übergib den Pfad zu deinem AOSP-Checkout als Argument, z. B. ./make-aosp-buggy.sh /path/to/aosp), die zu Beginn des Writeups beschriebene Änderung rückgängig machen und diesen Exploit gegen deinen AOSP-Build ausführen

Ich habe zuvor die OutputConfiguration-Klasse aus AOSP zum Verschlucken von Exceptions verwendet; vor Android 13 war das Verschlucken einer Exception in createFromParcel() in Kombination mit der Erlaubnis, andere Parcelable-Objekte zu konstruieren, an sich eine Schwachstelle, aber im Fall von SemImageClipData war das Verschlucken von Exceptions auf diesen Android-Versionen nicht vorhanden

Es gibt jedoch einen wichtigen Unterschied zwischen SemImageClipData und der zuvor verwendeten OutputConfiguration: Obwohl SemImageClipData eine Exception abfängt, gibt es dennoch ein Nicht-Null-Objekt zurück, und wenn es später in einen anderen Typ umgewandelt wird, würde das eine ClassCastException auslösen, was wir zu vermeiden versuchen

Java Type Erasure schlägt zurück

Java Type Erasure bedeutet, dass generische Methoden den generischen Typ, der vom Aufrufer verwendet wird, nicht tatsächlich kennen. Das hat die Ausnutzung normalerweise unterstützt```java // When we read some List, this actually didn't check if list contains only SomeParcelableType List myList = sourceParcel.readParcelableList();

// Above is why Android 13 has introduced typed methods that enforce type at runtime List myList = sourceParcel.readParcelableList(SomeParcelableType.class);

// If untyped method was used when reading, list can contain non-SomeParcelableType // items and they would be written without errors targetParcel.writeParcelableList(myList, 0);

// However if List contains non-SomeParcelableType item, this would throw during item access // (That however commonly didn't happen if we used Parcelable object only as container in gadget chain) SomeParcelableType myItem = myList.get(0);

root@kitploit:~
Diesmal spielte die Typauslöschung nicht zu unseren Gunsten. Zuerst hatten wir eine Methode, die den Konstruktor tatsächlich über Reflection aufrief.```java
private static <T extends IntentInfo> ArrayList<T> createIntentsList(Parcel in) {
    // ...
    final ArrayList<T> intentsList;
    // ...
    intentsList.add(cons.newInstance(in));
    // ...
    return intentsList;
}

Diese Methode hat einen generischen Parameter T. Es spielt keine Rolle, welcher Parametertyp vom Aufrufer verwendet wurde, jedoch wird durch die Deklaration <T extends IntentInfo> in dieser Methode die Zeile mit dem newInstance()-Aufruf zu intentsList.add((IntentInfo) cons.newInstance(in));, obwohl newInstance() Object zurückgibt und ArrayList.add() Object als Argument akzeptiert. Dies führte zu der Notwendigkeit, den Aufruf mit einem Parcelable zu umschließen, das die Exception verschluckt.

Dann haben wir den bundle.getParcelable()-Aufruf.```java @Deprecated @Nullable public T getParcelable(@Nullable String key) { unparcel(); Object o = getValue(key); if (o == null) { return null; } try { return (T) o; } catch (ClassCastException e) { typeWarning(key, o, "Parcelable", e); return null; } }

root@kitploit:~
Die Deserialisierungsprozedur wird durch den `getValue()`-Aufruf ausgeführt, der tatsächlich zu einem `createFromParcel()`-Aufruf führt. Wenn dort eine `ClassCastException` auftritt, wird sie nicht abgefangen. `getValue()` gibt nun den Wert zurück, der für diesen Schlüssel durch [`parcel.readValue()`](https://developer.android.com/reference/android/os/Parcel#readValue(java.lang.ClassLoader)) deserialisiert wurde.

Wenn wir jedoch `SemImageClipData` als Wert einfügen, würden wir im `try`-`catch`-Block versuchen, auf `T` zu casten, was in diesem Fall `Parcelable` ist, wie in der generischen Deklaration der Methode angegeben. Der Aufrufer verwendet diese Methode als generisch mit `T` als `Intent`, aber `getParcelable()` weiß das nicht und der Cast auf `Intent` erfolgt im Aufrufer, wodurch die `ClassCastException` außerhalb des `try`-Blocks ausgelöst wird.

Wir können unser `SemImageClipData` jedoch in ein `Parcelable[]`-Array verpacken. Dann schlägt der Cast auf `T` innerhalb von `getParcelable()` fehl, da `Parcelable[]` nicht auf `Parcelable` gecastet werden kann, und wirft eine `ClassCastException` innerhalb des `try`-Blocks. Diese `Exception` wird protokolliert und `null` wird zurückgegeben, was dann von `checkKeyIntent()` akzeptiert wird.

# Das Layout

Jetzt müssen wir die Inhalte im `Bundle` so ausrichten, dass nach dem `writeToParcel`/`createFromParcel`-Zyklus die Inhalte denen entsprechen, die wir vorbereitet haben.

Aber anders als beim typischen "`Bundle` FengShui", bei dem der Auslöser darin besteht, dass `createFromParcel` mehr oder weniger Daten liest, als `writeToParcel` zuvor geschrieben hat, überschreibt hier `writeInt(0)` einen Teil des nicht deserialisierten `LazyValue`.

So sieht `Bundle.mParcelledData` aus, wenn es zum ersten Mal von `AccountManagerService` entpackt wird (Offsets wurden durch Aufruf von `dataPosition()` über einen Debugger ermittelt, der an `system_server` angehängt ist).

<table>
<tr><th>Offset</th><th>Wert</th><th>Hinweis</th></tr>
<tr><td>0</td><td>3</td><td>Anzahl der Schlüssel-Wert-Paare</td></tr>
<tr><td>4</td><td>"intent"</td><td>Erster Schlüssel im <code>Bundle</code>, der über <code>getParcelable(AccountManager.KEY_INTENT)</code> abgerufen wird</td></tr>
<tr><td>24</td><td>16</td><td>Erster <code>LazyValue</code> beginnt hier, Typ ist <code>VAL_PARCELABLEARRAY</code></td></tr>
<tr><td>28</td><td>340</td><td>Deklarierte Länge des <code>LazyValue</code>, verwendet zum Auffinden des nächsten Schlüssels im <code>Bundle</code>. Unser <code>LazyValue</code> wird nach dem Lesen nicht wirklich diese Größe haben, aber <code>LazyValue.apply</code> <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Parcel.java;l=4501-4505;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">meldet das über <code>Slog.wtfStack()</code></a>, was <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Slog.java;l=230-235;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">keine Exception auslöst</a></td></tr>
<tr><td>32</td><td>1</td><td>Länge des <code>Parcelable[]</code>-Arrays. Das Array hat nur ein Element und ist vorhanden, damit die <code>ClassCastException</code> innerhalb des <code>try</code>-Blocks in <code>bundle.getParcelable()</code> auftritt</td></tr>
<tr><td>36</td><td>"com.samsung.android.<br>content.clipboard.data.<br>SemImageClipData"</td><td>Name der <code>Parcelable</code>-Klasse. Dies ist die Wrapper-Klasse, die die Exception verschluckt</td></tr>
<tr><td>160</td><td>2</td><td>Typ-Tag, das von <code>createClipBoardData()</code> verwendet wird</td></tr>
<tr><td>164</td><td></td><td>Elemente, die vom Superklassen-Konstruktor von <code>SemImageClipData</code> gelesen werden (einschließlich eines <code>readParcelable()</code>-Aufrufs, der jedoch außerhalb des <code>try</code>-Blocks erfolgt). Nicht wirklich relevant, aber wir müssen sie durchlaufen, bevor wir den interessanten Teil von <code>createFromParcel()</code> erreichen</td></tr>
<tr><td>252</td><td></td><td>Daten, die von <code>SemImageClipData.readFromSource()</code> gelesen werden</td></tr>
<tr><td>272</td><td>"android.content.pm.<br>PackageParser&#36;Activity"</td><td>Name des <code>Parcelable</code>, das von <code>mExtraParcelFd = in.readParcelable()</code> gelesen wird. Der Typ stimmt nicht überein, aber bevor der Cast erfolgt, wird ohnehin eine Exception ausgelöst</td></tr>
<tr><td>360</td><td></td><td><code>className</code>- und <code>metadata</code>-Felder von <code>PackageParser$Component</code></td></tr>
<tr><td>368</td><td>1</td><td>Anzahl der Elemente in <code>createIntentsList()</code></td></tr>
<tr><td>372</td><td>"android.os.<br>PooledStringWriter"</td><td>Name der Klasse, die wir über <code>Class.forName().getConstructor(Parcel.class).newInstance()</code> instanziieren. An dieser Position endet der erste <code>LazyValue</code>, die Analyse wird jedoch fortgesetzt, da <code>readValue()</code> das Ende nicht erreicht hat. Wird auch als zweiter Schlüssel im <code>Bundle</code> während des anfänglichen <code>unparcel()</code> interpretiert</td></tr>
<tr><td>436</td><td>4</td><td>Zweiter <code>LazyValue</code> beginnt hier. Diese 4 ist <code>VAL_PARCELABLE</code>, für das <code>Parcel.isLengthPrefixed()</code> <code>true</code> zurückgibt. Dieser Wert wird später vom <code>PooledStringWriter</code>-Konstruktor überschrieben, danach wird eine Exception ausgelöst und <code>getParcelable(AccountManager.KEY_INTENT)</code> endet</td></tr>
<tr><td>440</td><td>240</td><td>Länge des <code>LazyValue</code>, dessen Typ als <code>VAL_PARCELABLE</code> deklariert wurde. Dies wird verwendet, um die Position des nächsten Eintrags zu bestimmen und wie viele Daten während der erneuten Serialisierung in das Ziel-<code>Bundle</code> kopiert werden müssen. Dieser <code>LazyValue</code> wird nicht tatsächlich entpackt und dient als Rohdatencontainer</td></tr>
<tr><td>684</td><td>"1&y~pw"</td><td rowspan="2">Drittes Schlüssel-Wert-Paar. Der Schlüssel wird zufällig generiert, sodass sein Java-<code>hashCode()</code> über den zuvor verwendeten liegt (Elemente in <code>ArrayMap</code> werden nach aufsteigendem <code>hashCode()</code> des Schlüssels sortiert, und in dieser Reihenfolge werden Elemente aus dem <code>Bundle</code> in den <code>Parcel</code> geschrieben). Dieses Schlüssel-Wert-Paar ist nur vorhanden, um die Gesamtzahl der geschriebenen Paare zu erhöhen, da dies auch die Anzahl der gelesenen Paare sein wird, obwohl dieses Paar tatsächlich nicht gelesen wird</td></tr>
<tr><td>704</td><td>-1 (<code>VAL_NULL</code>)</td></tr>
</table>

Wenn das `Bundle` dann erneut serialisiert wird, sieht es so aus:

<table>
<tr><th>Offset</th><th>Wert</th><th>Hinweis</th></tr>
<tr><td>0</td><td>3</td><td>Anzahl der Schlüssel-Wert-Paare</td></tr>
<tr><td>4</td><td>"intent"</td><td>Erster Schlüssel im <code>Bundle</code></td></tr>
<tr><td>24</td><td>16</td><td><code>VAL_PARCELABLEARRAY</code>, das zuvor deserialisierte <code>Parcelable[]</code>-Array wird nun erneut serialisiert</td></tr>
<tr><td>28</td><td>196</td><td>Länge des <code>LazyValue</code>, das ist unser verpacktes <code>SemImageClipData</code>-Objekt. Diese Länge stammt aus der Ausführung mit meinem Mock-<code>SemImageClipData</code>, daher stimmen die ab hier dargestellten Offsets nicht mit denen überein, die auf einem echten Samsung-Gerät erscheinen würden. Dieses <code>LazyValue</code> wird jedoch nicht erneut deserialisiert, daher spielt das für die Exploit-Ausführung keine Rolle</td></tr>
<tr><td>224</td><td>"android.os.<br>PooledStringWriter"</td><td>Zweiter Schlüssel im <code>Bundle</code></td></tr>
<tr><td>288</td><td>0</td><td>Zweiter <code>LazyValue</code> beginnt hier. Das Element unter dem Schlüssel <code>"android.os.PooledStringWriter"</code> wurde nicht abgerufen, daher wird dieses <code>LazyValue</code> aus den Originaldaten kopiert. Das Typ-Tag wurde jedoch durch den <code>writeInt(0)</code>-Aufruf des <code>PooledStringWriter</code>-Konstruktors überschrieben, und beim Erreichen des Zielprozesses wird dies nicht mehr als <code>LazyValue</code> interpretiert</td></tr>
<tr><td>292</td><td>240</td><td>Dies war die Länge des zweiten <code>LazyValue</code>, das aus dem ursprünglichen <code>Bundle</code> kopiert wurde. Da das Typ-Tag jedoch mit <code>writeInt(0)</code> überschrieben wurde, was <code>VAL_STRING</code> ist, wird dieser Wert nun über <code>readString()</code> gelesen. Zuvor wurde die Länge für <code>LazyValue</code> in Bytes ausgedrückt, jetzt für <code>String</code> in Zwei-Byte-Zeichen. Es gibt nicht genügend Daten im Quell-<code>Parcel</code> dafür, daher schlägt <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=2221-2226;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">natives <code>parcel->readString16Inplace()</code> nach dem Lesen der Länge fehl</a>, das verursacht jedoch keine Exception auf der Java-Seite</td></tr>
<tr><td>296</td><td>"intent"</td><td>"Dritter" Schlüssel im <code>Bundle</code>. Überschreibt tatsächlich den ersten Schlüssel: Da <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=651-659;drc=584140c83a456b5de99880b440c2d5dfc3c70506">"intent" einen kleineren <code>hashCode()</code> hat als der zuvor gesehene Schlüssel, verwendet die <code>ArrayMap.append()</code>-Methode <code>put()</code>, was das Ersetzen von Werten ermöglicht</a>, andernfalls hätten wir <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=667-675;drc=584140c83a456b5de99880b440c2d5dfc3c70506">einen doppelten Schlüssel, der später von <code>validate()</code> abgelehnt würde</a></td></tr>
<tr><td>316</td><td>4</td><td><code>VAL_PARCELABLE</code>, hier beginnt das <code>LazyValue</code>, das den tatsächlichen <code>Intent</code> enthält, der gestartet wird</td></tr>
<tr><td>536</td><td>"1&y~pw"</td><td>Auffüll-Element, das geschrieben, aber nicht gelesen wird, da alle 3 Schlüssel-Wert-Paare bereits gelesen wurden. <a href="https://cs.android.com/android/_/android/platform/system/tools/aidl/+/96a02f50fdfa4d20aa46ae2dde927257eac46d4a">Anders als bei AIDL-Schnittstellen</a> gibt es keine <code>enforceNoDataAvail()</code>-Prüfung für <code>Bundle</code> (aber selbst wenn es eine gäbe, <a href="https://github.com/michalbednarski/ReparcelBug2/issues/3">könnte sie durch Einfügen eines Dummy-Eintrags umgangen werden, der die erwartete Länge angibt</a>)</td></tr>
</table>

# Wie es zweimal passierte

Lassen Sie uns nun vier Patches besprechen, von denen zwei die Schwachstelle beheben, um die es in diesem Writeup geht.

* CVE-2023-20944 ([Bulletin](https://source.android.com/docs/security/bulletin/2023-02-01#framework), [Patch](https://android.googlesource.com/platform/frameworks/base/+/d0bc9026e2e62e09fa88c1bcbf1dc1c3fb001375%5E%21/)): Dies ist eine weitere von mir gefundene Schwachstelle. Ähnlich wie hier macht der Patch nicht offensichtlich, wie er ausgenutzt werden könnte, aber <a href="https://konata.github.io/posts/creator-mismatch/">es sieht so aus, als hätte jemand anderes es herausgefunden (Blogbeitrag auf Chinesisch)</a>.
* CVE-2023-21098 ([Bulletin](https://source.android.com/docs/security/bulletin/2023-04-01#framework), [Patch](https://android.googlesource.com/platform/frameworks/base/+/107e6377328486fca55131ea06ca9d6a3c1585e0%5E%21/)): Dies ist das erste Mal, dass ich den hier vorgestellten Exploit gemeldet habe. Dieser Patch führt auch eine Korrektur für die `checkKeyIntentParceledCorrectly()`-Umgehung ein, die für Android-Versionen vor 13 gilt.
* CVE-2023-35669 ([Bulletin](https://source.android.com/docs/security/bulletin/2023-09-01#framework), [Patch](https://android.googlesource.com/platform/frameworks/base/+/f810d81839af38ee121c446105ca67cb12992fc6%5E%21/)): Dieser Patch ist nicht als Reaktion auf meinen Bericht entstanden, aber ich glaube, er wurde erstellt, um dasselbe Problem wie CVE-2023-20944 zu beheben, jedoch für Fälle, in denen `AccountManager.KEY_INTENT` von anderen Activities als `ChooseTypeAndAccountActivity` gestartet wird (zum Beispiel [`AddAccountSettings`](https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/accounts/AddAccountSettings.java;l=95-107;drc=32813a2bef49b172aed89122b4eb50bf14026ddc), das ich beim ersten Melden des Fehlers übersehen habe). Diese Änderung ersetzte die Verwendung von typisiertem `bundle.getParcelable()` durch die Verwendung von untypisiertem und eine manuelle `getClass() != Intent.class`-Prüfung, was tatsächlich die Korrektur für CVE-2023-21098 rückgängig machte.
* CVE-2023-45777 ([Bulletin](https://source.android.com/docs/security/bulletin/2023-12-01#framework), [Patch](https://android.googlesource.com/platform/frameworks/base/+/f4644b55d36a549710ba35b6fb797ba744807da6%5E%21/)): Dies ist das zweite Mal, dass ich diesen Exploit gemeldet habe. Der Patch behielt die manuelle `getClass() != Intent.class`-Prüfung bei, führte aber zusätzlich die Verwendung von typisiertem `bundle.getParcelable()` wieder ein, was eine gute Möglichkeit ist, beide Probleme zu beheben.

Während derselbe Exploit sowohl für CVE-2023-21098 als auch für CVE-2023-45777 funktioniert, unterscheidet sich die Art und Weise, wie er `checkKeyIntentParceledCorrectly()` umging.

Im Fall von CVE-2023-21098 wurde [`checkKeyIntent()` nicht tatsächlich aufgerufen, wenn das geprüfte `Bundle` keinen `Intent` enthielt](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=3519-3521;drc=cdd30b5c040ba7ebd0a1cc6009183ff602434fc0). Da `checkKeyIntent()` der Aufrufer von `checkKeyIntentParceledCorrectly()` ist, wurde das `Bundle` nach der erneuten Serialisierung in dem Fall, in dem das ursprüngliche `Bundle` keinen `Intent` zu enthalten schien, nicht geprüft.

Im Fall von CVE-2023-45777 wurde `checkKeyIntentParceledCorrectly()` korrekt aufgerufen, jedoch [`writeBundle()` dort vor dem `getParcelable()`-Aufruf ohne Typargument ausgeführt wurde (bis dahin änderte das `Bundle` seinen Inhalt nicht)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4921-4929;drc=b0f6558fb36eb76df35c516ec5a65030a34a8734).
Tool herunterladen