
Writeup e exploit para CVE-2024-34740, estouro de inteiro no BinaryXmlSerializer do Android para escrita de arquivo no system_server e então execução de código no system_server a partir de um aplicativo instalado normal.

As correções para o problema descrito aqui apareceram sob CVE-2024-34740 / A-307288067:
Dentro do system_server do Android, muitos serviços armazenam seu estado entre reinicializações em arquivos XML```
$ adb shell su 0 find /data/system -name '*.xml' | sort
/data/system/appops_accesses.xml
/data/system/cachequota.xml
/data/system/device_policies.xml
/data/system/device_policy_state.xml
/data/system/display-manager-state.xml
/data/system/input-manager-state.xml
/data/system/inputmethod/subtypes.xml
/data/system/install_sessions.xml
/data/system/job/jobs_1000.xml
/data/system/job/jobs_10131.xml
/data/system/log-files.xml
/data/system/netpolicy.xml
/data/system/notification_policy.xml
/data/system/overlays.xml
/data/system/packages.xml
/data/system/package-watchdog.xml
/data/system/sensor_privacy_impl.xml
/data/system/sensor_privacy.xml
/data/system/shortcut_service.xml
/data/system/users/0/app_idle_stats.xml
/data/system/users/0/appwidgets.xml
/data/system/users/0/package-restrictions.xml
/data/system/users/0/settings_global.xml
/data/system/users/0/settings_secure.xml
/data/system/users/0/settings_system.xml
/data/system/users/0/wallpaper_info.xml
/data/system/users/0.xml
/data/system/users/userlist.xml
/data/system/watchlist_settings.xml
Historicamente, esses eram arquivos XML de texto simples com indentação, o que permitia aos desenvolvedores lê-los facilmente, porém [no Android 12, uma nova versão binária desse formato foi introduzida, citando 1,5% de todo o tempo gasto pelo `system_server` sendo gasto nessas operações XML](https://android.googlesource.com/platform/frameworks/base/+/4ccea8796991d678ead4399130ec31edf63ff4fa%5E%21/)
Deve-se notar que este formato é usado apenas internamente pelo sistema e possui arquivos com valor mágico `"ABX\x00"`. É diferente do [formato usado dentro de APKs](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/ResourceTypes.cpp;l=1770;drc=d4e49e63519397789d284a03aea5fafc119cb1b0) para `AndroidManifest.xml`, `res/xml/*.xml`, `res/layout/*.xml`, etc., que não possui um "valor mágico" explícito, no entanto, geralmente começa com `0300 0800` (que é o [cabeçalho](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h;l=608;drc=d4e49e63519397789d284a03aea5fafc119cb1b0) com `type=RES_XML_TYPE` e `headerSize=8`)
Sempre que o sistema lê um desses arquivos XML de estado interno, ele [usa o valor mágico `"ABX\0"` no arquivo para escolher entre o analisador para arquivos XML Binários ou o analisador XML regular](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;l=188-192;drc=97a370a95275e79c69e79d7ead11aa38934a5575). O fato de esses arquivos serem salvos como XML Binário é [controlado por uma propriedade do sistema](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;drc=97a370a95275e79c69e79d7ead11aa38934a5575;l=74?q=Xml.java) e está habilitado por padrão
Quando arquivos XML Binários estão em uso, você pode ler seu conteúdo, por exemplo, através de `adb shell su 0 abx2xml /data/system/packages.xml -`
Uma das coisas que este formato binário oferece são acessadores tipados, então o serializador oferece o método `attributeInt(String namespace, String name, int value)`, que escreve o valor como um inteiro binário, evitando a ida e volta através de String, o que seria uma nova alocação e posterior objeto para Coleta de Lixo
Outro tipo que pode ser diretamente serializado é o array de bytes```java
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}
Existe também um método semelhante attributeBytesHex que difere apenas pela tag TYPE_* escrita. Essa tag é usada pela ferramenta abx2xml para converter um array de bytes na representação String apropriada.
mOut é uma instância de FastDataOutput, que fornece funções do DataOutputStream do Java. writeByte/writeShort/writeInt/writeUTF/write usam o mesmo formato que o DataOutputStream padrão.
Similarmente ao Parcel, se algo não corresponder durante a escrita/leitura, os dados lidos subsequentemente serão obtidos em offsets errados. No entanto, ao contrário do Parcel, erros no uso de BinaryXmlSerializer/BinaryXmlPullParser não dão ao atacante a capacidade de adulterar arbitrariamente os dados lidos (neste caso, o atacante não pode introduzir novos nomes/valores de tag/atributo).
Erros dentro da própria classe BinaryXmlSerializer ou em FastDataOutput, no entanto, dão essa capacidade.
No método acima, se tentássemos escrever um array de bytes com comprimento 65536, escreveríamos o comprimento com writeShort(), que efetivamente escreveria 0, após o qual o conteúdo real do array será escrito.
Para explorar essa incompatibilidade, precisamos escolher algum arquivo onde possamos injetar um array de bytes arbitrário em attributeBytesBase64 ou attributeBytesHex, e onde a modificação desse arquivo seja valiosa para o atacante.
A classe PackageInstaller oferece a capacidade de preparar um pacote para instalação. Sem necessidade de permissões, qualquer aplicativo pode escrever um novo APK a ser instalado em um diretório temporário. Depois que tudo necessário para a instalação foi escrito, o aplicativo instalador pode commit() PackageInstaller.Session, o que significa que não poderá fazer mais alterações nos arquivos de instalação e a Session está pronta para aprovação do usuário ou instalação real.
O estado dessas operações é armazenado em /data/system/install_sessions.xml. O aplicativo instalador pode, por exemplo, baixar metade de um APK grande para o diretório temporário criado pelo Package Manager Service para sua PackageInstaller.Session, depois reiniciar, retomar o download, escrever a metade restante e confirmar a instalação.
Uma das possibilidades é escrever dados em install_sessions.xml para marcar a sessão como staged, o que significa que será instalada após a próxima inicialização.
Outra possibilidade, apresentada aqui, é alterar o caminho para o diretório temporário no qual os arquivos de instalação são preparados, pois openWrite()/openRead() aceitam qualquer nome de arquivo válido, desde que não haja path traversal e colocam esse arquivo no diretório apontado pelo campo stageDir, que é lido do XML.
Agora precisamos realmente colocar nosso array de bytes controlado em attributeBytesBase64().
A PackageInstaller.Session oferece o método setChecksums().
No lado do system_server, os Checksums fornecidos são opcionalmente verificados contra a assinatura fornecida pelo chamador e então colocados em mChecksums.
Quando install_sessions.xml é escrito, checksum.getValue() é passado para writeByteArrayAttribute, que por sua vez o passa para attributeBytesBase64().
Existem alguns eventos que acionam a escrita de install_sessions.xml, um dos quais é a criação de uma nova Session. Portanto, este exploit, após definir o Checksum em uma sessão, cria uma nova Session para garantir que a primeira Session foi salva no arquivo.
Agora escrevemos um array de bytes com comprimento 65536; então, quando ele é lido, seu tamanho é interpretado como zero e o conteúdo desse array se torna dados brutos que o BinaryXmlPullParser analisa.
Não há contagem de atributos especificada; cada entrada tem um byte de tag que contém um token. No nibble inferior, há um dos tipos de evento definidos em XmlPullParser, como START_TAG, END_TAG ou END_DOCUMENT. Além desses tipos, existe o tipo especial ATTRIBUTE, que não é relatado através de next(), mas sim após ver o token START_TAG, o parser examina os próximos tokens até ver um token não ATTRIBUTE.
Como não há contagem de atributos especificada, podemos prosseguir imediatamente para fechar o elemento atual através do token END_TAG. Em seguida, também fechamos </session>, pois todos os atributos interessantes dos elementos estão na tag de abertura <session>, mas já passamos desse ponto. No entanto, agora podemos abrir um novo elemento <session> e defini-los ali.
Conforme observado acima, FastDataInput é compatível com o DataInputStream do Java, exceto que há um método adicional readInternedUTF(), que pode se referir a Strings passadas. Como não sabemos quais Strings foram internadas anteriormente, sempre especificamos que uma String não vista anteriormente foi escrita. Isso também adiciona as strings recém-lidas ao pool, o que pode causar problemas com a leitura de dados escritos após nosso ponto de injeção. No entanto, como parte da injeção, insiro todas as tags de fechamento e o token END_DOCUMENT, então nada mais será lido desse arquivo após minha injeção.
PackageInstaller.Session com stageDir adulteradoQuando o sistema lê o install_sessions.xml modificado, obtemos um objeto PackageInstallerSession com stageDir definido para um valor controlado por nós.
Minha primeira ideia foi definir stageDir como /proc/self, depois ler maps e escrever mem, mas isso não funcionou.
Quando tentei usar openRead() para abrir /proc/self/maps, o system_server abriu o arquivo com sucesso, no entanto, passar esse arquivo para untrusted_app via Binder foi bloqueado pelo SELinux.
As escritas, no entanto, não são feitas passando o descritor de arquivo bruto para outro processo, mas sim através do system_server, pois o system_server deve ser capaz de revogar o acesso de escrita uma vez que a sessão é confirmada. Isso significa que poderíamos escrever em /proc/self/mem? Acontece que, embora o system_server possa abrir esse arquivo, antes de escrever qualquer coisa, ele chama Os.chmod() nesse arquivo, o que não pode fazer em /proc/self/mem. Portanto, não podemos usar isso para exploração aqui, embora, tirando isso, o system_server seja capaz de abrir esse arquivo e realizar escritas em offsets especificados por nós, e esse arquivo permite sobrescrever páginas de código, o que nos daria diretamente execução de código.
Com isso não sendo uma opção, tentei a próxima ideia: substituir o conteúdo de /data/system/packages.xml. Este é o arquivo que contém o estado do PackageManagerService, principalmente quais aplicativos estão instalados e quais uids são atribuídos a eles.
Parece que o system_server não tem permissão para escrever diretamente nesse arquivo: em vez disso, sempre que o sistema escreve esse arquivo, ele primeiro escreve em um arquivo temporário e depois substitui packages.xml por esse arquivo temporário e ativa a proteção nele.
No entanto, ao ler /data/system/packages.xml, o sistema primeiro verifica se o arquivo /data/system/packages-backup.xml está presente e, se estiver, considera o packages.xml primário corrompido e lê o backup. Durante a operação normal, o arquivo /data/system/packages-backup.xml não está presente e podemos criar um usando uma PackageInstallerSession manipulada com stageDir definido como /data/system.
Além disso, o system_server pode enviar um descritor de arquivo somente leitura de /data/system/packages.xml quando uso openRead(), então posso facilmente construir um arquivo corrigido contendo apenas minhas modificações sem corromper o conteúdo anterior.
sharedUserId="android.uid.system"Em packages.xml tenho definições de aplicativos instalados registrados, como:```xml
<package name="com.android.settings" codePath="/system_ext/priv-app/Settings" ... sharedUserId="1000" ...>
Poderíamos escrever um novo APK em algum lugar em `/data/app` (usando outro `PackageInstallerSession`) e adicionar um novo elemento `<package>` ao `packages.xml` e fazer com que seja instalado dessa forma?
Sim, no entanto devemos fornecer em `<cert>` a assinatura válida do nosso APK recém-instalado e o sistema irá verificá-la em relação ao arquivo APK durante a inicialização
Poderíamos definir o atributo `userId` (em vez de `sharedUserId` para indicar APK sem o atributo `<manifest android:sharedUserId>` no `AndroidManifest.xml`) para o valor que desejamos?
Sim, no entanto não devemos usar um valor que já esteja sendo usado por outro pacote ou `sharedUserId`
Poderíamos definir `sharedUserId="1000"` para o nosso aplicativo?
Se fizermos isso, durante a inicialização o sistema validará essa configuração através de [`canJoinSharedUserId()`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java;l=658-750;drc=7ec13b04c3bbaeac99cbbc4db9f9f80492c508fe)
Em particular, esse método usará [`checkCapability()`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/SigningDetails.java;l=613-637;drc=97a370a95275e79c69e79d7ead11aa38934a5575) para verificar se as assinaturas correspondem exatamente ou se a assinatura de um lado corresponde a uma das assinaturas passadas do outro
Essas "assinaturas passadas" vêm do `packages.xml`, em particular quando temos o elemento `<sigs>` com `<cert>`, podemos adicionar o elemento `<pastSigs>` sob `<sigs>` para adicionar novas entradas a [`SigningDetails.mPastSigningCertificates`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/SigningDetails.java;l=74-87;drc=97a370a95275e79c69e79d7ead11aa38934a5575)
No final, nosso elemento `<shared-user>` adulterado fica assim:```xml
<shared-user name="android.uid.system" userId="1000">
<sigs count="1" schemeVersion="3">
<cert index="3" />
<pastSigs count="2" schemeVersion="3">
<cert index="19" flags="2" />
<cert index="19" flags="2" />
</pastSigs>
</sigs>
</shared-user>
O elemento <cert> sob <pastSigs> é inserido duas vezes porque a última assinatura passada é considerada atual e, portanto, não é considerada
flags="2" significa que o certificado é permitido para sharedUserId
Além disso, o registro de <package sharedUserId="1000"> deve ser aplicado ao aplicativo que declara android:sharedUserId="android.uid.system" no manifesto, portanto, deve ser um APK separado daquele que realiza a exploração
Embora eu tenha conseguido registrar um novo certificado confiável para android:sharedUserId="android.uid.system", normalmente um aplicativo assinado com esse certificado e declarando apenas sharedUserId no manifesto não conseguiria iniciar. Ao iniciá-lo, veremos a seguinte mensagem no logcat:```
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: 'JNI FatalError called: (com.example.abxoverflow.droppedapk) frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:1976: selinux_android_setcontext(1000, 0, "default:privapp:targetSdkVersion=33:complete", "com.example.abxoverflow.droppedapk") failed'
Isto porque nenhuma das definições no arquivo [`seapp_contexts`](https://cs.android.com/android/platform/superproject/main/+/main:system/sepolicy/private/seapp_contexts) correspondeu
A regra `user=` nesse arquivo é [mapeada a partir de `uid`](https://cs.android.com/android/platform/superproject/main/+/main:external/selinux/libselinux/src/android/android_seapp.c;l=819-833;drc=530165a996d8ca5ab5959c33bc040c78951bcb59) (primeiro argumento de `selinux_android_setcontext()`), no nosso caso será `user=system`, para aplicativos normais isso é `user=_app`
A outra coisa a corresponder é a regra `seinfo=`, que é obtida do 3º argumento de `selinux_android_setcontext()` até o primeiro dois-pontos. Originalmente esse valor vem da comparação da assinatura do aplicativo iniciado com as definidas em `/system/etc/selinux/plat_mac_permissions.xml`
No final, nosso aplicativo tenta corresponder a `user=system seinfo=default` e não existe tal regra em `seapp_contexts`
No entanto, embora o processo para nosso novo aplicativo com `android:sharedUserId="android.uid.system"` não possa ser iniciado, o aplicativo ainda pode ser carregado em um processo existente se especificado através do [atributo `android:process`](https://developer.android.com/guide/topics/manifest/application-element#proc). Em particular, aplicativos executando sob `android.uid.system` podem especificar `android:process="system"` para serem carregados em `system_server`
# Travando o sistema
Em geral, [um aplicativo causando travamento do `system_server` é considerado um bug com Impacto de Segurança Negligenciável](https://bughunters.google.com/learn/invalid-reports/android-platform/5148417640366080/bugs-with-negligible-security-impact#triggering-a-local-temporary-denial-of-service) e aqui vale apenas notar porque faz parte de uma cadeia de exploração que requer duas reinicializações do `system_server`
De qualquer forma, temos a cadeia `Parcelable`:
* [O método AIDL `IAlarmManager.set()` aceita `AlarmManager.AlarmClockInfo`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl;l=32-35;drc=ca41ed611ac9c6584c6d5c38ae8428b8e4f3b135)
* [`AlarmClockInfo` chama o método obsoleto `readParcelable()` sem argumento de tipo](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/apex/jobscheduler/framework/java/android/app/AlarmManager.java;l=1598;drc=04bf84e220ade9d7ad8ef0b2f7e6ce6ec72841c8) (porque está em um módulo apex e estes não foram migrados para os novos métodos)
* Eu especifico [`android.content.pm.PackageParser$Activity`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=8240;drc=7d3ffbae618e9e728644a96647ed709bf39ae759) como classe `Parcelable`
* A leitura disso leva à [invocação de qualquer construtor público que aceite um único argumento `Parcel`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759)
* Eu especifico [`android.os.PooledStringWriter`, que chama `writeInt(0)` no `Parcel` fornecido](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=55;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb)
* Essa chamada `writeInt()` foi feita no `Parcel` recebido como argumento `data` de [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)), que é apoiado por memória somente leitura `mmap`-ada de `/dev/binder`. Escrever nisso causa `SIGSEGV`
Também digno de nota, [usei a combinação `PackageParser`+`PooledStringWriter` como parte de relatórios anteriores, por exemplo para CVE-2023-21098](https://github.com/michalbednarski/TheLastBundleMismatch)
# O fluxo completo
Isto é o que acontece quando você pressiona o botão "Do everything" dentro do aplicativo
1. `RebootBackgroundRunner` é iniciado como um processo separado, que agora usará apenas [`setsid()`](https://man7.org/linux/man-pages/man2/setsid.2.html) para sobreviver à reinicialização do userspace e depois disso aguardará em segundo plano
2. Uma nova `PackageInstaller.Session` é alocada e um novo objeto `Checksum` é adicionado a ela. Esse objeto `Checksum` contém um array de bytes com tamanho que causará estouro de inteiro durante a serialização e, uma vez que seus dados sejam desserializados de volta, o sistema verá `PackageInstaller.Session`-s cujos dados eram anteriormente o payload do `Checksum`. Em particular, duas sessões são injetadas
* Uma com `sessionStageDir="/data/system"` e `prepared="true"` (significando que o diretório de stage já está pronto e não precisa ser criado)
* Uma com `sessionStageDir="/data/app/dropped_apk"` e `prepared="false"` (significando que o diretório será criado na primeira `Session.openWrite()`)
3. Uma nova `PackageInstaller.Session` é alocada e imediatamente destruída. Isso aciona o sistema para escrever conteúdos atualizados em `install_sessions.xml`
4. Após um pequeno atraso, uma travamento do `system_server` é acionado
5. Durante a próxima inicialização do `system_server`, o arquivo `install_sessions.xml` é lido e agora as `PackageInstaller.Session`-s que injetamos podem ser usadas
6. `RebootBackgroundRunner` esteve aguardando em segundo plano durante a reinicialização do userspace e, uma vez que percebe que o sistema está de volta e pronto, realiza os próximos passos
7. Usando uma `PackageInstaller.Session`, um novo APK é extraído de assets e escrito em `/data/app/dropped_apk/base.apk`
8. A outra sessão é usada para ler `/data/system/packages.xml`, esse arquivo é corrigido para declarar que o APK recém-removido já foi instalado e que o certificado usado para ele foi usado anteriormente para `android:sharedUserId="android.uid.system"` e ainda é confiável para esse propósito. O arquivo alterado é escrito como `/data/system/packages-backup.xml`
9. Outro travamento do `system_server` é acionado
10. Quando o `system_server` durante a inicialização vê `packages-backup.xml`, ele considera o `packages.xml` original como corrompido e usa o backup
11. Como o sistema leu o `packages.xml` modificado, o aplicativo recém-removido está presente e se inicia a partir de [`ACTION_BOOT_COMPLETED`](https://developer.android.com/reference/android/content/Intent#ACTION_BOOT_COMPLETED). Esse novo aplicativo executa dentro do `system_server` porque possui `<manifest android:sharedUserId="android.uid.system">` e `<application android:process="system">` no `AndroidManifest.xml`
# Scripts em `utils/`
Junto com o aplicativo PoC há um diretório `utils` com alguns scripts
* `moveapk.sh` move o APK compilado para ser solto em `assets` do dropper, para ser executado após `gradle :droppedapk:assembleRelease`
* `peeksessions.sh` permite visualizar o conteúdo atual de `install_sessions.xml` (requer build `eng`/`userdebug` do Android)
* `wipesessions.sh` limpa quaisquer `PackageInstaller.Session`-s presentes e reinicia o sistema (requer build `eng`/`userdebug` do Android)
# Curiosidades
Não sei se isso está relacionado, mas olhando o histórico de possíveis bugs relacionados a ABX (`cd frameworks/base ; git log -S ABX`) encontrei o commit ["Stop processing on IOException"](https://android.googlesource.com/platform/frameworks/base/+/5112cfef2a2023a2629a426154547444593e9f9b%5E!/), que **inclui a adição de um teste de unidade com arquivo ABX truncado**. Esse commit foi sequência de ["Ignore malformed shortcuts"](https://android.googlesource.com/platform/frameworks/base/+/d5122bfaf18f1503e73c1a3a177a56d0f604a008%5E%21/), que foi [descrito no boletim como DoS](https://source.android.com/docs/security/bulletin/2022-12-01#framework)