
Análise técnica detalhada e prova de conceito para o Android CVE-2022-20474, uma vulnerabilidade de incompatibilidade de Bundle que explora LazyValue com comprimento negativo para alcançar comportamento de Bundle auto-modificável.
Nota importante: antes de ler este artigo, você deve ter uma compreensão básica das vulnerabilidades relacionadas a Bundle Mismatch. Se ainda não leu os seguintes materiais de referência, é recomendável lê-los primeiro:
Recentemente, estava estudando detalhadamente o artigo LeakValue de michalbednarski. Ao discutir com Canyie, ele mencionou que neste artigo também é mencionada uma situação de Bundle auto-alterável no cenário LazyValue. Então fui procurar o texto original e, de fato, havia esse trecho, que eu tinha pulado ao ler o artigo de Michal. O original dizia o seguinte:
(Also
LazyValuewith negative length specified can be used (without using other bugs described in this writeup) to create self-changingBundle, the thingLazyValuewas created to eliminate. But that is another story (and separately reported to Google), in this exploit I'm aiming for more)
Michal provavelmente se refere ao CVE-2022-20474 (boletim, patch). Dei uma olhada no patch, mas a função no link do patch não estava completa. Após completá-la, examinei com mais cuidado:
@@ -4388,6 +4388,9 @@
public Object readLazyValue(@Nullable ClassLoader loader) {
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
int objectLength = readInt();
+ if (objectLength < 0) {
+ return null;
+ }
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);
}
}
O objectLength no código é o length no LazyValue. Na verdade, este é apenas o comprimento do objeto variável contido no LazyValue, enquanto o comprimento total do LazyValue é controlado pelo campo mLength, ou seja, valueLength no código, que é passado para mLength no construtor do LazyValue.
Vamos comparar com o formato de layout do LazyValue:
/**
* | 4B | 4B |
* mSource = Parcel{... | type | length | object | ...}
* a b c d
* length = d - c
* mPosition = a
* mLength = d - a
*/
Com base no conteúdo acima, podemos obter os seguintes fatos:
mLength representa o comprimento total do LazyValue, mLength = objectLength + 8 bytes.objectLength deve ser maior ou igual a 0.mLength, não objectLength, porque a cópia de memória do LazyValue é baseada na cópia de todo o objeto.Então, depois de muita reflexão, concordamos que esses fatos NÃO SERVEM PARA NADA! Porque, com base nos fatos acima, só é possível modificar uma vez durante a leitura, e sabemos que a ideia central do Bundle auto-alterável é modificar após a leitura para contornar as verificações de segurança.
Quando estávamos prestes a desistir, de repente notamos alguns detalhes na descrição do patch:
Addresses a security vulnerability where a (-8) length object would cause dataPosition to be reset back to the statt of the value, and be re-read again.
objectLength anormalEle menciona que quando objectLength é -8, existem alguns problemas, o que nos deu algumas dicas extras. Neste ponto, o LazyValue ainda consegue aplicar normalmente?
@Override
public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) {
Parcel source = mSource;
if (source != null) {
synchronized (source) {
// Check mSource != null guarantees callers won't ever see different objects.
if (mSource != null) {
int restore = source.dataPosition();
try {
source.setDataPosition(mPosition);
mObject = source.readValue(mLoader, clazz, itemTypes);
} finally {
source.setDataPosition(restore);
}
mSource = null;
}
}
}
return mObject;
}
/**
* @see #readValue(int, ClassLoader, Class, Class[])
*/
@Nullable
private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz,
@Nullable Class<?>... itemTypes) {
int type = readInt();
final T object;
if (isLengthPrefixed(type)) {
int length = readInt();
int start = dataPosition();
object = readValue(type, loader, clazz, itemTypes);
int actual = dataPosition() - start;
if (actual != length) {
Slog.wtfStack(TAG,
"Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
+ " consumed " + actual + " bytes, but " + length + " expected.");
}
} else {
object = readValue(type, loader, clazz, itemTypes);
}
return object;
}
Pode-se ver que o readValue real começa a ler a partir de mPosition, depois lê LazyType e objectLength em sequência, e então entra no fluxo normal de leitura de Value. Por exemplo, Parcelable precisa ler ClassName e depois executar createFromParcel. Após a leitura, não há diferença de um Key-Value comum, e não afeta a serialização subsequente. Relembrando: a ideia central do Bundle auto-alterável é modificar após a leitura; isso aqui é apenas uma leitura fora dos limites comum. Parece que esse caminho não funciona.
E se o LazyValue não for apply durante esse processo? Ou seja, se ele continuar participando do IPC como LazyValue, então sua função writeToParcel será chamada:
public void writeToParcel(Parcel out) {
Parcel source = mSource;
if (source != null) {
synchronized (source) {
if (mSource != null) {
out.appendFrom(source, mPosition, mLength);
return;
}
}
}
out.writeValue(mObject);
}
Todo o LazyValue é copiado diretamente, a menos que mLength = 0. Espera! Mencionamos acima que mLength = objectLength + 8 bytes, e pelas informações do patch sabemos que para desencadear a vulnerabilidade, objectLength deve ser -8, então mLength = 0 é válido. Em outras palavras, neste cenário, todo o LazyValue simplesmente desaparece, apenas a String Key é copiada, resultando em uma escrita ausente, e a condição de Bundle auto-alterável é diretamente satisfeita.
Sabendo a causa, podemos começar a reproduzir, mas antes disso, precisamos de mais alguns detalhes.