Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
LeakValue — Exploit para CVE-2022-20452, escalada de privilégios no Android de um aplicativo instalado para aplicativo de sistema (ou outro aplicativo) via LazyValue usando Parcel após recycle() | Kitploit
Ferramentas/GitHubGitHub/michalbednarski/leakvalue
Segurança AndroidEscalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoSegurança MóvelExploração de Binários
GitHubmichalbednarski/leakvalue

LeakValue

Exploit para CVE-2022-20452, escalada de privilégios no Android de um aplicativo instalado para aplicativo de sistema (ou outro aplicativo) via LazyValue usando Parcel após recycle()

Ver Repositório
34765há 3 anosRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Android 13 introduz muitas melhorias para endurecer o mecanismo de serialização Parcel

Aqui está a apresentação da equipe de Segurança e Privacidade do Android sobre as melhorias feitas

Isso é ótimo, definitivamente elimina ou torna inexploráveis muitas vulnerabilidades. Além disso, eles descrevem como quebrar meu exploit anterior, que permitia que aplicativos carregassem seu código em outros aplicativos (incluindo os do sistema)

Mas agora estou de volta com um novo exploit que alcança o mesmo, embora de forma diferente. Ele depende das seguintes vulnerabilidades que foram introduzidas durante o endurecimento do Parcel mencionado:

  • CVE-2022-20452 (boletim, patch)
  • CVE-2022-20474 (boletim, patch)

Captura de tela do aplicativo exibindo texto. Título: LeakValue. Texto principal: Created 6 ValueLeaker-s. Locking ActivityTaskManagerService. Locked ActivityTaskManagerService. Unlocking ActivityTaskManagerService. Unlocked ActivityTaskManagerService. leakedBinders=[android.os.BinderProxy@f06702e]. Leaked interface: android.app.IApplicationThread. Requesting code execution. Shellcode has been executed in uid=1000 pid=6904 packageName=com.android.settings uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0. Na parte inferior da tela, há dois botões: START e MANUAL TESTING

(Também logcat da execução do aplicativo, a exploração é barulhenta nos logs)

Introdução aos bugs de incompatibilidade entre Parcel e Parcelable

A classe Parcel do Android é a base da comunicação entre processos

