
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