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
ReparcelBug2 — Writeup e exploit para escalonamento de privilégios de aplicativo instalado para sistema no Android 12 Beta através do CVE-2021-0928, uma incompatibilidade de serialização `writeToParcel`/`createFromParcel` em `OutputConfiguration` | Kitploit
Ferramentas/GitHubGitHub/michalbednarski/reparcelbug2
Segurança AndroidEscalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoSegurança MóvelAnálise de Binários
GitHubmichalbednarski/reparcelbug2

ReparcelBug2

Writeup e exploit para escalonamento de privilégios de aplicativo instalado para sistema no Android 12 Beta através do CVE-2021-0928, uma incompatibilidade de serialização `writeToParcel`/`createFromParcel` em `OutputConfiguration`

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
Ver Repositório
12323há 4 anosRevisado pelo Kitploit

CVE-2021-0928, incompatibilidade de serialização writeToParcel/createFromParcel em android.hardware.camera2.params.OutputConfiguration

Este é um exploit que usa essa vulnerabilidade para escalonamento de privilégios de um app Android instalado para o app de Configurações do Android (ou qualquer outro app instalado para o qual o app pudesse enviar para <receiver> declarado em AndroidManifest.xml; o escalonamento de privilégios enviando para <activity> também era possível, embora não apresentado aqui)

Encontrei o problema originalmente no Android 12 Developer Preview 3

A versão do exploit presente neste repositório funciona no Android 12 Beta 2 e 3

A vulnerabilidade foi corrigida no primeiro lançamento oficial do Android 12

O writeup abaixo foi originalmente escrito para o Google para consideração deste relatório como cadeia de exploit completa

No momento da escrita, o Android 12 não estava disponível no AOSP (as versões Android Developer Preview/Beta não são open source)

Captura de tela da notificação do Android do app de Configurações: Hello from uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0

Introdução ao Parcel

A maior parte da IPC no Android é feita através da classe chamada Parcel

O uso básico do Parcel é o seguinte:```java Parcel p = Parcel.obtain(); p.writeInt(1); p.writeString("Hello");

root@kitploit:~
Então, o `Parcel` é enviado para outro processo [através do `Binder`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Alternativamente, para fins de teste, pode-se chamar [`p.setDataPosition(0)`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)) para rebobinar o parcel até a posição inicial e começar a leitura:```java
int a = p.readInt(); // a = 1
String b = p.readString(); // b = "Hello"

Deve-se notar que o Parcel mantém internamente a posição a partir da qual as leituras são realizadas. É responsabilidade do usuário da classe Parcel garantir que os métodos read* correspondam aos métodos write* usados anteriormente; caso contrário, as leituras subsequentes serão feitas a partir de posições erradas no buffer.

O Parcel também oferece a capacidade de escrever objetos personalizados; a forma preferida de fazer isso é implementando a interface Parcelable.

