
Artigo e exploit para CVE-2024-49746: Android Parcel::continueWrite fechando File Descriptors que são usados posteriormente
A correção para este problema apareceu como CVE-2024-49746: boletim, correção
O título acima é o comentário do método Parcel::continueWrite, que é na verdade responsável por redimensionar objetos Parcel, seja quando explicitamente solicitado pelo usuário (por exemplo através de setDataSize()) ou ao chamar um dos métodos write quando a capacidade atual de dados é muito pequena```cpp
status_t Parcel::continueWrite(size_t desired)
{
// SNIP: Validate desired size
// SNIP: Assign kernelFields & rpcFields from variant member of this class
// SNIP: Count number of objects (Binder handles and File Descriptors)
// that will be present after resize and assign to objectsSize
if (mOwner) {
// If the size is going to zero, just release the owner's data.
if (desired == 0) {
freeData();
return NO_ERROR;
}
// If there is a different owner, we need to take
// posession.
uint8_t* data = (uint8_t*)malloc(desired);
// SNIP: Check if malloc succeeded
binder_size_t* objects = nullptr;
if (kernelFields && objectsSize) {
objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
// SNIP: Check if calloc succeeded
// Little hack to only acquire references on objects
// we will be keeping.
size_t oldObjectsSize = kernelFields->mObjectsSize;
kernelFields->mObjectsSize = objectsSize;
acquireObjects();
kernelFields->mObjectsSize = oldObjectsSize;
}
// SNIP: rpcFields handling for non-/dev/binder Parcels
if (mData) {
memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
}
if (objects && kernelFields && kernelFields->mObjects) {
memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
}
// ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
if (kernelFields) {
// TODO(b/239222407): This seems wrong. We should only free FDs when
// they are in a truncated section of the parcel.
closeFileDescriptors();
}
mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
kernelFields ? kernelFields->mObjectsSize : 0);
mOwner = nullptr;
// SNIP: Allocation count tracking
// SNIP: Assign data and objects to this object
} else if (mData) {
// SNIP: Resize data owned by this instance of Parcel
} else {
// SNIP: Allocate initial data for currently empty Parcel
}
return NO_ERROR;
}
[Quando esse comentário foi introduzido](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/), a chamada `closeFileDescriptors()` foi movida de `IPCThreadState::freeBuffer()` (que é chamado no código acima através do ponteiro de função `mOwner()`) para o método `continueWrite()`, porém a lógica permaneceu a mesma. Afinal, `Parcel` é parte central do IPC do Android e se o IPC central estivesse fechando Descritores de Arquivo, não deveria ser um problema óbvio.
O que nos leva à parte importante: quando o código acima é usado? Ele é usado quando a classe Parcel transfere a propriedade dos dados recebidos do driver Binder (que nesse ponto residem no [`/dev/binder` `mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) e não podem ser escritos (qualquer tentativa de escrever nessa memória levaria a `SIGSEGV`)), ou seja, `Parcel` sendo dados de transação recebida (argumento `data` passado para [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))) ou resposta recebida (isto é, objeto `Parcel` que foi passado para a chamada [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) como argumento `reply`, `transact()` define uma referência dentro desse objeto `Parcel`).
Na prática, o único caso em que entraríamos no bloco `if (mOwner)` é quando o sistema [chama `setDataSize(0)` para liberar dados de transação](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567), mas nesse caso também entraríamos em `if (desired == 0)`, que faz um retorno antecipado. Durante o uso legítimo do sistema, não há caso em que entraríamos no caminho "Se há um proprietário diferente, precisamos tomar posse".
# Acionando o caminho "tomar posse"
Em um dos meus exploits anteriores, mostrei [caso em que `createFromParcel()` pode realmente chamar `writeInt(0)` em `Parcel` que deveria estar lendo](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects). Embora a correção lá tenha impedido a execução de qualquer método `createFromParcel()` não-`Intent` dentro de `AccountManagerService`, o caminho de `createFromParcel()` para `writeInt(0)` foi mantido intacto.
Para recapitular, [dentro de `PackageParser` temos o seguinte código](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N);
for (int i = 0; i < N; ++i) {
intentsList.add(cons.newInstance(in));
}
Portanto, podemos ter o objeto Parcel que foi passado para createFromParcel passado para qualquer construtor public disponível no sistema que aceita um único argumento Parcel
E em outro lugar temos o seguinte código:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }
Portanto, para acionar o caminho "take possession", precisamos ter uma chamada `readParcelable` arbitrária em um `Parcel` que foi passado para `onTransact()` como `data`. Neste exploit, estou usando o [mesmo caminho que usei anteriormente em outro](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them). Eu tenho `readParcelable` chamando `PackageParser$Activity.CREATOR.createFromParcel()`, que por sua vez lê o nome `PooledStringWriter` e chama seu construtor, e depois disso os dados do `Parcel` terminam, então `writeInt()` precisa realocar o Parcel, entrando no nosso caminho "take possession"
Vale notar aqui que, se não houvesse fim dos dados do `Parcel` naquele ponto, `writeInt()` tentaria sobrescrever os dados no local, o que, no caso de dados apoiados por `/dev/binder` `mmap`, levaria a `SIGSEGV`
# File Descriptor Sanitizer
Minha ideia inicial era fazer com que o caminho "take possession" fechasse os Descritores de Arquivo, após o que, ao final da transação, os mesmos Descritores seriam fechados novamente, mas entre essas coisas, eu colocaria outro Descritor de Arquivo dentro do `system_server` em outra transação e, posteriormente, recuperaria meu Descritor de Arquivo, já que esse FD se refere naquele ponto a um arquivo diferente
Isso funcionou no meu emulador usando uma versão antiga do AOSP, porém, quando tentei com uma versão mais recente, esse plano foi interrompido pelo [File Descriptor Sanitizer (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md)
Em particular, [na versão `android-14.0.0_r29`, a cobertura do FDSan foi estendida para cobrir FDs dentro de `Parcel`](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)
De fato, depois que o FDSan cobriu o Parcel, eu não conseguia mais nem chegar à chamada de `closeFileDescriptors()` quando o `Parcel` continha FDs. Antes dessa chamada, há uma chamada para `acquireObjects();`, que adquire referências a handles `Binder` (que são liberados pela chamada `mOwner()` mais tarde nessa função), no entanto `acquireObjects()` também irá [definir tags FDSan para FDs](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe):```cpp
case BINDER_TYPE_FD:
if (obj.cookie != 0) { // owned
FdTag(obj.handle, nullptr, who);
}
A questão é que nós já marcamos FDs recebidos do kernel```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }
Portanto, o duplo-`FdTag` (sem fechar ou alterar a tag ao especificar a tag antiga esperada) leva a um erro do FDSan, que aborta o processo, e ainda nem chegamos à chamada `closeFileDescriptors()`. Como o caminho "tomar posse" é código morto durante o uso normal, tais problemas podem passar despercebidos
No entanto, podemos ver a condição `if (obj.cookie != 0)`. Se for falsa, significa que o FD presente dentro de `Parcel` não é realmente de propriedade dessa `Parcel` e é responsabilidade do usuário da `Parcel` mantê-los abertos enquanto essa `Parcel` existir. No entanto, como essa `Parcel` acabou de vir do kernel, os valores `cookie` na verdade vêm do processo original e são considerados irrelevantes quando a `Parcel` realmente possui `mOwner`. Mas o caminho "tomar posse" não considera isso e apenas copia os valores `cookie`
Somando tudo isso, ao definir os valores `cookie` como zero no lado do remetente, podemos obter uma `Parcel` que referencia Descritores de Arquivo que foram fechados, mas também não os considera como próprios, o que significa que não os fechará novamente. Isso nos permite evitar acionar o FDSan, no entanto também elimina quaisquer caminhos de exploração de duplo fechamento
# Truques no lado Java da Parcel
Tais FDs ainda poderiam ser passados para outra `Parcel` (e então para outro processo), no entanto nossa maneira de desencadear a criação de tais FDs pendentes envolve a construção de `PooledStringWriter` através de reflexão, após o que `ClassCastException` é lançada quando tentamos `add()` em `ArrayList<IntentInfo>`
Precisaríamos:
* Estar no final da `Parcel` que foi passada como argumento `data` para `onTransact()`
* Realizar a construção de `PooledStringWriter`, após o que a Parcel terá FDs pendentes, mas também `ClassCastException` é lançada
* Aguardar que os FDs que queremos vazar sejam alocados dentro de `system_server`
* Ter os FDs dessa `Parcel` copiados para alguma outra `Parcel` que seria enviada ao nosso processo
## Truques antigos
Vamos começar revisando truques antigos, a maioria deles já foi descrita no meu exploit [`LazyValue`-using-`Parcel`-after-`recycle()`](https://github.com/michalbednarski/LeakValue)
1. [A classe `RemoteViews` realiza a desserialização do `Bundle` contido com `Parcel.ReadWriteHelper` definido](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4). [Quando `ReadWriteHelper` está definido, os `Bundle`s não são desserializados ansiosamente, mas sim de forma preguiçosa](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545). Vale notar neste ponto que todos os `Bundle`s contidos dentro desse `Bundle` sob `RemoteViews` também serão desserializados ansiosamente, não apenas aquele que está diretamente dentro de `RemoteViews`
2. [Se `BadParcelableException` for lançada ao desserializar `Bundle` dentro de `system_server`, essa `BadParcelableException` será silenciosamente capturada e o conteúdo do `Bundle` será limpo](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e). Observe, porém, que nosso gatilho de escrita em `createFromParcel` lança `ClassCastException`, que não será capturada aqui
3. [A classe `ParceledListSlice` durante a desserialização faz uma chamada Binder de saída bloqueante para o objeto especificado nos dados serializados](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364), que podemos usar para atrasar a execução da desserialização
Todos esses serão necessários agora, mas isso não é tudo
## Novos truques
[AIDL é uma ferramenta para gerar implementação de interface RPC](https://developer.android.com/guide/components/aidl), no entanto, além disso, também é capaz de gerar implementações de estrutura `Parcelable`
Essas estruturas são prefixadas por comprimento, então diferentes versões da mesma estrutura são compatíveis dentro do sistema, desde que nenhum campo seja adicionado no meio (ou seja, as versões são compatíveis se uma versão for prefixo da outra)
Vamos dar uma olhada em qual código AIDL gerou para a estrutura [`ReceiverInfo`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl):```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
int _aidl_start_pos = _aidl_parcel.dataPosition();
int _aidl_parcelable_size = _aidl_parcel.readInt();
try {
if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
data = _aidl_parcel.readString();
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
// SNIP: Other fields
} finally {
if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
throw new android.os.BadParcelableException("Overflow in the size of parcelable");
}
_aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
}
}
Este código nos permitirá fazer os dois truques restantes, ambos necessários para realizar ações adicionais após acionar o caminho "take possession", que exige que estejamos no final do Parcel e lança ClassCastException
Primeiro, estamos lendo o comprimento do Parcel, usando-o para pular campos que não estão presentes na versão que foi escrita e, no final, ajustaremos a posição dentro do Parcel com base nesse comprimento. Não podemos nos mover antes da posição desse Parcelable AIDL (e mover além do final do Parcel é possível, mas não faz nada realmente perigoso). No entanto, podemos nos mover para o meio de um objeto já lido, de modo que durante a primeira leitura possamos chegar ao final do Parcel, ter o PooledStringWriter construído e depois retroceder para o meio de algo que está dentro deste objeto ReceiverInfo
Isso resolve o problema de estar-no-final-do-Parcel, ainda há o problema do ClassCastException. Aqui, porém, temos um bloco finally que, antes de chamar setDataPosition(), valida se não há estouro. Se houver estouro, um BadParcelableException será lançado. Agora, o que acontece se o bloco finally lançar uma Exception quando outra está pendente? A exceção lançada dentro do finally tem prioridade, a Exception anterior desaparece silenciosamente. Agora, em vez de ClassCastException, temos BadParcelableException, que o Bundle ignoram amavelmente para evitar Exception-s dentro do
ParcelableListBinderEstamos usando o ParcelableListBinder do MediaSession para colocar Parcelable arbitrários dentro do system_server e depois recuperar esse objeto
Recentemente houve um patch do ParcelableListBinder que desabilita exatamente isso
Do ponto de vista deste exploit, esse patch não me impede na prática, mas preciso tratá-lo de forma diferente quando presente ou ausente
Se esse patch estiver presente, itens que não são QueueItem são removidos silenciosamente da lista recebida pelo system_server (não causam Exception-s). Como precisamos de itens não-QueueItem para efeitos colaterais, podemos colocar um item não-QueueItem como primeiro item e depois segui-lo com um QueueItem real, o que não nos permite realizar desserialização arbitrária de Parcelable, mas contém um Bundle que pode conter Descritores de Arquivo (que queremos passar para nosso processo)
Se esse patch estiver ausente, não temos esse obstáculo, mas também não podemos usar o mesmo fluxo. Pois no caso acima teríamos uma lista com RemoteViews e QueueItem, então o ParceledListSlice se recusaria a transferir uma lista de tipos mistos. Nesse caso, preciso colocar nossos FDs vazados dentro da instância de RemoteViews
O mais interessante sobre esse patch, no entanto, é o motivo pelo qual foi implementado. A mensagem de commit menciona "permitir que aplicativos iniciem a partir do segundo plano" e, embora o Android Security Bulletin não diga muito, podemos encontrar informações úteis na entrada CVE, que se refere ao campo Notification.mAllowlistToken, que durante a leitura pode ser obtido de um campo estático, que dentro do system_server é o token que permite inícios de Activity em segundo plano e posteriormente esse token seria escrito em Notification.writeToParcel(). Isso significa que agora todos os casos em que o system_server desserializa Parcelable arbitrário e o envia de volta ao aplicativo são vulnerabilidades? De qualquer forma, por enquanto isso é apenas um pensamento; neste exploit quero fazer mais de qualquer maneira
Acho que este exploit apresenta a cadeia de gadgets Parcelable mais complexa que já fiz:
RemoteViews (1)
ReflectionAction (2)
Bundle
Parcelable[] (3)
ReceiverInfo (para seek, 4 e 10)
Intent
ComponentName (segmento A, 5)
ParceledListSlice (11)ParcelableParcel ou QueueItem (12)Bundle (para catch, segmento B, 6)
ReceiverInfo (para rethrow, 7)
Bundle (8)
As anotações "segmento A" e "B" na lista acima referem-se aos blocos entre os comentários "START A"/"END A"/"START B"/"END B" na minha classe FdLeaker.java; os números referem-se aos pontos na lista abaixo
A árvore acima descreve a hierarquia da perspectiva do remetente; da perspectiva do receptor, parece um pouco diferente:
ParcelableListBinder; o objeto mais externo é RemoteViewsRemoteViews há um Bundle aninhado. O RemoteViews irá definir Parcel.ReadWriteHelper para que este Bundle e todos os Bundle-s dentro dele sejam lidos ansiosamente. Isso é necessário porque, caso contrário, não poderíamos executar readParcelable arbitrário de ReceiverInfo.readFromParcel()Parcelable[] é apenas um wrapper de conveniência aqui para agrupar todos os Parcelable-s que estou colocando dentro de Vamos dar uma olhada no ataque acima de um nível alto:
system_server, uma referência a ele é lembrada e o FD é fechadosystem_server para abrir um Descritor de Arquivo diferenteHá uma limitação significativa neste ataque: Não podemos pegar Descritores de Arquivo que foram abertos antes do início do nosso ataque
No entanto, ainda há algumas coisas úteis que podemos fazer
InputChannelPara ser honesto, esta é a única variante do exploit que consegui fazer funcionar sem suposições adicionais
Eventos de entrada, ou seja, eventos de tela sensível ao toque e teclado, são recebidos pelo aplicativo através de um socket UNIX do system_server. Quando uma Activity é iniciada ou uma nova janela é adicionada ao sistema, um novo InputChannel é criado, o que por sua vez significa que um par de sockets UNIX é criado e uma das extremidades é enviada ao aplicativo e a outra é usada pelo system_server para enviar eventos
O layout das estruturas enviadas por esses sockets é bem definido (pois devem ser compatíveis entre processos de 32 e 64 bits) e parece que nada se incomoda com números de sequência inesperados. Além disso, o InputChannel parece ser o único socket alocado dentro do system_server após a chamada startActivity(), então pode ser facilmente determinado qual FD é o socket do lado do servidor do InputChannel

Embora este seja um exemplo de brinquedo, também poderíamos aprovar prompts de permissão ou instalação de aplicativos, ativar Media Projection ou Accessibility Service
zygote durante a inicializaçãoEste é principalmente teórico; consegui realizar este ataque em um emulador lento, mas em um dispositivo real a janela de disputa era muito pequena
O system_server abre uma conexão com /dev/socket/zygote apenas uma vez na inicialização; depois disso, todas as solicitações são enviadas usando essa conexão
Durante a inicialização do system_server, o MediaSessionService (que é usado para enviar e receber Parcelable-s para/de system_server) é publicado no servicemanager antes de a conexão com o zygote ser estabelecida
Portanto, teoricamente é possível que um aplicativo inicie um processo secundário, faça o system_server travar e, a partir desse processo em segundo plano, realize o ataque durante a inicialização do system_server
zygote através do SensorServiceEntão, há outro bug que encontrei; aqui está o método SensorService::createSensorDirectConnection()```cpp
sp SensorService::createSensorDirectConnection(
const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format,
const native_handle *resource) {
// SNIP: Reject direct connections when sensor privacy is enabled
// SNIP: Irrelevant parameter checks
// check specific to memory type
switch(type) {
case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
if (resource->numFds < 1) {
ALOGE("Ashmem direct channel requires a memory region to be supplied");
android_errorWriteLog(0x534e4554, "70986337"); // SafetyNet
return nullptr;
}
// SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
}
case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
// no specific checks for gralloc
break;
default:
ALOGE("Unknown direct connection memory type %d", type);
return nullptr;
}
native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
return nullptr;
}
native_handle_set_fdsan_tag(clone);
sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
// SNIP: usual case where sensor belong to this device (not app streaming)
} else {
auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
} else {
int fd = dup(clone->data[0]);
channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
}
}
// SNIP: Return connection
}
Temos a chamada `dup(clone->data[0])`. `clone` é um `native_handle_t` recebido de um processo remoto. O native handle contém um certo número de FDs e um certo número de inteiros simples dentro dos dados, cujas quantidades o utilizador do native handle deve verificar consultando [`numFds` e `numInts`](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e) antes de aceder a `data`.
Aqui temos até a secção "verificação específica ao tipo de memória", que verifica que, para `SENSOR_DIRECT_MEM_TYPE_ASHMEM` isso é verificado aqui; para `SENSOR_DIRECT_MEM_TYPE_GRALLOC`, o formato do native handle é específico do dispositivo e não pode ser validado aqui. A questão é que, para "sensores em tempo de execução", o tipo é sempre tratado como `SENSOR_DIRECT_MEM_TYPE_ASHMEM`, mas podemos especificar `SENSOR_DIRECT_MEM_TYPE_GRALLOC` para contornar a validação nesse caso.
No entanto, este código só é acessível se "sensores em tempo de execução" estiverem presentes. Não tenho a certeza em que situação isso realmente acontece; penso que é quando o utilizador usa o "Nearby app streaming" (?).
Para testar, no entanto, estou a adicionar uma pequena classe que permite o registo de [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary). Pode usá-la através de```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'
Depois disso, é possível executar código como uid do sistema no dispositivo de produção

system_serverPackageParser$Activity (9)
PooledStringWriterRemoteViewsReceiverInfo mais externo tem um comprimento definido que cai no meio de seus dados, mas durante a leitura isso ainda não é conhecido e o Intent é lido normalmentereadString feita pelo ComponentName.readFromParcel() estático (usando ComponentName porque ele usa readString UTF-16 independentemente da versão do Android; essa readString ainda retornará null porque essa string se sobrepõe a objetos Binder, então, embora a construção do ComponentName tenha pulado dados ocultos desta passagem, o objeto ComponentName não é criadoBundle aninhado; no final da desserialização deste Bundle, um BadParcelableException será capturadoReceiverInfo. Este ReceiverInfo tem comprimento especificado como Integer.MAX_VALUE e, portanto, lançará BadParcelableException no bloco finally, descartando silenciosamente o ClassCastExceptionBundle. Isso é apenas para poder alcançar readParcelable arbitrário de ReceiverInfo, o que agora podemos fazer porque estamos dentro de RemoteViews, então o Bundle é lido ansiosamentePackageParser$Activity + PooledStringWriter aciona uma chamada writeInt(0) no Parcel que está sendo lido. Como estávamos no final do Parcel, writeInt() deve expandir a capacidade do Parcel, acionando o caminho "take possession". Essa combinação também leva a ClassCastException, que é engolido conforme descrito nos passos 7 e 6 acimaReceiverInfo externo; ReceiverInfo busca para a posição de acordo com o comprimento em seu cabeçalho e cai dentro de dados que estavam anteriormente dentro de ComponentName, que são lidos como próximos itens em Parcelable[]ParceledListSlice que realiza uma transação Binder bloqueante para meu processo. Neste ponto, os Descritores de Arquivo definidos neste Parcel foram fechados, mas nada de interessante ocupou seu lugar ainda. Enquanto essa desserialização aguarda o retorno desta chamada, posso fazer o sistema abrir alguns Descritores de Arquivo interessantes que serão então enviados para mimParcelableListBinder filtra itens ou não
a. Se o ParcelableListBinder não filtra itens, os FDs são salvos dentro de ParcelableParcel, que, similar ao Bundle, copia os dados do Parcel literalmente usando Parcel.appendFrom(), mas não tem a lógica especial de hasReadWriteHelper(), então faz isso apesar de estar sob o Bundle de RemoteViews
b. Se o ParcelableListBinder filtra itens, este é o fim de RemoteViews; o objeto RemoteViews é descartado pelo ParcelableListBinder, mas isso é aceitável para mim, pois os efeitos colaterais já ocorreram. O próximo item recebido pelo ParcelableListBinder é QueueItem, que contém MediaDescription, que por sua vez contém Bundle, que é onde meus FDs vazados são mantidos