
Análisis técnico detallado y prueba de concepto para Android CVE-2022-20474, una vulnerabilidad de discrepancia de Bundle que explota LazyValue con longitud negativa para lograr un comportamiento de Bundle que se modifica a sí mismo.
Nota: antes de leer este artículo, debería tener una comprensión básica de las vulnerabilidades relacionadas con Bundle Mismatch. Si aún no ha leído los siguientes materiales de referencia, se recomienda hacerlo primero:
Recientemente estaba estudiando en detalle el artículo LeakValue de michalbednarski. Mientras discutía con Canyie, mencionó que en este artículo también se mencionaba un caso de Bundle auto-modificado en el escenario LazyValue. Así que fui a buscar el texto original, y efectivamente, había ese párrafo, pero yo lo había pasado por alto al leer el artículo de Michal. El texto original dice lo siguiente:
(También se puede usar
LazyValuecon una longitud negativa especificada (sin usar otros errores descritos en este informe) para crear unBundleauto-modificado, lo mismo queLazyValuefue creado para eliminar. Pero esa es otra historia (y se informó por separado a Google); en este exploit apunto a algo más).
Michal se refiere a CVE-2022-20474 (boletín, parche). Le eché un vistazo al parche, pero la función en el enlace del parche no estaba completa. Después de completarla, la revisé con más detalle:
@@ -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);
}
}
objectLength en el código es la length de LazyValue, pero de hecho esto es solo la longitud del objeto variable contenido en LazyValue, mientras que la longitud total de LazyValue es controlada por el campo mLength, es decir, valueLength en el código, que se asigna a mLength en el constructor de LazyValue.
Comparemos con el formato de diseño de LazyValue de la siguiente manera:
/**
* | 4B | 4B |
* mSource = Parcel{... | type | length | object | ...}
* a b c d
* length = d - c
* mPosition = a
* mLength = d - a
*/
Basado en lo anterior, podemos obtener los siguientes hechos:
mLength representa la longitud total de LazyValue; mLength = objectLength + 8 bytes.objectLength debería ser mayor o igual a 0.LazyValue solo guarda mLength, no objectLength, porque LazyValue hace copia de memoria basada en todo el objeto.LazyValue.Luego, después de una reflexión profunda, coincidimos en que estos hechos no sirven de nada, porque basándose en lo anterior, solo se puede modificar una vez durante la lectura. Sabemos que la idea central de Bundle auto-modificado es modificar después de que la lectura haya terminado para eludir la verificación de seguridad.
Justo cuando estábamos a punto de rendirnos, de repente notamos algunos detalles en la descripción del parche:
Addresses a security vulnerability where a (-8) length object would cause dataPosition to be reset back to the start of the value, and be re-read again.
objectLength anormalSe menciona que cuando objectLength es -8, hay algunos problemas, lo que nos dio ideas adicionales. ¿Puede LazyValue todavía aplicar de manera normal en este caso?
@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;
}
Se puede ver que el readValue real comienza a leer desde mPosition, luego lee secuencialmente LazyType y objectLength, y luego entra en el flujo normal de lectura de Value, por ejemplo, Parcelable necesita leer ClassName y luego ejecutar createFromParcel. Después de la lectura, no hay diferencia con un Key-Value normal y no afecta la serialización posterior. Revisando de nuevo, la idea central de Bundle auto-modificado es modificar después de que la lectura haya terminado. Aquí solo es una lectura fuera de límites común; parece que este camino no funciona.
¿Y si LazyValue no se apply en este proceso? Es decir, si sigue participando en IPC como LazyValue, se llamará a su función 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);
}