Aqui está um exemplo de implementação da interface Parcelable (código irrelevante removido; a classe WindowContainerTransaction é usada no exploit como parte da cadeia de gadgets, no entanto não há nada de errado com ela):```java package android.window; public final class WindowContainerTransaction implements Parcelable { private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>(); private final ArrayList mHierarchyOps = new ArrayList<>();

root@kitploit:~
private WindowContainerTransaction(Parcel in) {
    in.readMap(mChanges, null /* loader */);
    in.readList(mHierarchyOps, null /* loader */);
}

@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
    dest.writeMap(mChanges);
    dest.writeList(mHierarchyOps);
}

@NonNull
public static final Creator<WindowContainerTransaction> CREATOR =
        new Creator<WindowContainerTransaction>() {
            @Override
            public WindowContainerTransaction createFromParcel(Parcel in) {
                return new WindowContainerTransaction(in);
            }
        };

}

root@kitploit:~
Como pode ser visto acima, o método `writeToParcel()` é usado durante a escrita. Em seguida, durante a leitura, o método de fábrica `CREATOR.createFromParcel()` é chamado. É responsabilidade da implementação de `Parcelable` garantir que `createFromParcel` leia a mesma quantidade de dados que foi escrita por `writeToParcel`; caso contrário, todas as leituras subsequentes desse `Parcel` lerão dados de um deslocamento incorreto

Essa classe pode ser gravada/lida de/para `Parcel` através de:

* Chamando diretamente `obj.writeToParcel(parcel, 0)` / `obj = WindowContainerTransaction.CREATOR.createFromParcel()`, isso é frequentemente usado quando o tipo da classe é conhecido, por exemplo, quando `Parcelable` tem um campo com um `Parcelable` diferente ou em código gerado por AIDL quando o método RPC definido tem `Parcelable` como argumento
* Através de `Parcel.writeParcelable`/`readParcelable`. [`writeParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=1909;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) primeiro grava o nome da classe e depois chama o método `writeToParcel` da interface `Parcelable`. [`readParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3282;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) lê o nome da classe gravado, encontra a classe com esse nome no `ClassLoader` fornecido ou no `BOOTCLASSPATH` se null for fornecido. Uma vez que a classe é encontrada, seu campo estático `CREATOR` é usado para obter uma instância de [`Parcelable.Creator`](https://developer.android.com/reference/android/os/Parcelable.Creator), que é a fábrica usada para ler essa classe. É importante notar que quando o método `readParcelable` é usado, ele pode ler qualquer `Parcelable` disponível no classpath, pois o nome do objeto a ser criado é lido do mesmo `Parcel`
* `readParcelable` é usado por muitos outros métodos de `Parcel`, por exemplo, `readList` visto no exemplo acima lê elementos através de `readValue`, que é o método mais genérico de transferir objetos em `Parcel` e uma das formas que ele usa é através de `readParcelable`. Além disso, no exemplo acima, devido ao Type Erasure do Java, o campo `ArrayList<HierarchyOp> mHierarchyOps` pode na verdade conter quaisquer objetos suportados por Parcel, não apenas aqueles compatíveis com o tipo especificado na declaração do tipo genérico

# Incompatibilidades de `writeToParcel`/`createFromParcel`

Como observado acima, é responsabilidade da implementação da interface `Parcelable` garantir que `createFromParcel` leia a mesma quantidade de dados do `Parcel` que o `writeToParcel` correspondente gravou anteriormente. Sempre que houver no `BOOTCLASSPATH` um `Parcelable` que possa violar esse contrato, isso cria uma vulnerabilidade, pois permite o seguinte cenário:

1. Um aplicativo malicioso envia ao `system_server` um `Bundle` OU `Parcelable` contendo uma instância defeituosa de `Parcelable` juntamente com dados especificamente construídos que serão realmente lidos no passo 3, mas passados verbatim durante o passo 2
2. O `system_server` verifica que o `Bundle` é seguro e o encaminha OU o `system_server` passa o `Parcelable` fornecido para um método AIDL que também tem dados críticos passados no próximo parâmetro (se os dados recebidos nesse parâmetro pudessem ser modificados, isso causaria um problema de segurança)
3. Outro aplicativo recebe dados do `system_server` e confia neles, porém, devido à serialização defeituosa, os dados que ele realmente vê diferem dos dados que o `system_server` pretendia enviar

Usei "OU" nos passos acima, pois esses passos descrevem tanto uma [variante antiga de exploit que leva a iniciar uma Activity arbitrária, que publiquei em 2017](https://github.com/michalbednarski/ReparcelBug) (no lado esquerdo do "OU") quanto uma nova variante que descreverei aqui na próxima seção

# Como `BroadcastReceiver` é executado no aplicativo

Do ponto de vista do desenvolvedor de aplicativos que usa as APIs disponíveis no Android SDK, a forma como [`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) funciona é que um aplicativo chama [`sendBroadcast`](https://developer.android.com/reference/android/content/Context#sendBroadcast(android.content.Intent)) (embora muitas vezes os aplicativos queiram receber broadcasts do sistema, não de aplicativos) e então o Intent transmitido é correspondido ao `<receiver>` definido no `AndroidManifest.xml`; quando isso acontece, o sistema inicia o processo do aplicativo receptor, instancia a subclasse de `BroadcastReceiver` conforme definido no atributo `<receiver android:name>` e então chama o método [`onReceive`](https://developer.android.com/reference/android/content/BroadcastReceiver#onReceive(android.content.Context,%20android.content.Intent))

Vamos dar uma olhada na comunicação com o `system_server` que ocorre no processo que recebe o broadcast:

* Quando o processo do aplicativo é iniciado inicialmente, ele [chama `IActivityManager.attachApplication()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7340;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c), ao fazer isso ele passa o handle de [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl), que é usado pelo sistema para dizer ao processo do aplicativo o que fazer
* Quando o sistema quer executar um `BroadcastReceiver` registrado no manifest no processo do aplicativo, ele chama o método [`scheduleReceiver`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=950;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c) usando o `IApplicationThread` descrito no ponto anterior. Este método tem vários argumentos, mas aqui os mais importantes são os dois primeiros:
  1. `Intent intent`, que foi previamente passado ao sistema quando `sendBroadcast()` foi chamado
  2. `ActivityInfo info`, que contém informações sobre o componente que deve ser executado. O valor para este parâmetro é obtido pelo sistema do Package Manager Service. Mais importante, os dados passados neste parâmetro incluem o caminho para o arquivo do qual a classe Java que manipula o broadcast recebido será carregada

Neste ponto, você provavelmente pode adivinhar qual é esse novo caminho de exploit: chamar `sendBroadcast()` passando um `Intent` que fará com que, quando o sistema tentar chamar `scheduleReceiver`, isso cause que o aplicativo no qual `scheduleReceiver` é invocado veja um `ActivityInfo` adulterado

Deve-se notar que esse novo caminho de exploit se tornou viável no Android 12, pois anteriormente não havia como colocar `Parcelable`s arbitrários em um `Intent` ([extras de Intent](https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable)) não contam, pois são colocados em um `Bundle` que tem seu comprimento total gravado no Parcel e é [lido como um único blob](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1671-1681;drc=5d123b67756dffcfdebdb936ab2de2b29c799321), então extras não podem causar má interpretação do objeto `Intent` que os contém)

# Acionando a incompatibilidade de `writeToParcel`/`createFromParcel`

Na maioria das vezes, incompatibilidades de `writeToParcel`/`createFromParcel` são casos em que, em um desses métodos, um dos campos é esquecido ou gravado duas vezes; nesse caso, enviar tal objeto sempre acionará a incompatibilidade. (Na maioria das vezes isso acontece quando o objeto, embora seja `Parcelable`, não é realmente usado entre processos; caso contrário, isso seria rapidamente notado durante o uso normal)

Desta vez, porém, esse não foi o caso e acionar a incompatibilidade não é óbvio

Vamos dar uma olhada na classe vulnerável ([original estava aqui](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/hardware/camera2/params/OutputConfiguration.java;drc=46c390a1c695e2dc458cb889e40559f259f60aed), linhas marcadas com `// New in Android 12` foram adicionadas manualmente, pois não estavam presentes no AOSP no momento da escrita) ([Aqui está o commit que originalmente introduziu a vulnerabilidade](https://android.googlesource.com/platform/frameworks/base/+/a69b1bc58b0838e06deefb190e226774a34671e6%5E%21/#F10), porém foi publicado após o lançamento do Android 12)```java
package android.hardware.camera2.params;

public final class OutputConfiguration implements Parcelable {
    private OutputConfiguration(@NonNull Parcel source) {
        int rotation = source.readInt();
        int surfaceSetId = source.readInt();
        int surfaceType = source.readInt();
        int width = source.readInt();
        int height = source.readInt();
        boolean isDeferred = source.readInt() == 1;
        boolean isShared = source.readInt() == 1;
        ArrayList<Surface> surfaces = new ArrayList<Surface>();
        source.readTypedList(surfaces, Surface.CREATOR);
        String physicalCameraId = source.readString();
        boolean isMultiResolution = source.readInt() == 1; // New in Android 12
        ArrayList<Integer> sensorPixelModesUsed = new ArrayList<Integer>(); // New in Android 12
        source.readList(sensorPixelModesUsed, Integer.class.getClassLoader()); // New in Android 12

		// SNIP: copy values from variables set above to fields of this class
    }

    public static final @android.annotation.NonNull Parcelable.Creator<OutputConfiguration> CREATOR =
            new Parcelable.Creator<OutputConfiguration>() {
        @Override
        public OutputConfiguration createFromParcel(Parcel source) {
            try {
                OutputConfiguration outputConfiguration = new OutputConfiguration(source);
                return outputConfiguration;
            } catch (Exception e) {
                Log.e(TAG, "Exception creating OutputConfiguration from parcel", e);
                return null;
            }
        }

        @Override
        public OutputConfiguration[] newArray(int size) {
            return new OutputConfiguration[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mRotation);
        dest.writeInt(mSurfaceGroupId);
        dest.writeInt(mSurfaceType);
        dest.writeInt(mConfiguredSize.getWidth());
        dest.writeInt(mConfiguredSize.getHeight());
        dest.writeInt(mIsDeferredConfig ? 1 : 0);
        dest.writeInt(mIsShared ? 1 : 0);
        dest.writeTypedList(mSurfaces);
        dest.writeString(mPhysicalCameraId);
        dest.writeInt(mIsMultiResolution ? 1 : 0); // New in Android 12
        dest.writeList(mSensorPixelModesUsed); // New in Android 12
    }

    private ArrayList<Surface> mSurfaces;
    private final int mRotation;
    private final int mSurfaceGroupId;
    private final int mSurfaceType;
    private final Size mConfiguredSize;
    private final int mConfiguredFormat;
    private final int mConfiguredDataspace;
    private final int mConfiguredGenerationId;
    private final boolean mIsDeferredConfig;
    private boolean mIsShared;
    private String mPhysicalCameraId;
    private boolean mIsMultiResolution; // New in Android 12
    private ArrayList<Integer> mSensorPixelModesUsed; // New in Android 12
}

Então, o que está errado aqui e por que essa mudança introduz uma vulnerabilidade? Como eu disse ao discutir o exemplo de Parcelable WindowContainerTransaction, o readList pode, na verdade, preencher a lista com qualquer objeto suportado por Parcel, não apenas aqueles que correspondem à declaração genérica (ArrayList<Integer>). No entanto, como estamos apenas usando essa classe como parte de uma cadeia de gadgets de serialização e não a estamos usando de fato, esse campo não será usado para nada além de ler e escrever no Parcel (e tentativas de usar elementos de um ArrayList contendo elementos que não correspondem à sua declaração genérica levariam a uma ClassCastException de qualquer forma), isso não é um problema em si.

Nesta classe também há um try-catch dentro de createFromParcel, o que significa que se uma Exception for lançada durante a leitura, a leitura de OutputConfiguration será interrompida e a leitura do objeto que contém OutputConfiguration prosseguirá. Quando isso acontece, todo o OutputConfiguration será gravado no Parcel, mas será lido apenas até o ponto em que a Exception ocorreu. Isso cria uma incompatibilidade, pois os dados não consumidos gravados dentro de OutputConfiguration.writeToParcel serão, na verdade, lidos pelo objeto que estava chamando OutputConfiguration.CREATOR.createFromParcel.

Agora, a combinação desses dois fatores (permitir aninhar objetos arbitrários suportados por Parcel e envolver isso em um try-catch sem relançar) dá a capacidade de construir um Parcelable que pode ser gravado pelo system_server e posteriormente lido de uma forma controlada pelo aplicativo que inicialmente construiu o Parcelable que está sendo encaminhado pelo system_server.

Ok, então, para construir tal Parcelable, agora precisamos encontrar algo para colocar em mSensorPixelModesUsed que seja lido com sucesso no system_server (já que este objeto está sendo recebido do aplicativo atacante através do Parcel), gravado com sucesso pelo system_server e, em seguida, falhe ao desempacotar e lance uma Exception no aplicativo vítima.

Uma das maneiras de fazer isso é usar uma classe que esteja presente dentro do system_server, mas não nos aplicativos, de modo que tentar desserializá-la leve a uma ClassNotFoundException. No entanto, não posso escolher um Parcelable do system_server como readParcelable sem um ClassLoader explicitamente especificado, pois ele só pesquisará no BOOTCLASSPATH, que não conterá classes específicas do system_server. A solução para esse problema é usar uma das classes Serializable, pois o ObjectInputStream escolherá o ClassLoader do primeiro método não-BOOTCLASSPATH no stack trace.

Escolhi PackageManagerException, no entanto, antes de usá-la, há mais uma coisa que precisamos fazer. No construtor de OutputConfiguration, quando readList é chamado, o argumento loader é explicitamente definido como Integer.class.getClassLoader(). Esse valor de loader é propagado para readValue(), depois para readSerializable() e dentro de readSerializable(), se o parâmetro loader não for nulo, ele é usado em vez de do (essa verificação não faz nada porque quando não encontra a classe, ela lança uma exceção em vez de retornar nulo). A maneira de contornar isso é bastante simples, apenas precisamos envolver em algum que faça sem especificar um . É aqui que entra a classe descrita acima.

Então, neste ponto, temos o seguinte objeto:

  • OutputConfiguration
    • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
      • mHierarchyOps.get(0) = PackageManagerException

Agora, tal objeto pode ser desserializado com sucesso dentro do system_server: quando WindowContainerTransaction chama readList, ele tentará encontrar a classe PackageManagerException usando o ClassLoader do servidor de sistema (não o BootClassLoader), pois pode encontrá-lo no stack trace. Esse class loader está presente no stack trace porque, embora todos os métodos a seguir não fossem do classpath do servidor de sistema: Binder#execTransact(), IActivityManager$Stub#onTransact() gerado por AIDL e métodos de todas as classes Parcelable usadas, havia um método declarado dentro do servidor de sistema no stack trace: um onTransact sobrescrito dentro de ActivityManagerService. Portanto, o pode ler e posteriormente gravar tal objeto no , e quando o aplicativo de destino tentar lê-lo, a classe não estará disponível e, portanto, uma será lançada, e então .

Então, acionamos essa incompatibilidade. Bem, na verdade ainda não neste ponto, porque a exceção foi capturada quando não havia dados não lidos deixados por OutputConfiguration.writeToParcel, mas podemos facilmente adicionar outro item à List mSensorPixelModesUsed, e esse item será gravado através de Parcel.writeValue e deixado não lido após a leitura de OutputConfiguration.

Colocando isso em Intent

Como observado acima, quero acionar a incompatibilidade a partir do objeto Intent, pois ele será passado pelo system_server para um método AIDL que tem Intent no primeiro parâmetro e informações de execução no segundo parâmetro, de modo que a serialização/desserialização do Intent passado no primeiro parâmetro leve à modificação do valor no segundo parâmetro.

Em Intent.readFromParcel(), todos os valores são lidos através de métodos tipados dedicados, então não podemos especificar uma classe Parcelable personalizada lá.

Dentro de Intent, no entanto, há um ClipData aninhado e, desde o Android 12, em ClipData$Item há um novo campo ActivityInfo mActivityInfo (não estava presente no AOSP no momento da escrita inicial, aqui está o commit que introduz esse campo, este campo é lido através de in.readTypedObject(ActivityInfo.CREATOR) dentro do construtor ClipData(Parcel in)).

Então, dentro do construtor ActivityInfo(Parcel source), novamente não há como colocar um Parcelable personalizado, mas como ActivityInfo estende ComponentInfo, ele tem o campo applicationInfo.

Finalmente, dentro de ApplicationInfo, há o campo SparseArray<int[]> splitDependencies, que é lido através de readSparseArray, que por sua vez usa readValue para ler os itens do SparseArray.

Neste ponto, poderíamos colocar OutputConfiguration dentro de splitDependencies, no entanto, a leitura de splitDependencies é seguida por algumas chamadas readString8() e seria bom ter controle total sobre os dados não consumidos após a incompatibilidade ocorrer, para que possamos colocar strings vazias diretamente lá e não nos preocupar com diferentes interpretações dos dados não consumidos.

Para fazer isso, primeiro precisamos colocar algum contêiner de dados brutos dentro de OutputConfiguration.mSensorPixelModesUsed que será gravado através de writeValue. Escolhi Bundle. Dessa forma, nos dados não consumidos teremos deixado:

  1. Tag VAL_BUNDLE do writeValue
  2. Comprimento dos dados brutos (este link também se aplica aos itens restantes nesta lista)
  3. BUNDLE_MAGIC
  4. Dados brutos passados literalmente através de Parcel.appendFrom

Portanto, temos três itens Parcel.writeInt que ficariam não consumidos; podemos nos livrar deles envolvendo OutputConfiguration dentro de algum Parcelable que, ao lê-lo, lê um valor Parcelable arbitrário seguido por três ints. Encontrei isso no CREATOR de ZenPolicy.

Para resumir, temos a seguinte hierarquia de objetos (que está presente no system_server e que ele tenta passar para scheduleReceiver):

  • Intent
    • mClipData = ClipData
      • mItems.get(0).mActivityInfo = ActivityInfo
        • applicationInfo = ApplicationInfo
          • splitDependencies.get(0) = ZenPolicy
            • mVisualEffects.get(0) = OutputConfiguration
              • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
                • mHierarchyOps.get(0) = PackageManagerException
              • mSensorPixelModesUsed.get(1) = Bundle

Isso é gravado pelo system_server. Em seguida, o aplicativo receptor lê tudo até (e incluindo os dados readSerializable de) PackageManagerException normalmente, no entanto, após os dados Serializable para PackageManagerException serem lidos, uma exceção é lançada e a leitura de tudo abaixo de OutputConfiguration é cancelada, deixando Bundle não lido. A leitura prossegue para ZenPolicy, que consome os três ints que precedem os dados brutos dentro de Bundle. Então, a leitura de ApplicationInfo prossegue com a leitura de dados que eram anteriormente dados brutos passados literalmente no Bundle. A leitura desses dados brutos continuará com os objetos restantes nesta pilha (ApplicationInfo, ActivityInfo, e ) e, em seguida, esses dados brutos serão usados para ler o próximo parâmetro do método .

O que acontece então dentro de handleReceiver

Como acabei de dizer, agora os parâmetros restantes de scheduleReceiver são lidos do buffer controlado pelo atacante.

Vamos dar uma olhada no que acontece quando esse método é invocado.

Primeiro, scheduleReceiver empacota os valores de todos os argumentos e usa sendMessage() para passar a execução para o thread principal.

Em seguida, no thread principal, handleReceiver é chamado.

handleReceiver chama getPackageInfoNoCheck, passando o ApplicationInfo que recebeu como parte do ActivityInfo que foi passado como argumento para scheduleReceiver.

getPackageInfo verifica se o pacote com o nome fornecido já está presente no cache e, se não estiver, constrói uma nova instância de LoadedApk, passando o objeto ApplicationInfo recebido anteriormente (como o atacante quer causar a construção de um novo LoadedApk, um packageName de um pacote que não foi visto anteriormente neste processo é usado).

Em seguida, o método ContextImpl.getClassLoader() é usado, que na primeira execução delega para mPackageInfo.getClassLoader(), com mPackageInfo sendo um LoadedApk construído no parágrafo anterior.

Então, há createOrUpdateClassLoaderLocked, que chama makePaths para popular zipPaths com os caminhos a serem usados no ClassLoader, então eles são unidos e atribuídos à variável zip e isso é passado para createClassLoader.

makePaths preenche zipPaths usando informações de ApplicationInfo, e o mais importante, isso inclui sourceDir. O aplicativo atacante faz o ApplicationInfo injetado com sourceDir definido para o caminho do próprio apk, portanto, a classe do receptor será realmente carregada do apk do atacante. Isso leva diretamente à execução de código controlado pelo atacante dentro do aplicativo que recebe a transmissão.

Nota sobre verificações de API oculta

Havia mais uma coisa que precisava ser contornada: verificações de API oculta. Elas nunca foram destinadas a ser um limite de segurança (já que o aplicativo sempre pode usar NDK e chamar o syscall subjacente diretamente), mas neste caso foram contornadas construindo um ClipData elaborado gravando manualmente dados no Parcel e depois usando readParcelable. Tal ClipData poderia então ser normalmente anexado a um Intent e depois passado para sendBroadcast(), de modo que o envio da transmissão em si fosse feito apenas usando APIs públicas.

Correções

O relatório acima foi originalmente enviado ao Google e parece que eles o utilizaram, pois há várias correções resultantes dele (acho, não tenho prova sobre causalidade direta).

Lançado com Android 12:

  • A captura de exceção de OutputConfiguration e classes relacionadas foi removida
  • OutputConfiguration#mSensorPixelModesUsed não é mais gravado através de writeValue
  • ClipData#mActivityInfo não é mais gravado no Parcel a menos que explicitamente solicitado durante a gravação (então Intent não pode mais conter Parcelables arbitrários do BOOTCLASSPATH, eliminando essa técnica de exploração)

Presente apenas no branch master no momento da escrita, não nas versões lançadas, provavelmente aparecerá no Android 13 (não no 12L):

  • Existem novos métodos de leitura de List no Parcel que verificam o tipo dos itens e versões sem tipo foram marcadas como obsoletas
  • Há um novo método Parcel#enforceNoDataAvail() que verifica se não há dados não lidos restantes no Parcel, que aparentemente será usado pelo AIDL após ler os argumentos da chamada RPC. Normalmente, minhas explorações dependiam do fato de que, após todos os dados serem lidos do Parcel, todo o resto era ignorado; isso não seria mais o caso, embora eu ache que em muitos casos se poderia construir dados que causassem, no final, um seek para a posição final, então essa não é uma mitigação forte. De qualquer forma, às vezes esses problemas ocorrem naturalmente sem serem detectados, então isso capturaria esses casos. Mais discussão sobre isso está no issue #3
  • Cada item em Bundle terá seu comprimento salvo separadamente. Isso praticamente mata toda a classe de bugs da qual venho relatando bugs privadamente ao Google desde 2014, descrição publicada em 2017 e código em si cerca de um ano depois. Se estou contando corretamente, serão 8 anos de vida da classe de bugs (honestamente, não tenho ideia se isso é muito, embora ainda possa haver variantes de exploração não-Bundle, como exatamente esta (embora exatamente esta já tenha sido corrigida)).
Baixar ferramenta
resolveClass
ObjectInputStream
c != null
Class.forName
PackageManagerException
Parcelable
readList
ClassLoader
WindowContainerTransaction
system_server
Parcel
PackageManagerException
ClassNotFoundException
envolvida em uma RuntimeException
capturada pelo CREATOR de OutputConfiguration
ClipData
Intent
handleReceiver