
Detaillierte technische Analyse und Proof-of-Concept für Android CVE-2022-20474, eine Bundle-Mismatch-Schwachstelle, die LazyValue mit negativer Länge ausnutzt, um ein sich selbst veränderndes Bundle-Verhalten zu erreichen.
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.