
Writeup y exploit para CVE-2023-45777, bypass de la validación de Intent dentro de AccountManagerService en Android 13 a pesar de la mitigación "Lazy Bundle"
Empecemos esta vez con el parche que apareció como corrección para CVE-2023-45777 en el Android Security Bulletin:```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();
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
if (intent != null && intent.getClass() != Intent.class) {
return false;
}
Pocas personas sintieron suficiente curiosidad como para preguntarme; anteriormente les respondí con algunas pistas y ahora estoy publicando el análisis completo de este problema.
Pero primero, proporcionemos algo de contexto sobre qué está ocurriendo en este parche.
Este es un cambio en el método [`checkKeyIntent()`](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). Este método realiza múltiples comprobaciones para garantizar que el `Intent` proporcionado por la aplicación sea seguro para que el sistema lo lance (usando los privilegios del sistema).
Primero, este método usa `checkKeyIntentParceledCorrectly()`, que serializa y deserializa nuevamente el `Bundle` que estamos comprobando y verifica si el `Intent` obtenido del `Bundle` antes de eso coincide con el `Intent` del `Bundle` después de dicho ciclo. Dado que el lanzamiento del `Intent` ocurre en otros procesos de aplicaciones del sistema distintos de aquel que realiza la validación, anteriormente era [posible construir `Bundle`-s que parecían seguros durante la validación dentro de `AccountManagerService`, pero que contenían un `Intent` diferente después de ser enviados al siguiente proceso](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). Esto simula el envío del `Bundle` al siguiente proceso para detectar tales situaciones.
Después de `checkKeyIntentParceledCorrectly()` tenemos la llamada a `bundle.getParcelable()`, que este parche cambia de la versión obsoleta que podía construir cualquier objeto a una que valida que el objeto que está a punto de deserializarse sea del tipo especificado en el segundo parámetro.
Esa versión con parámetro de tipo se introdujo en Android 13, como parte de un endurecimiento más amplio de `Parcel`/`Bundle`. En particular, antes de Android 13, cuando un `Bundle` se enviaba entre procesos, mantenía una copia sin procesar de todos los datos serializados hasta que se accedía a cualquier elemento, momento en el que se deserializaba cada valor. Ahora, cuando se accede a cualquier valor por primera vez después de que el `Bundle` haya sido recibido, solo se deserializan las claves `String` y los valores de tipos primitivos, mientras que los valores no primitivos se dejan como `LazyValue`-s, que tienen su longitud almacenada como parte de los datos serializados para garantizar que, incluso cuando la lógica de serialización/deserialización no coincida, dichas discrepancias no afecten a otras entradas.
Antes de profundizar, echemos un vistazo a `LazyValue`: en su código fuente [tenemos un buen comentario que explica su estructura de datos](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 y mLength describen la ubicación de todos los datos de LazyValue en el Parcel original, incluyendo type y length. "length" (sin la "m" al principio) se refiere al valor de longitud tal como se escribe en el Parcel y excluye el encabezado (type y length)
Si el Bundle que contiene LazyValue se reenvía a otro proceso, todo el LazyValue incluyendo los campos type y length se copia textualmente de Bundle.mParcelledData al Parcel de destino
Cuando se accede al elemento del Bundle representado por LazyValue, el Parcel se rebobina hasta mPosition y se llama a readValue(). Si se pasa un argumento de tipo a bundle.getParcelable(), este se propaga a readValue(), que tanto garantizará que el tipo que se va a desparcelar sea el esperado como verificará después de desparcelar que el tipo del valor desparcelado sea el esperado. Después de desparcelar, LazyValue se reemplaza, de modo que la próxima vez que el Bundle se escriba en el Parcel, el valor se serializará de nuevo mediante writeValue()
El uso del parámetro tipado de Bundle.get*()/Parcel.read*() es relevante principalmente para métodos como Parcel.readParcelableList(), que devuelve un ArrayList y, debido al borrado de tipos de Java, incluso si hicieras algo como List<SomeParcelableType> field = parcel.readParcelableList();, la parte <SomeParcelableType> no se aplicaba en tiempo de ejecución y dicha List podría contener cualquier clase Parcelable disponible en el sistema y, por lo tanto, todos los createFromParcel/writeToParcel disponibles en el sistema podrían usarse como parte de la serialización/deserialización del tipo que contenía dicha List
También te puede interesar consultar la presentación del equipo de Seguridad y Privacidad de Android sobre la introducción de estos mecanismos (diapositivas, vídeo)
Aquí, sin embargo, el uso de la versión tipada parece redundante, ya que también comprobamos explícitamente el tipo del objeto devuelto. Entonces, ¿qué está pasando y qué vulnerabilidad se está corrigiendo aquí?
Echa un vistazo al parche desde el principio de nuevo