
Writeup e exploit para CVE-2023-45777, bypass para validação de Intent dentro do AccountManagerService no Android 13 apesar da mitigação "Lazy Bundle"
Vamos começar desta vez com o patch que apareceu como correção para o CVE-2023-45777 no 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;
}
Poucas pessoas ficaram curiosas o suficiente para me perguntar; anteriormente respondi a elas com algumas dicas e agora estou publicando o writeup completo para este problema
Mas primeiro vamos fornecer algum contexto sobre o que está acontecendo neste patch
Esta é uma alteração no [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 várias verificações para garantir que o `Intent` fornecido pelo aplicativo seja seguro para o sistema lançar (usando privilégios do sistema)
Primeiro, este método usa `checkKeyIntentParceledCorrectly()`, que serializa e desserializa novamente o `Bundle` que estamos verificando e checa se o `Intent` obtido do `Bundle` antes disso corresponde ao `Intent` do `Bundle` após tal ciclo. Como o lançamento do `Intent` acontece em outros processos de aplicativos do sistema, diferentes daquele que realiza a validação, anteriormente era [possível construir `Bundle`-s que pareciam seguros durante a validação dentro do `AccountManagerService`, mas continham um `Intent` diferente após serem enviados para o próximo processo](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). Isso simula o envio do `Bundle` para o próximo processo, a fim de detectar tais situações.
Após `checkKeyIntentParceledCorrectly()`, temos a chamada `bundle.getParcelable()`, que este patch altera da versão obsoleta que podia construir qualquer objeto para uma que valida que o objeto prestes a ser desserializado é do tipo especificado no segundo parâmetro
Essa versão com parâmetro de tipo foi introduzida no Android 13, como parte de um endurecimento maior do `Parcel`/`Bundle`. Em particular, antes do Android 13, quando um `Bundle` era enviado entre processos, ele mantinha uma cópia bruta de todos os dados serializados até que qualquer item fosse acessado, momento em que cada valor era desserializado. Agora, quando qualquer valor é acessado pela primeira vez após o `Bundle` ter sido recebido, apenas as chaves `String` e os valores de tipos primitivos são desserializados, enquanto valores não primitivos são deixados como `LazyValue`-s, que têm seu comprimento armazenado como parte dos dados serializados para garantir que, mesmo quando a lógica de serialização/desserialização estiver incompatível, tais incompatibilidades não afetem outras entradas
Antes de mergulharmos, vamos dar uma olhada no `LazyValue`: em seu código-fonte [temos um ótimo comentário explicando sua estrutura de dados](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 e mLength descrevem a localização de todos os dados de LazyValue no Parcel original, incluindo type e length. "length" (sem o "m" no início) refere-se ao valor de comprimento como escrito no Parcel e exclui o cabeçalho (type e length)
Se um Bundle contendo LazyValue estiver sendo encaminhado para outro processo, todo o LazyValue, incluindo os campos type e length, é copiado literalmente de Bundle.mParcelledData para o Parcel de destino
Quando o item do Bundle representado por LazyValue é acessado, o Parcel é rebobinado para mPosition e readValue() é chamado. Se um argumento de tipo for passado para bundle.getParcelable(), ele é propagado para readValue(), que garantirá tanto que o tipo prestes a ser desparcelado seja o esperado quanto verificará após o desparcelamento que o tipo do valor desparcelado é o esperado. Após o desparcelamento, LazyValue é substituído, de modo que na próxima vez que o Bundle for escrito no Parcel, o valor será serializado novamente por meio de writeValue()
O uso do parâmetro tipado de Bundle.get*()/Parcel.read*() é mais relevante para métodos como Parcel.readParcelableList(), que retorna um ArrayList e, devido ao Apagamento de Tipo (Type Erasure) do Java, mesmo que você fizesse algo como List<SomeParcelableType> field = parcel.readParcelableList();, a parte <SomeParcelableType> não era aplicada em tempo de execução, e tal List poderia conter qualquer classe Parcelable disponível no sistema; portanto, todos os createFromParcel/writeToParcel disponíveis no sistema poderiam ser usados como parte da serialização/desserialização do tipo que continha tal List
Você também pode querer conferir a apresentação da equipe de Segurança e Privacidade do Android sobre a introdução desses mecanismos (slides, vídeo)
Aqui, no entanto, o uso da versão tipada parece redundante, pois também verificamos explicitamente o tipo do objeto retornado. Então, o que está acontecendo e qual vulnerabilidade está sendo corrigida aqui?
Dê outra olhada no patch desde o início
"intent" for um Intent
Intent, teríamos um problema muito maiorIntent
Intent dentro do Bundle depois que ele for enviado para outro processo, mas o tipo do Parcelable é salvo em um offset anterior a qualquer possível incompatibilidade, e o prefixo de comprimento do LazyValue nos impede de modificar os próximos pares chave-valor em caso de incompatibilidade de writeToParcel/createFromParcel