
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/createFromParcelEntão, o que a chamada perigosa a bundle.getParcelable(AccountManager.KEY_INTENT) sem argumento de tipo poderia fazer aqui?
[Resposta no próximo parágrafo; tente adivinhar antes de continuar lendo. Se eu tivesse uma fursona, este seria o lugar para alguma arte]
A resposta é chamar um createFromParcel() não relacionado que na verdade modifica os dados brutos do LazyValue armazenado sob uma chave diferente e que será passado literalmente para o próximo processo
Temos uma implementação de createFromParcel() que pode realmente chamar writeInt() no Parcel fornecido
Mas não devido a um writeInt colocado por engano, mas sim devido à reflexão irrestrita. Em particular, dentro de PackageParser temos o seguinte código:```java
final Class cls = (Class) Class.forName(componentName);
final Constructor cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N); for (int i = 0; i < N; ++i) { intentsList.add(cons.newInstance(in)); }
We can have `Parcel` object which was passed to `createFromParcel` passed to any available in system `public` constructor that accepts single `Parcel` argument
And then [we have following code](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=51-56;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb):```java
public PooledStringWriter(Parcel out) {
mOut = out;
mPool = new HashMap<>();
mStart = out.dataPosition();
out.writeInt(0); // reserve space for final pool size.
}
Temos um construtor que chama writeInt(0) no Parcel fornecido, porém há algumas coisas que complicam a exploração
Primeiro de tudo, embora não seja diretamente visível no código-fonte, imediatamente após newInstance() ser chamado, um cast é realizado e uma ClassCastException é lançada
Eu precisava de algo que, durante createFromParcel, chamasse createFromParcel de outra classe dentro de um bloco try e depois falhasse ao propagar a Exceção capturada
Esta é a parte onde o exploit não funciona de fato em AOSP puro, usei uma classe específica da Samsung
Incluí cópia das partes relevantes dessa classe neste repositório
Este repositório também inclui script que a integra ao AOSP, então para testar você pode executá-lo (passe o caminho para seu checkout do AOSP como argumento, ex.: ./make-aosp-buggy.sh /path/to/aosp), reverta a alteração descrita no início do writeup e execute este exploit contra sua build do AOSP
Anteriormente usei a classe OutputConfiguration do AOSP para engolir Exceções, antes do Android 13 engolir uma Exceção em createFromParcel() combinado com a permissão de construir outros Parcelable-s é uma vulnerabilidade em si, porém no caso do SemImageClipData o engolimento de Exceção não estava presente nessas versões do Android
Há, no entanto, uma diferença importante entre SemImageClipData e o OutputConfiguration usado anteriormente: mesmo que SemImageClipData capture uma Exceção, ele ainda retorna um objeto não-nulo e, se ele for posteriormente convertido para outro tipo, isso acionaria uma ClassCastException, que é o que estamos tentando evitar
O Type Erasure do Java significa que métodos genéricos não sabem de fato sobre o tipo genérico usado pelo chamador. Isso geralmente ajudava na exploração```java // When we read some List, this actually didn't check if list contains only SomeParcelableType List myList = sourceParcel.readParcelableList();
// Above is why Android 13 has introduced typed methods that enforce type at runtime List myList = sourceParcel.readParcelableList(SomeParcelableType.class);
// If untyped method was used when reading, list can contain non-SomeParcelableType // items and they would be written without errors targetParcel.writeParcelableList(myList, 0);
// However if List contains non-SomeParcelableType item, this would throw during item access // (That however commonly didn't happen if we used Parcelable object only as container in gadget chain) SomeParcelableType myItem = myList.get(0);
Desta vez, o apagamento de tipo não jogou a nosso favor. Primeiro tivemos um método que, na verdade, invocava o construtor por meio de reflexão.```java
private static <T extends IntentInfo> ArrayList<T> createIntentsList(Parcel in) {
// ...
final ArrayList<T> intentsList;
// ...
intentsList.add(cons.newInstance(in));
// ...
return intentsList;
}
Este método tem o parâmetro genérico T. Não importa qual tipo de parâmetro foi usado pelo chamador, no entanto, como na declaração deste método existe <T extends IntentInfo>, a linha com a chamada newInstance() torna-se intentsList.add((IntentInfo) cons.newInstance(in));, mesmo que newInstance() retorne Object e ArrayList.add() aceite Object como argumento. Isso introduziu a necessidade de envolver a chamada disso com algum Parcelable que engula a Exception
Depois temos a chamada `bundle.getParcelable()````java @Deprecated @Nullable public T getParcelable(@Nullable String key) { unparcel(); Object o = getValue(key); if (o == null) { return null; } try { return (T) o; } catch (ClassCastException e) { typeWarning(key, o, "Parcelable", e); return null; } }
O procedimento de desserialização é realizado pela chamada `getValue()`, que na verdade leva à chamada `createFromParcel()`. Se um `ClassCastException` ocorrer lá, ele não será capturado. `getValue()` agora retorna qualquer valor que foi desserializado para esta chave através de [`parcel.readValue()`](https://developer.android.com/reference/android/os/Parcel#readValue(java.lang.ClassLoader))
No entanto, se colocarmos `SemImageClipData` como valor, dentro do bloco `try`-`catch` tentaríamos fazer cast para `T`, que neste caso é `Parcelable` conforme declarado na declaração genérica do método. O chamador usa este método como genérico com `T` sendo um `Intent`, porém `getParcelable()` não sabe disso e o cast para `Intent` acontece no chamador e, portanto, `ClassCastException` é lançado fora do `try`
Podemos, no entanto, envolver nosso `SemImageClipData` dentro de um array `Parcelable[]`, então o cast para `T` dentro de `getParcelable()` falhará ao tentar converter `Parcelable[]` para `Parcelable` e lançará `ClassCastException` dentro do `try`, essa `Exception` será registrada e `null` será retornado e então aceito por `checkKeyIntent()`
# O Layout
Então agora precisamos alinhar as coisas dentro do `Bundle` para que após o ciclo `writeToParcel`/`createFromParcel` seu conteúdo seja aquele que preparamos
Mas ao contrário do típico "`Bundle` FengShui" onde o gatilho é ter `createFromParcel` lendo mais ou menos dados do que o `writeToParcel` correspondente fez anteriormente, aqui temos `writeInt(0)` sobrescrevendo parte do `LazyValue` não desserializado
Então aqui está como `Bundle.mParcelledData` se parece quando é primeiro desempacotado por `AccountManagerService` (Offsets obtidos chamando `dataPosition()` através do depurador anexado ao `system_server`)
<table>
<tr><th>Offset</th><th>Valor</th><th>Nota</th></tr>
<tr><td>0</td><td>3</td><td>Número de pares chave-valor</td></tr>
<tr><td>4</td><td>"intent"</td><td>Primeira chave no <code>Bundle</code>, aquela que será acessada por <code>getParcelable(AccountManager.KEY_INTENT)</code></td></tr>
<tr><td>24</td><td>16</td><td>Primeiro <code>LazyValue</code> começa aqui, o tipo é <code>VAL_PARCELABLEARRAY</code></td></tr>
<tr><td>28</td><td>340</td><td>Comprimento declarado do <code>LazyValue</code>, usado para encontrar a próxima chave no <code>Bundle</code>. Nosso <code>LazyValue</code> não terá realmente esse tamanho após ser lido, mas <code>LazyValue.apply</code> <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Parcel.java;l=4501-4505;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">relata isso através de <code>Slog.wtfStack()</code></a> que <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Slog.java;l=230-235;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">não lança exceção</a></td></tr>
<tr><td>32</td><td>1</td><td>Comprimento do array <code>Parcelable[]</code>, o array tem apenas um item e está presente para que o <code>ClassCastException</code> ocorra dentro do bloco <code>try</code> que está em <code>bundle.getParcelable()</code></td></tr>
<tr><td>36</td><td>"com.samsung.android.<br>content.clipboard.data.<br>SemImageClipData"</td><td>Nome da classe <code>Parcelable</code>, esta é a classe wrapper que engolirá a Exception</td></tr>
<tr><td>160</td><td>2</td><td>Tag de tipo usada por <code>createClipBoardData()</code></td></tr>
<tr><td>164</td><td></td><td>Itens que são lidos pelo construtor da superclasse <code>SemImageClipData</code> (que incluem a chamada <code>readParcelable()</code>, porém isso acontece fora do bloco <code>try</code>). Não são realmente relevantes, mas precisamos passar por eles antes de chegar à parte interessante de <code>createFromParcel()</code></td></tr>
<tr><td>252</td><td></td><td>Dados lidos por <code>SemImageClipData.readFromSource()</code></td></tr>
<tr><td>272</td><td>"android.content.pm.<br>PackageParser$Activity"</td><td>Nome do <code>Parcelable</code> lido por <code>mExtraParcelFd = in.readParcelable()</code>. O tipo não corresponde, porém antes que o cast aconteça uma Exception será lançada de qualquer forma</td></tr>
<tr><td>360</td><td></td><td>Campos <code>className</code> & <code>metadata</code> de <code>PackageParser$Component</code></td></tr>
<tr><td>368</td><td>1</td><td>Número de itens em <code>createIntentsList()</code></td></tr>
<tr><td>372</td><td>"android.os.<br>PooledStringWriter"</td><td>Nome da classe que será instanciada através de <code>Class.forName().getConstructor(Parcel.class).newInstance()</code>. Nesta posição o primeiro <code>LazyValue</code> termina, porém a análise dele continua pois <code>readValue()</code> não alcançou o fim. Também é interpretado como segunda chave no <code>Bundle</code> durante o <code>unparcel()</code> inicial</td></tr>
<tr><td>436</td><td>4</td><td>Segundo <code>LazyValue</code> começa aqui, este 4 é <code>VAL_PARCELABLE</code> para o qual <code>Parcel.isLengthPrefixed()</code> retornará <code>true</code>. Este valor é posteriormente sobrescrito pelo construtor <code>PooledStringWriter</code>, após o qual uma Exception é lançada e <code>getParcelable(AccountManager.KEY_INTENT)</code> termina</td></tr>
<tr><td>440</td><td>240</td><td>Comprimento do <code>LazyValue</code> cujo tipo foi declarado como <code>VAL_PARCELABLE</code>, isso é usado para determinar a posição da próxima entrada e quantos dados precisam ser copiados para o <code>Bundle</code> de destino durante a re-serialização. Este <code>LazyValue</code> não é realmente desempacotado e é usado como contêiner de dados brutos</td></tr>
<tr><td>684</td><td>"1&y~pw"</td><td rowspan="2">Terceiro par chave/valor, a chave é gerada aleatoriamente para ter <code>hashCode()</code> Java acima dos usados anteriormente (Itens armazenados dentro de <code>ArrayMap</code> são ordenados por <code>hashCode()</code> crescente da chave e essa é a ordem em que os itens do <code>Bundle</code> serão gravados no <code>Parcel</code>). Este par chave-valor está presente aqui apenas para aumentar o número total de pares gravados, pois esse será o número de pares lidos, mesmo que este par na verdade não seja lido</td></tr>
<tr><td>704</td><td>-1 (<code>VAL_NULL</code>)</td></tr>
</table>
Então, quando o Bundle é serializado novamente, fica assim:
<table>
<tr><th>Offset</th><th>Valor</th><th>Nota</th></tr>
<tr><td>0</td><td>3</td><td>Número de pares chave-valor</td></tr>
<tr><td>4</td><td>"intent"</td><td>Primeira chave no <code>Bundle</code></td></tr>
<tr><td>24</td><td>16</td><td><code>VAL_PARCELABLEARRAY</code>, o array <code>Parcelable[]</code> previamente desserializado está agora sendo serializado novamente</td></tr>
<tr><td>28</td><td>196</td><td>Comprimento do <code>LazyValue</code>, que é nosso objeto <code>SemImageClipData</code> encapsulado. Este comprimento é obtido da execução com meu <code>SemImageClipData</code> mock e, portanto, os offsets apresentados a partir deste ponto não corresponderão aos que apareceriam em um dispositivo Samsung real, porém este <code>LazyValue</code> não será desserializado novamente, então isso não importa para a execução do exploit</td></tr>
<tr><td>224</td><td>"android.os.<br>PooledStringWriter"</td><td>Segunda chave no <code>Bundle</code></td></tr>
<tr><td>288</td><td>0</td><td>Segundo <code>LazyValue</code> começa aqui, o item sob a chave <code>"android.os.PooledStringWriter"</code> não foi acessado, então este <code>LazyValue</code> está sendo copiado dos dados originais, porém a tag de tipo foi sobrescrita pela chamada <code>writeInt(0)</code> feita pelo construtor <code>PooledStringWriter</code> e ao chegar ao processo de destino isso não é mais interpretado como um <code>LazyValue</code></td></tr>
<tr><td>292</td><td>240</td><td>Este era o comprimento do segundo <code>LazyValue</code> que foi copiado do <code>Bundle</code> original, porém como a tag de tipo foi sobrescrita com <code>writeInt(0)</code>, que é <code>VAL_STRING</code>, este valor agora está sendo lido através de <code>readString()</code>. Anteriormente, para <code>LazyValue</code>, o comprimento era expresso em bytes, mas agora, para <code>String</code>, isso é expresso em caracteres de dois bytes. Não há dados suficientes no <code>Parcel</code> de origem para isso, então <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=2221-2226;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">o <code>parcel->readString16Inplace()</code> nativo falha após ler o comprimento</a>, isso porém não causa Exception no lado Java</td></tr>
<tr><td>296</td><td>"intent"</td><td>"Terceira" chave no <code>Bundle</code>. Na verdade sobrescreve a primeira chave: como <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=651-659;drc=584140c83a456b5de99880b440c2d5dfc3c70506">"intent" tem <code>hashCode()</code> menor do que a chave vista anteriormente, o método <code>ArrayMap.append()</code> usa <code>put()</code> que permite substituir valores</a>, caso contrário teríamos <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=667-675;drc=584140c83a456b5de99880b440c2d5dfc3c70506">chave duplicada que seria posteriormente rejeitada por <code>validate()</code></a></td></tr>
<tr><td>316</td><td>4</td><td><code>VAL_PARCELABLE</code>, aqui começa o <code>LazyValue</code> contendo o <code>Intent</code> real que será iniciado</td></tr>
<tr><td>536</td><td>"1&y~pw"</td><td>Item de preenchimento que foi gravado mas não é lido porque todos os 3 pares chave-valor já foram lidos. <a href="https://cs.android.com/android/_/android/platform/system/tools/aidl/+/96a02f50fdfa4d20aa46ae2dde927257eac46d4a">Diferente das interfaces AIDL</a>, não há verificação <code>enforceNoDataAvail()</code> feita no <code>Bundle</code> (mas mesmo se houvesse, ela <a href="https://github.com/michalbednarski/ReparcelBug2/issues/3">poderia ser contornada inserindo uma entrada dummy que especifica o comprimento esperado</a>)</td></tr>
</table>
# Como aconteceu duas vezes
Vamos agora discutir quatro patches, dois dos quais corrigem a vulnerabilidade sobre a qual este writeup trata
* CVE-2023-20944 ([boletim](https://source.android.com/docs/security/bulletin/2023-02-01#framework), [patch](https://android.googlesource.com/platform/frameworks/base/+/d0bc9026e2e62e09fa88c1bcbf1dc1c3fb001375%5E%21/)): Esta é outra vulnerabilidade encontrada por mim. Similarmente a esta, o patch não torna óbvio como seria explorado, mas [parece que outra pessoa descobriu (post de blog em chinês)](https://konata.github.io/posts/creator-mismatch/)
* CVE-2023-21098 ([boletim](https://source.android.com/docs/security/bulletin/2023-04-01#framework), [patch](https://android.googlesource.com/platform/frameworks/base/+/107e6377328486fca55131ea06ca9d6a3c1585e0%5E%21/)): Esta é a primeira vez que relatei o exploit apresentado aqui. Esse patch também introduz a correção para o bypass de `checkKeyIntentParceledCorrectly()` que é aplicável às versões do Android anteriores à 13
* CVE-2023-35669 ([boletim](https://source.android.com/docs/security/bulletin/2023-09-01#framework), [patch](https://android.googlesource.com/platform/frameworks/base/+/f810d81839af38ee121c446105ca67cb12992fc6%5E%21/)): Este não é em resposta ao meu relatório, mas acredito que foi feito para corrigir o mesmo problema do CVE-2023-20944, mas para casos onde `AccountManager.KEY_INTENT` é iniciado por Activities diferentes de `ChooseTypeAndAccountActivity` (por exemplo [`AddAccountSettings`](https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/accounts/AddAccountSettings.java;l=95-107;drc=32813a2bef49b172aed89122b4eb50bf14026ddc), que eu perdi ao relatar o bug pela primeira vez). Esta mudança substituiu o uso do `bundle.getParcelable()` tipado pelo uso do não tipado e verificação manual de `getClass() != Intent.class`, o que na verdade reverteu a correção para CVE-2023-21098
* CVE-2023-45777 ([boletim](https://source.android.com/docs/security/bulletin/2023-12-01#framework), [patch](https://android.googlesource.com/platform/frameworks/base/+/f4644b55d36a549710ba35b6fb797ba744807da6%5E%21/)): Esta é a segunda vez que relatei este exploit. O patch manteve a verificação manual de `getClass() != Intent.class`, mas além disso trouxe de volta o uso do `bundle.getParcelable()` tipado, o que é uma boa maneira de corrigir ambos os problemas
Embora o mesmo exploit funcione tanto para CVE-2023-21098 quanto para CVE-2023-45777, a maneira como ele contornou `checkKeyIntentParceledCorrectly()` difere
No caso do CVE-2023-21098, [`checkKeyIntent()` não era realmente chamado se o `Bundle` verificado não tivesse um `Intent`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=3519-3521;drc=cdd30b5c040ba7ebd0a1cc6009183ff602434fc0). Como `checkKeyIntent()` é o que chama `checkKeyIntentParceledCorrectly()`, no caso em que o `Bundle` original não parecia conter um `Intent`, o `Bundle` após a re-serialização não era verificado
No caso do CVE-2023-45777, `checkKeyIntentParceledCorrectly()` foi corretamente chamado, porém [`writeBundle()` aconteceu lá antes da chamada `getParcelable()` sem argumento de tipo (até a qual o `Bundle` não mudou seu conteúdo)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4921-4929;drc=b0f6558fb36eb76df35c516ec5a65030a34a8734)