Objetos podem implementar a interface Parcelable para permitir que sejam escritos em um Parcel, por exemplo (copiado do AOSP):```java public class UsbAccessory implements Parcelable { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public UsbAccessory createFromParcel(Parcel in) { String manufacturer = in.readString(); String model = in.readString(); String description = in.readString(); String version = in.readString(); String uri = in.readString(); IUsbSerialReader serialNumberReader = IUsbSerialReader.Stub.asInterface( in.readStrongBinder());

root@kitploit:~
        return new UsbAccessory(manufacturer, model, description, version, uri,
                serialNumberReader);
    }
};

public void writeToParcel(Parcel parcel, int flags) {
    parcel.writeString(mManufacturer);
    parcel.writeString(mModel);
    parcel.writeString(mDescription);
    parcel.writeString(mVersion);
    parcel.writeString(mUri);
    parcel.writeStrongBinder(mSerialNumberReader.asBinder());

} }

root@kitploit:~
Note que `Parcel` internamente armazena a posição na qual a escrita ou leitura é realizada; `readString()` analisa dados em String e também avança a posição. Essa posição pode ser obtida/definida manualmente através de [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)). Implementações da interface `Parcelable` devem garantir que seus métodos `writeToParcel` e `createFromParcel` escrevam/leiam a mesma quantidade de dados; caso contrário, todas as leituras subsequentes obterão dados com deslocamentos incorretos

[`Bundle`](https://developer.android.com/reference/android/os/Bundle) (mapa chave-valor que pode ser enviado entre processos) pode conter [uma variedade de objetos que podem ser escritos em Parcel através de `writeValue()`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937). Quando o conteúdo de `Bundle` é lido de `Parcel`, qualquer classe `Parcelable` disponível no sistema pode ser lida

`Bundle` adia a análise real do conteúdo ao ter o comprimento de todos os dados empacotados escritos em `Parcel` e então apenas [copiando a parte relevante do Parcel original para um Parcel secundário armazenado em `mParcelledData`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683) (isso permite, por exemplo, que [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)) forneça `Parcelable`s que não estão disponíveis em `system_server`; o `Bundle` inteiro é então passado para `system_server` e de volta sem análise do conteúdo)

No entanto, uma vez que qualquer valor em `Bundle` foi acessado, todos os valores dentro de `Bundle` [eram desempacotados](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) e [cada par chave-valor presente era analisado](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). Se tal mapa contivesse um `Parcelable` com métodos `writeToParcel` e `createFromParcel` desequilibrados e, posteriormente, tal `Bundle` fosse encaminhado para outro processo, esse outro processo poderia ver conteúdo diferente do `Bundle`. Isso tornou todas essas [incompatibilidades em classes disponíveis em vulnerabilidades do sistema](https://github.com/michalbednarski/ReparcelBug), pois existem [lugares no sistema onde o `Bundle` é inspecionado para ser seguro](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=5037-5046) e então encaminhado para outro processo

Neste artigo, chamo tal `Bundle` que apresenta um conteúdo e depois outro após ser encaminhado de `Bundle` auto-alterável

Outra coisa importante aqui é que, além de apenas bytes (Strings, números, objetos compostos pelos itens acima), `Parcel` também pode conter Descritores de Arquivo e `Binder`s. `Binder`s são objetos nos quais se pode fazer chamadas RPC; ou seja, um processo cria um objeto `Binder` e sobrescreve o [método `onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Em seguida, o `Binder` é passado para outro processo; no código de exemplo acima, você pode ver chamadas `read`/`writeStrongBinder()` usadas para ler e escrever em `Parcel`. Em outro processo, quando `readStrongBinder()` é usado, um objeto `BinderProxy` é criado (oculto por trás da [interface `IBinder`](https://developer.android.com/reference/android/os/IBinder)). Então, esse outro processo pode chamar [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) nesse objeto e, no objeto original, `onTransact()` será executado. Normalmente, porém, não se escreve manualmente `transact()`/`onTransact()`, mas [usa-se AIDL](https://developer.android.com/guide/components/aidl) em vez disso

# Entra `LazyValue`, o fim dos `Bundle`s auto-alteráveis

Como no passado houve muitos casos de classes com incompatibilidade entre `writeToParcel`/`createFromParcel`, o Android 13 resolve o problema de qualquer classe desse tipo estar presente em qualquer lugar no sistema permitindo a construção de `Bundle` auto-alterável ao [introduzir `LazyValue`](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/)

Agora, quando `writeValue` é usado, se o valor que está sendo escrito não for primitivo, [o comprimento do valor também é escrito em `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804)

Quando um aplicativo normal usa diretamente `Parcel.readValue()`, [tudo acontece como antes, exceto que um aviso é impresso se o `comprimento` lido de `Parcel` não corresponder ao tamanho dos dados realmente lidos](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804) (Observe que [`Slog.wtfStack` nunca lança exceção](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577))

`Bundle`, no entanto, agora usa [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804) em vez disso

Vamos dar uma olhada mais de perto em como funciona: na classe `LazyValue` temos [um comentário bacana explicando a estrutura dos dados `LazyValue` dentro de `Parcel`](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

mSource é referência ao Parcel original onde readLazyValue() foi chamado

mPosition e mLength descrevem a localização de todo o dado LazyValue no Parcel original, incluindo type e length

"length" (sem "m" no início) refere-se ao valor de comprimento conforme escrito no Parcel e exclui o cabeçalho (type e length)

Então, aqui está o que acontece quando alguém (seja sistema ou aplicativo) obtém um valor de Bundle que foi lido de Parcel:

  1. O chamador usa um dos vários métodos get*() da classe Bundle, por exemplo novo getParcelable() com argumento de tipo (O fluxo será o mesmo para métodos novos e antigos, apenas métodos novos garantem que o argumento clazz não seja null enquanto os legados o definem como null)
  2. unparcel() é chamado, que verificará se este Bundle possui mParcelledData (significa que foi lido de Parcel mas nenhum valor foi acessado ainda e os nomes das chaves ainda não foram descompactados; se não for o caso, pule para o passo 5.)
  3. unparcel() delega para , , é definido como , uma cópia do que o fez e o parâmetro é definido como para indicar que o passado pertence ao e é seguro chamar nele

Se Bundle estiver sendo encaminhado enquanto ainda contém LazyValue (significa que este valor específico não foi acessado, mas algum outro valor desse Bundle foi (ou seja, unparcel() foi chamado, mas LazyValue.apply() para esse item não foi)):

  1. LazyValue é detectado por Parcel.writeValue() e a escrita é delegada para LazyValue.writeToParcel()
  2. LazyValue.writeToParcel() usa out.appendFrom(source, mPosition, mLength) para copiar todos os dados LazyValue do Parcel original (novamente, mPosition e mLength incluem o cabeçalho LazyValue, então isso também copia type e length do original)

Parcel.ReadWriteHelper e Parcel.readSquashed

(Detalhes destes não são importantes para este exploit; a única coisa relevante aqui é que esses mecanismos existem)

Outra característica interessante do Parcel é a capacidade opcional de desduplicar Strings e objetos escritos.

A desduplicação de Strings é feita sobrescrevendo a classe Parcel.ReadWriteHelper: Parcel.readString() na verdade delega para ReadWriteHelper e o helper padrão diretamente lê String do Parcel

Uma implementação alternativa de Parcel.ReadWriteHelper pode substituir chamadas readString por ler um pool de Strings antecipadamente e usar readInt para obter índices de Strings no pool; isso, no entanto, nunca é feito com Parcels controlados pelo aplicativo.

Parcel oferece o método hasReadWriteHelper(), que permite que chamadores detectem a presença de tal mecanismo de desduplicação ativo e desabilitem recursos incompatíveis com ele.

Outro mecanismo de desduplicação disponível em Parcel é o squashing:

  1. Primeiro, o squashing precisa ser habilitado com Parcel.allowSquashing()
  2. Então, quando uma classe que suporta squashing está sendo escrita, ela primeiro chama Parcel.maybeWriteSquashed(this). Se esse método retornou true, significa que o objeto já foi escrito neste Parcel e agora apenas o deslocamento para os dados do objeto anterior foi escrito no Parcel. Caso contrário (ou squashing não está habilitado ou é a primeira vez que este objeto é escrito) maybeWriteSquashed escreve zero como deslocamento para indicar que o objeto não está comprimido e retorna false para indicar ao chamador que eles devem escrever os dados reais do objeto.
  3. Ao ler, Parcel.readSquashed é chamado e a função de leitura real é passada para ele como lambda. readSquashed verifica se o deslocamento escrito por indica que outra ocorrência do objeto foi lida anteriormente: se sim, o objeto lido anteriormente é retornado; caso contrário, a lambda fornecida é chamada para lê-lo agora.

Use-after-Parcel.recycle()

No lado Java, objetos Parcel podem ser reciclados em um pool; ou seja, quando você termina com um Parcel, você chama recycle() nele e da próxima vez que alguém chamar Parcel.obtain(), eles receberão um Parcel reciclado anteriormente. Isso permite reduzir a quantidade de alocações de objetos e subsequente Coleta de Lixo

Por outro lado, esse gerenciamento manual de memória traz a possibilidade de bugs do tipo Use-After-Free no Java (embora com segurança de tipo, ao contrário do Use-After-Free usual em C)

Conforme observado acima, Bundle cria uma cópia do Parcel e não chamará Parcel.recycle() se LazyValue estiver presente; isso, no entanto, não é o caso se Parcel.hasReadWriteHelper() for true; nesse caso:

  1. initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle); é chamado, isso significa que Bundle não reciclará o Parcel, pois ainda pertence ao chamador; no entanto, isso cria LazyValues que se referem ao Parcel original e podem ultrapassar o tempo de vida do Parcel original.
  2. Portanto, a próxima coisa depois disso é chamar unparcel(/* itemwise */ true), que usará getValueAt() em todos os itens para substituir todos os LazyValues presentes no Bundle por valores reais.

Agora, podemos fazer esses LazyValues sobreviverem ao passo 2. e transformar esse comportamento em Use-After-Recycle?

Se a desserialização falhar (por exemplo, a classe com o nome especificado dentro de Parcel não pôde ser encontrada), uma BadParcelableException é lançada e então capturada por getValueAt(). Se o campo estático BaseBundle.sShouldDefuse for true, uma exceção não é levantada e a execução prossegue deixando o Bundle contendo LazyValue referindo-se ao Parcel original. sShouldDefuse indica que valores indisponíveis de Bundle não devem causar exceções em um processo específico e é definido como true em

Se o Parcel original for reciclado e depois disso o Bundle lido dele for escrito em outro Parcel, o conteúdo do Parcel original será copiado para o Parcel de destino, mas nesse ponto o Parcel original pode ser reutilizado para outra coisa e dados de uma operação IPC não relacionada podem ser copiados.

Ok, mas como fazemos Parcel.hasReadWriteHelper() ser true enquanto o Bundle fornecido por nós está sendo desserializado?

Acontece que a classe RemoteViews (normalmente usada, por exemplo, para passar widgets para a tela inicial) define explicitamente ReadWriteHelper ao ler Bundles aninhados nela. Este ReadWriteHelper não faz desduplicação de String e está presente apenas para fazer com que Bundle pule a cópia de dados para um Parcel secundário. A razão para isso ser feito é que RemoteViews habilita squashing a fim de desduplicar objetos ApplicationInfo aninhados nele, mas isso também pode fazer com que objetos ApplicationInfo presentes dentro de Bundle sejam comprimidos, então a leitura desse não pode ser adiada porque então esses objetos comprimidos falhariam ao descomprimir.

Colocando Parcelables em system_server e recuperando-os

Então agora queremos que system_server leia nosso RemoteViews contendo Bundle contendo LazyValue que falha ao desserializar e depois (em outra transação IPC Binder) envie esse objeto de volta para nós

Provavelmente poderia ser feito através de meios legítimos, como nos registrar como host de widget de aplicativo (mas isso exigiria interação do usuário para nos conceder permissão) ou postar uma Notification com contentView definido (mas isso causaria interação com outros processos e/ou seria visível para o usuário e eu preferi evitar ambos)

Em vez disso, decidi criar um MediaSession e chamar setQueue(List<MediaSession.QueueItem> queue) nele para enviar objeto para system_server e depois recuperá-lo através do método List<MediaSession.QueueItem> getQueue() de MediaController (que pode ser obtido através de MediaSession.getController()). Embora esses métodos não pareçam aceitar RemoteViews, na verdade eles aceitam graças ao Java Type Erasure e ao fato de que, internamente, eles são implementados usando operações genéricas de serialização em List

No entanto, não estou usando esses métodos SDK; estou manualmente escrevendo dados para as transações Binder subjacentes (porque preciso escrever e depois ler dados serializados malformados), então vamos dar uma olhada em como esses métodos funcionam.

Ambos os métodos tiveram que levar em conta que o tamanho total da fila pode exceder o tamanho máximo da transação Binder, então a transferência pode ser dividida em várias transações

Enviar "fila" para system_server normalmente ocorre da seguinte forma:

  1. MediaSession.setQueue() primeiro chama ISession.getBinderForSetQueue()
  2. Do lado de system_server, esse método constrói e retorna um objeto ParcelableListBinder
  3. Depois disso, MediaSession.setQueue() chama ParcelableListBinder.send() que enviará o conteúdo da lista para o Binder fornecido, possivelmente em várias transações:
    • A primeira transação contém no início o número total de itens que estarão na lista antes do conteúdo da primeira parte
    • Então, para cada item transferido, escreve-se 1 e o item real é escrito através de (que escreve o nome da classe sendo enviada e então chama para enviar dados)

Recuperar "fila", por outro lado, funciona de maneira um pouco diferente:

  1. MediaController.getQueue() apenas chama ISessionController.getQueue() e desempacota o ParceledListSlice recebido
  2. Do lado de system_server, getQueue() apenas empacota mQueue em ParceledListSlice e retorna
  3. Toda a lógica de divisão em várias transações está dentro dos métodos ParceledListSlice.writeToParcel() e createFromParcel(), em particular, writeToParcel() ao atingir o limite de tamanho seguro escreverá um objeto que permite recuperar próximos pedaços

Quanto ao porquê dessas diferenças: há um esforço contínuo para garantir que system_server não faça chamadas Binder síncronas de saída para outros aplicativos, porque se essas chamadas travassem, poderiam travar todo o system_server. Isso significa que system_server não deveria estar recebendo ParceledListSlices. Embora exista código que alerta sobre transações síncronas de saída de system_server, ainda não pôde ser imposto porque ainda há casos em que system_server faz tais chamadas, por exemplo realmente recebendo ParceledListSlice

Escolhendo o alvo do vazamento

Então agora temos os primitivos necessários para fazer system_server executar parcel_que_sera_enviado_para_nos.appendFrom(algum_parcel_reciclado, posicao_parcialmente_controlada, tamanho_controlado)

Poderíamos tentar aleatoriamente extrair dados de Parcel do sistema ou organizar as coisas para pegar algo específico

Há as seguintes considerações:* Quando Parcel.recycle() é chamado, o conteúdo desse Parcel é limpo. Isso significa que o Parcel do qual gostaríamos de copiar dados não deve ser recycle()ado, o que aproximadamente significa que não podemos pegar dados de uma transação Binder que já foi concluída.

  • Alternativamente, poderíamos pegar dados de algum Parcel de algum Bundle presente no sistema (isso inclui extras de Intent e savedInstanceState de Activity). Geralmente, esses não são recycle()ados (são limpos pelo Garbage Collector e não retornam ao pool; quando o pool se esgota, Parcel.obtain() cria novos objetos Parcel. Claro, os Parcel aos quais estamos mantendo referência não serão coletados pelo GC, mesmo que o sistema não tenha mais uso para eles).
  • Parcels usados para transações Binder de entrada usam um pool separado dos outros Parcels no sistema. Quando uma transação Binder de saída está sendo feita, copia dados para um secundário, ou um aplicativo usa para seus próprios propósitos, eles chamam . Por outro lado, quando há uma transação de entrada, , que . Em ambos os casos, é usado posteriormente e cuida de . Isso significa que o exploit deve fazer com que leia de um pertencente ao mesmo pool do qual gostaríamos de vazar dados. Antes de decidir por uma variante específica, eu escrevi ambas, então você pode encontrar os métodos e na minha classe

No final, decidi tentar capturar o Binder IApplicationThread, que é enviado pelo aplicativo ao system_server quando o processo do aplicativo inicia e o system_server o usa para dizer ao aplicativo quais componentes ele deve carregar

Quando o processo do aplicativo inicia inicialmente, uma das primeiras coisas que ele faz é enviar IApplicationThread para o system_server através da chamada attachApplication() e é dessa transação que vou capturar esse Binder. Existem outros lugares onde IApplicationThread é colocado em um Parcel, como quando é passado para identificação do chamador pelo sistema ao iniciar uma activity (mas eu não tinha muito controle sobre quando o aplicativo alvo faz isso) ou quando é enviado pelo sistema ao aplicativo como parte do gerenciamento do ciclo de vida da Activity (mas isso é feito em transação oneway saindo do system_server e as chances de vencer a corrida contra Parcel.recycle() seriam pequenas)

Dito isso, capturar o Binder que está sendo recebido pelo system_server durante a transação attachApplication() também não é trivial e houve alguns problemas a serem superados

Rebobinando o Parcel

O primeiro problema para capturar o Binder IApplicationThread do Parcel do qual os dados para attachApplication() são recebidos é que esse Binder está em uma dataPosition() bastante inicial/baixa, muito mais baixa do que nosso LazyValue no Bundle dentro de RemoteViews poderia estar

Os dados para a transação attachApplication() consistem apenas de cabeçalho RPC seguido pelo Binder IApplicationThread. O cabeçalho RPC (escrito via Parcel.writeInterfaceToken()) consiste em alguns ints e o nome da interface, neste caso "android.app.IActivityManager"

Enquanto isso, para ler o Bundle embutido em RemoteViews, precisaríamos passar por pelo menos (alguns itens menores são pulados):

  • Flag de presença do item para iniciar readParcelable
  • Nome do Parcelable: "android.view.RemoteViews"
  • Objeto ApplicationInfo bastante grande presente em RemoteViews (também deve não ser null e ter packageName não null ou RemoteViews.writeToParcel() falhará quando tentarmos obter esse objeto para ser enviado de volta)
  • Finalmente chegamos a , que , que constrói , que após finalmente

Agora no Bundle, só precisamos colocar a chave String e a leitura de LazyValue começa, a posição no Parcel é lembrada, mas neste ponto está muito além da posição onde o Binder IApplicationThread estaria

Podemos, ao chegar neste ponto, rebobinar a posição no Parcel? Em outras palavras, poderíamos ter Parcel.setDataPosition() chamado com um valor apontando para uma posição anterior à atual?

Acontece que podemos, graças a outro bug em LazyValue. Este é o código usado para lê-lo:```java public Object readLazyValue(@Nullable ClassLoader loader) { int start = dataPosition(); int type = readInt(); if (isLengthPrefixed(type)) { int objectLength = readInt(); 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); } }

root@kitploit:~
([Original in AOSP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804), construtor `LazyValue` apenas atribui parâmetros a campos)

A questão é que `MathUtils.addOrThrow()` verifica overflow, [mas aceita de boas valores negativos](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)

Se tentássemos `Parcel.writeValue()` em `LazyValue` com `mLength` negativo (preenchido a partir do parâmetro `valueLength`), [isso lançaria em `appendFrom()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804), porém como estamos durante a leitura de `Bundle` com `Parcel.hasReadWriteHelper()` sendo `true`, todos os `LazyValue`s são "unparcelled" após a leitura e tivemos que colocar intencionalmente um `Parcelable` defeituoso dentro dele para fazê-lo permanecer como `LazyValue`. Se colocarmos dados parcelados válidos na posição onde `LazyValue` está, ele será "unparcelled" e, como observado anteriormente, um comprimento incompatível apenas acionará uma mensagem no `logcat`. Este exploit em particular define o tipo como `VAL_MAP` e o número de pares chave-valor como zero. No `logcat`, ao ler esse valor, podemos ver a seguinte mensagem: "`E Parcel  : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP  consumed 4 bytes, but -540 expected.`"

(Além disso, `LazyValue` com comprimento negativo especificado pode ser usado (sem usar outros bugs descritos neste writeup) para criar um `Bundle` que se modifica, aquilo que `LazyValue` foi criado para eliminar. Mas essa é outra história (e relatado separadamente ao Google), neste exploit pretendo algo maior)

Então, quanto queremos retroceder?

Após a chamada `setDataPosition()`, a leitura prosseguirá para o próximo par chave-valor no `Bundle`, então precisamos escolher uma posição onde teremos:

1. Chave do Bundle, lida com `Parcel.readString()`, pode ser basicamente qualquer coisa, inclusive apontando para um comprimento inválido (negativo ou excedendo o tamanho total da `Parcel`); nesse caso, `readString()` retornaria `null`, que é uma chave válida no `Bundle`
2. Tipo do valor, deve ser um dos [tipos para os quais `isLengthPrefixed()` retorna `true`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804)
3. Comprimento do valor, também será um valor controlado por nós; `Parcel.appendFrom()` falhará se o comprimento não estiver alinhado ou exceder o tamanho total da `Parcel` de origem

Então, que posição na `Parcel` poderia ser, considerando que os mesmos dados já foram lidos e são necessários para chegar a este ponto:

* Não antes do nome do `Parcelable` (`"android.view.RemoteViews"`), porque não há espaço suficiente
* Não dentro do nome do `Parcelable`, porque não conseguimos definir o tipo e o comprimento
* Não diretamente após o nome do `Parcelable`, porque a primeira coisa em `RemoteViews` é `mode`, que [devemos definir como `MODE_NORMAL` para alcançar nosso código](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* Não depois disso, porque já passou do ponto onde o `Binder` `IApplicationThread` está

Hmm, não há um bom lugar quando `RemoteViews` é o objeto mais externo nos dados parcelados

Precisamos encontrar algum outro `Parcelable` que:

1. Tenha, no início ou próximo, um lugar onde possamos colocar dados arbitrários (ex.: `int`s ou `String`s que são apenas dados e não afetam o processo de serialização)
2. Possa conter `RemoteViews` (diretamente ou via `readParcelable` arbitrário)
3. Não tenha um nome de classe totalmente qualificado muito longo, porque ainda estamos limitados pelo tamanho da posição onde `IApplicationThread` permanece na `Parcel` alvo

Então peguei a lista de classes `Parcelable` no sistema, ordenei pelo comprimento crescente do nome da classe totalmente qualificado e comecei a verificar os itens dessa lista para ver se satisfaziam a condição 2

Assim cheguei a [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216), que é o que este exploit usa. Agora, o processo de leitura do nosso objeto preparado a partir da `Parcel` ocorre da seguinte forma:

* [Flag de presença do item](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) para iniciar `readParcelable`
* [Nome do `Parcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804): `"android.os.Message"`
* Alguns [`int`s que podemos definir para quaisquer valores que quisermos](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216) são lidos em campos
* Alcançamos a chamada `readParcelable()`, que percorre todo o caminho descrito acima através de `RemoteViews` e começa a ler o `Bundle` com `Parcel.hasReadWriteHelper` sendo `true`
* Esse `Bundle` declara ter dois pares chave-valor. No primeiro valor temos `LazyValue` com comprimento negativo, que aciona `Parcel.setDataPosition()` para a posição onde está a `String` `"android.os.Message"`
* A leitura prossegue para o segundo par chave-valor; a chave é `"android.os.Message"` e o tipo, comprimento e dados do `LazyValue` são retirados dos `int`s descritos no terceiro bullet. Consegui um `LazyValue` com `mPosition` e `mLength` que eu queria. Viva!
* Após a leitura, os `LazyValue`s são "unparcelled". Aquele com tamanho negativo é "unparcelled" com sucesso e substituído por um `Map` vazio, enquanto o outro falha na desserialização, mas essa exceção é capturada e o `LazyValue` apenas permanece no `Bundle`
* `readParcelable()` termina, mas isso não é o fim dos dados de `Message`. `Message.readFromParcel()` agora continua lendo dados após o retrocesso e vê dados que foram inicialmente escritos como parte de `RemoteViews`. Se algo lançar uma exceção neste ponto, todo o plano é frustrado
* Primeira possível exceção: [há uma chamada `readBundle()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216). [`Bundle` tem um valor mágico e se estiver errado, uma exceção será lançada](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). Esse valor mágico, no entanto, não está presente se o comprimento [for zero](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91) ou [negativo](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804) e foi exatamente o caso quando o comprimento dos dados do `LazyValue` foi definido com o valor que eu precisava para capturar `IApplicationThread`. Então tive sorte aqui
* O próximo problema possível poderia ser a [chamada `Messenger.readMessengerOrNullFromParcel()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216). Na verdade, isso é um objeto `Binder` encapsulado. A leitura desse `Binder` falha, porque `Binder` é um objeto especial na `Parcel` e deve ser anotado fora de banda para ser lido. Esse problema é [detectado e registrado pelo `Parcel` no lado nativo, porém isso não é propagado como erro e simplesmente retorna `null`](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)

# Travando `attachApplication()`

Ok, então na etapa anterior criamos com sucesso um objeto que nos permitirá capturar o objeto `IApplicationThread` enquanto o método `attachApplication()` está em execução

O problema é que esse método é concluído rapidamente e nossas chances em uma disputa justa contra sua conclusão seriam bastante pequenas

No entanto, esse método adquire alguns mutexes (através do uso de blocos `synchronized () {}` do Java); se conseguirmos adquirir um desses mutexes e travar ali, esse método também travaria

Agora voltemos a algumas coisas que já foram ditas neste writeup e que serão úteis para este propósito:

* `Bundle` realiza a desserialização dos valores contidos nele quando esses valores são acessados
* Existe a classe `ParceledListSlice` que, durante a desserialização, fará uma chamada `Binder` bloqueante de saída para o objeto especificado dentro dos dados serializados

Somando tudo isso: Se encontrarmos no `system_server` um lugar onde o conteúdo de um `Bundle` fornecido pelo app é acessado sob um mutex que também é usado por `attachApplication()`, conseguiremos travar `attachApplication()` até que a transação `Binder` feita para o nosso processo termine

[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) é uma classe que descreve vários parâmetros relacionados ao início de uma `Activity` (por exemplo, animação). Diferente de outras classes que descrevem parâmetros passados para o `system_server`, esta não implementa `Parcelable`, mas sim fornece um método para convertê-la em `Bundle`

No lado do `system_server`, esse [`Bundle` é convertido de volta para `ActivityOptions`, acionando a desserialização](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). Encontrei um lugar onde [essa operação é feita enquanto o mutex `ActivityTaskManagerService.mGlobalLock` está mantido em `ActivityTaskManagerService.moveTaskToFront()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)

Então chamo [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle)), passando um `Bundle` que contém um `ParceledListSlice` em vez do valor com o tipo esperado. Esse [`ParceledListSlice` faz uma chamada `Binder` para o meu processo](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577) e até que eu retorne dessa chamada, o mutex `ActivityTaskManagerService.mGlobalLock` permanecerá bloqueado

# Criando múltiplos `LazyValue`s apontando para diferentes `Parcel`s

`Parcel.recycle()` e `Parcel.obtain()` funcionam de forma [Último a Entrar, Primeiro a Sair](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))

Isso significa que, se eu criar um `LazyValue` adulterado quando nenhuma outra transação `Binder` para o `system_server` estiver em execução, obterei um `LazyValue` que apontará para a `Parcel` que é sempre usada quando há apenas uma transação chegando ao `system_server` (até que aconteça que duas transações concorrentes cheguem ao `system_server` e terminem em ordem não-pilha)

Como não tenho controle sobre quais outras transações estão chegando ao `system_server`, para melhorar a confiabilidade do exploit, criei múltiplos `LazyValue`s apontando para várias `Parcel`s

Como tenho a capacidade de acionar uma transação `Binder` síncrona para o meu processo a partir do `system_server`, usei essa capacidade para criar `LazyValue`s em vários níveis de recursão entre meu processo e o `system_server` (embora desta vez sem manter um mutex global)

Então:

* Crio um `LazyValue`
* Aciono uma chamada para o `system_server`; o `system_server` me chama de volta
    * Crio um `LazyValue`
    * Aciono uma chamada para o `system_server`; o `system_server` me chama de volta
        * Crio um `LazyValue`
        * Aciono uma chamada para o `system_server`; o `system_server` me chama de volta
            * ...

Depois, quando tenho `LazyValue`s suficientes, paro de fazer isso, retorno de todas essas chamadas e todas as `Parcel`s que foram reservadas por essas chamadas são recicladas (`recycle()`)

Cada um dos `LazyValue`s que criei está encapsulado em um [`ParceledListSlice` separado criado por `getQueue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804) e posso chamar o `Binder` do `ParceledListSlice` para fazer o `system_server` serializá-lo e enviá-lo para o meu processo

(Uma maneira alternativa de fazer isso seria criar múltiplos `MediaSession`s)

# Iniciando o processo do app alvo

Agora temos tudo o que é necessário para capturar `IApplicationThread` de `attachApplication()` quando isso ocorrer, mas ainda precisamos fazer com que `attachApplication()` aconteça

Em geral, [há alguns tipos de componentes de app com os quais outro app pode interagir](https://developer.android.com/guide/components/fundamentals#Components), cada um exigindo que o processo do app seja iniciado

Queria iniciar o aplicativo de Configurações do sistema (que [executa sob o uid do sistema](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) e, portanto, tem acesso a [tudo por trás das permissões do Android](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b))

Inicialmente, tentei iniciá-lo através de `startActivity()`, porém quando tentei, o processo não foi iniciado até que eu liberasse o bloqueio do `ActivityTaskManagerService`. Detalhes sobre por que isso aconteceu estão na seção "Nota adicional: chamadas `Binder` e reentrância de mutexes", mas como solução decidi solicitar ao sistema um [`ContentProvider` desse app](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) em vez de uma `Activity`. Isso teve a vantagem adicional de evitar interferência com minha interface de usuário

Não usei a [API oficial `ContentResolver` exposta pelo SDK](https://developer.android.com/reference/android/content/ContentResolver), mas sim usei uma [interna do sistema, pois precisava de uma API assíncrona](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907) porque a vinculação ao `ContentProvider` não terminaria até que `attachApplication()` (que estou pendurando) terminasse, embora iniciar outra thread pudesse ser uma alternativa

(Não importa o que esse `ContentProvider` específico oferece; a única coisa relevante é que posso estabelecer uma conexão com ele)

Então é assim que inicio o processo do aplicativo de Configurações. Garanto que ele não está em execução em primeiro lugar usando o [método oficialmente disponível `ActivityManager.killBackgroundProcesses()`](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))

# Juntando tudo

As primitivas agora estão descritas, então aqui está como tudo funciona em conjunto (isto é basicamente uma transcrição do método `MainActivity.doAllStuff()` deste exploit):

1. Habilitar acesso a API oculta (APIs ocultas não são fronteira de segurança e existem [soluções alternativas publicamente disponíveis](https://www.xda-developers.com/bypass-hidden-apis/), embora aqui eu tenha usado um método baseado em [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String)), que não vi em outro lugar)
2. (Apenas se estamos executando o exploit novamente após a primeira tentativa) Liberar a conexão com o `ContentProvider` que estabelecemos na etapa 6 durante a execução anterior. Temos que fazer isso, caso contrário `ActivityManager.killBackgroundProcesses()` não considerará o processo alvo como "em segundo plano" e não o matará
3. Matar o processo do aplicativo vítima usando `ActivityManager.killBackgroundProcesses()`, pois `attachApplication()` é chamado apenas na inicialização do processo
4. Solicitar ao `system_server` que crie um monte de objetos contendo `LazyValue` apontando para uma `Parcel` que é posteriormente reciclada. Obtenho a referência `Binder` do `ParceledListSlice` para cada objeto contendo `LazyValue` e posso fazer uma transação `Binder` para ele para acionar o sistema a escrevê-lo de volta. Cada criação de objeto `LazyValue` é feita em diferentes profundidades de [chamadas mutuamente recursivas](https://en.wikipedia.org/wiki/Mutual_recursion) entre o `system_server` e meu app para tornar provável que cada um desses `LazyValue`s tenha uma referência pendente para um objeto `Parcel` diferente
5. Bloqueio `ActivityTaskManagerService.mGlobalLock` fazendo uma chamada para `ActivityTaskManagerService.moveTaskToFront()`, passando como argumento um `Bundle` que, ao ser desserializado, realiza uma transação `Binder` síncrona para o meu processo. Os próximos passos são feitos a partir desse callback e, portanto, são feitos com esse bloqueio mantido
6. Solicito ao `ActivityManagerService` uma conexão com o `ContentProvider` do app vítima (observe que não há "`Task`" no nome; `ActivityTaskManagerService` é uma classe focada principalmente em lidar com componentes `Activity` de apps, enquanto `ActivityManagerService` lida com outros [componentes de app](https://developer.android.com/guide/components/fundamentals#Components) (bem como a inicialização geral de processos); essa [divisão ocorreu no Android 10, anteriormente ambos os tratamentos de `Activity` e outros componentes de app estavam em `ActivityManagerService`](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. `sleep()` um pouco para dar tempo ao processo recém-iniciado de começar a chamar `attachApplication()`
8. Enquanto o bloqueio ainda está mantido, solicito a todos os objetos `ParceledListSlice` criados anteriormente que enviem seus conteúdos restantes (que não couberam na transação inicial), ou seja, objetos contendo `LazyValue` apontando para `Parcel` reciclada. Então, a partir de um deslocamento fixo correspondente à posição de `IApplicationThread` passado para `attachApplication()`, leio o objeto `Binder`. Neste momento, estou apenas salvando os `Binder`s recebidos em um `ArrayList` para evitar fazer muitas operações com o bloqueio mantido
9. Este é o fim do código que executo a partir do callback iniciado na etapa 5. `ActivityTaskManagerService.mGlobalLock` é desbloqueado
10. Tenho o `Binder` `IApplicationThread`. Agora posso simplesmente usá-lo para carregar meu código no app vítima, conforme descrito na próxima seção

# Como uso `IApplicationThread`

Conforme observado anteriormente, o `Binder` [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) é enviado pelo app ao `system_server` quando o processo do app é iniciado e, em seguida, o `system_server` o utiliza para informar ao aplicativo quais componentes ele deve carregar

Presume-se que esse objeto seja passado apenas para o `system_server` e, portanto, não há verificações baseadas em `Binder.getCallingUid()` lá; assim, podemos simplesmente chamar diretamente os métodos oferecidos por essa interface

[Já descrevi em meu writeup anterior como obtenho execução de código manipulando argumentos de `scheduleReceiver()`](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). Agora a situação é a mesma, exceto que desta vez estou chamando `scheduleReceiver()` eu mesmo, enquanto naquela época estava adulterando a interpretação de argumentos de uma chamada feita pelo `system_server`

# Notas adicionais

Nesta seção, descrevo algumas coisas que no final não se mostraram úteis neste caso, embora possam ser características que vale a pena conhecer ou são bugs potenciais

## Nota adicional: `Bundle.clear()`Para simplificar, descrevi aqui o `Bundle` atualizado sem um [commit que foi introduzido posteriormente, que permite que o `Parcel` usado no `Bundle` para dar suporte a `LazyValue`s seja reciclado chamando `Bundle.clear()`](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)

Conforme observado na mensagem do commit, é rastreado se o `Bundle` foi copiado e, nesse caso, `clear()` não reciclará o `Parcel`

No entanto, esse commit também altera a semântica do parâmetro/variável `recycleParcel` de [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)

Anteriormente, `recycleParcel` sendo `false` indicava que o `Parcel` não deveria ser reciclado, seja porque [o chamador definiu `recycleParcel` como `false` para indicar que o `Parcel` não pertence ao `Bundle`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1) ou foi [definido como `false` com base no resultado de `parcelledData.readArrayMap()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)

Agora, as razões pelas quais `recycleParcel` poderia ser `false` são as mesmas, porém a interpretação disso mudou; agora isso não significa "não recicle este `Parcel`", significa "adiar a reciclagem do `Parcel` até a chamada `Bundle.clear()`"

Isso significa que, se `clear()` fosse chamado em um `Bundle` criado com `Parcel.hasReadWriteHelper()` sendo `true`, isso levaria à reciclagem do `Parcel`, enquanto o código que invocou a criação desse `Bundle` também reciclaria esse `Parcel`, resultando em um `recycle()` duplo, que se comporta de forma semelhante a um double-free: as próximas chamadas a `Parcel.obtain()` retornariam o mesmo objeto duas vezes

No entanto, não encontrei uma maneira de fazer com que `clear()` fosse chamado nesse `Bundle`

Desde que escrevi isso originalmente, [o comportamento de `recycle()` foi alterado e agora uma reciclagem adicional é um no-op com possível travamento via `Log.wtf()`](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([dependendo da configuração, mas nunca travando o `system_server`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056)). Eu diria que esse novo comportamento ainda pode ser perigoso, especialmente quando temos a capacidade de interromper programaticamente a desserialização que ocorre em outro processo, mas realmente não há uma boa maneira de lidar com o double-recycle

## Nota adicional: chamadas `Binder` e reentrância de mutexes

Uma característica não muito conhecida do `Binder` é que ele suporta o despacho de chamadas recursivas para a thread original

Ou seja, se o processo A faz uma chamada `Binder` síncrona para o processo B e, em seguida, o processo B, ao tratá-la na mesma thread, faz uma chamada `Binder` síncrona para o processo A, essa chamada no processo A será despachada na mesma thread que está aguardando a conclusão da chamada original para o processo B

Outra coisa é que seções `synchronized () {}` em Java são mutexes reentrantes, o que significa que, se você entrar duas vezes pela mesma thread, ele permitirá a entrada e não causará deadlock

Isso significa que, em teoria, enquanto mantivermos `ActivityTaskManagerService.mGlobalLock` bloqueado, ainda poderíamos iniciar o aplicativo Configurações usando `startActivity(new Intent(Settings.ACTION_SETTINGS))` e entraríamos com sucesso no [bloco `synchronized` que estamos travando](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d), no entanto, iniciar essa `Activity` também envolve a criação de `Task`, que envolve chamar [`notifyTaskCreated()`, que posta uma mensagem](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) para [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547), e [o tratamento disso](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) tenta [adquirir o bloqueio que estamos travando de outra thread](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f). Portanto, até liberar `ActivityTaskManagerService.mGlobalLock`, a thread `DisplayThread` permanecerá bloqueada. Depois, o procedimento de iniciar a `Activity` envolve [postar uma mensagem para a mesma thread para iniciar o processo do aplicativo](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c). Tudo isso significa que, neste caso, o processo do aplicativo não será iniciado até que liberemos o bloqueio e&nbsp;o&nbsp;motivo pelo qual estávamos segurando esse bloqueio em primeiro lugar era para evitar que a transação `attachApplication()` terminasse para podermos capturar handles dela, mas neste caso essa transação não seria realmente iniciada

Mesmo se iniciarmos uma `Activity` que fará parte da mesma `Task` que a atual (ou seja, iniciaríamos uma `Activity` diferente do aplicativo Configurações, uma que não especifica `android:launchMode="singleTask"`), esse procedimento ainda envolverá [`notifyTaskDescriptionChanged()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=444-450;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f), que tem o mesmo impacto aqui que `notifyTaskCreated()`

Portanto, embora minha thread pudesse chamar métodos que usam `synchronized (ActivityTaskManagerService.mGlobalLock) {}`, iniciar um novo processo de aplicativo após `startActivity()` envolveu o uso desse bloqueio de uma thread diferente e isso não foi útil neste caso, então optei por acionar o início do processo do aplicativo através do `ContentProvider`

## Nota adicional: Outras formas de usar `IApplicationThread`

`IApplicationThread` é um handle muito privilegiado, então considero o uso dele após obtê-lo como pós-exploração

Neste exploit, usei-o diretamente para solicitar execução de código no processo alvo, aproveitando o fato de que o acesso a essa operação é controlado por capacidade (posse do objeto `Binder`, que aqui vazamos) e não por `Binder.getCallingUid()`

Adicionar a verificação de `Binder.getCallingUid()` em [`ApplicationThread.scheduleReceiver()` (que usamos aqui para solicitar execução de código)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) e em outros métodos de `ApplicationThread` (já que `scheduleReceiver()` não é o único método em `IApplicationThread` que permite carregamento de código) ainda não impediria o uso de `IApplicationThread` para carregar código no processo de outro aplicativo, pois o atacante poderia passar o `IApplicationThread` vazado no lugar do seu próprio para `attachApplication()`

Além de carregar código no processo, ter `IApplicationThread` permite realizar [`grantUriPermission()` usando privilégios do processo ao qual esse handle pertence](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5631-5632;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)
Baixar ferramenta
unparcel(boolean itemwise)
que chama initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
source
mParcelledData
Parcel
Bundle
recycleParcel
true
Parcel
Bundle
Parcel.recycle()
  • initializeFromParcel chama recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) para ler o conteúdo do mapa chave-valor. As chaves são Strings e os valores são lidos usando readLazyValue(), criando objetos LazyValue para valores de tipos que são escritos junto com o prefixo de comprimento. readArrayMap() retorna um valor indicando se é seguro reciclar o Parcel. Se houver objetos LazyValue presentes, recycleParcel é definido como false e o Parcel ao qual os LazyValue se referem não será reciclado (há uma exceção para isso, mas não é relevante aqui; descreverei na seção "Nota adicional: Bundle.clear()")
  • Uma vez que unparcel() é concluído, mMap é definido (não null) e mapeia chaves String para valores reais, se estiverem prontos, ou objetos LazyValue
  • Depois disso, getValue() é chamado, que mapeia chave (String) para índice (int) e passa para getValueAt()
  • getValueAt() detecta LazyValue através de instanceof BiFunction e chama apply() para desserializá-lo
  • LazyValue.apply() retrocede o Parcel para a posição de LazyValue.mPosition e chama o normal Parcel.readValue() sobre o qual já falei
  • Após a desserialização bem-sucedida, LazyValue é substituído em mMap, para que a próxima chamada Bundle.get*() para a mesma chave retorne diretamente o valor e a desserialização LazyValue não seja repetida. Quando Bundle é encaminhado, esse valor será serializado novamente em vez de ter os dados originais copiados literalmente (no entanto, após o Bundle encaminhado ser lido, esse valor será LazyValue novamente e quaisquer possíveis incompatibilidades writeToParcel/createFromParcel não poderão afetar outros valores)
  • Parcel
    maybeWriteSquashed()
    system_server
    Bundle
    Parcel.writeParcelable()
    Parcelable.writeToParcel
  • Se nos aproximarmos do limite do tamanho da transação Binder, 0 é escrito para indicar que não há mais itens nesta transação e os próximos itens serão enviados em outra transação
  • Uma vez que ParcelableListBinder recebeu o número de elementos especificado na primeira transação, ele invoca a lambda passada para seu construtor, que neste caso atribui a lista recuperada a MediaSessionRecord.mQueue
  • Binder
  • Quando ParceledListSlice é lido de Parcel, ele lê a primeira parte diretamente do Parcel e então se nem todos os elementos foram escritos inline, chama o Binder que foi escrito no Parcel para recuperar esses itens
  • Bundle
    Parcel
    Parcel
    Parcel.obtain(), que usa Parcel.sOwnedPool
    Binder
    o sistema chama Parcel.obtain(long obj)
    usa Parcel.sHolderPool
    Parcel.recycle()
    retornar o objeto Parcel ao pool apropriado
    RemoteViews
    Parcel
    makeOwnedLeaker
    makeHolderLeaker
    ValueLeakerMaker
    RemoteViews.readActionsFromParcel()
    chama getActionFromParcel()
    ReflectionAction
    ler os parâmetros comuns de BaseReflectionAction
    constrói Bundle