
Analisi tecnica dettagliata e proof-of-concept per Android CVE-2022-20474, una vulnerabilità di mismatch del Bundle che sfrutta LazyValue con lunghezza negativa per ottenere un comportamento del Bundle auto-modificante.
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:
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
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 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:
@@ -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:
/**
* | 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:
mLength rappresenta la lunghezza dell'intero LazyValue; mLength = objectLength + 8 byte.objectLength deve essere maggiore o uguale a 0.LazyValue conserva solo mLength e non objectLength, perché quando LazyValue esegue una copia in memoria, copia l'intero oggetto.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 anomaloLì 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?
@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:
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);
}