Skip to content

Visão Geral da Plataforma Android

Este capítulo apresenta a plataforma Android do ponto de vista da arquitetura. As cinco áreas principais a seguir são discutidas:

  1. Arquitetura do Android
  2. Segurança do Android: abordagem de defesa em profundidade
  3. Estrutura de aplicativos Android
  4. Publicação de aplicativos Android
  5. Superfície de ataque de aplicativos Android

Visite o site oficial de documentação para desenvolvedores Android para obter mais detalhes sobre a plataforma Android.

Arquitetura do Android

Android é uma plataforma de código aberto baseada em Linux desenvolvida pela Open Handset Alliance (um consórcio liderado pelo Google), que atua como um sistema operacional (SO) móvel. Hoje, a plataforma é a base para uma ampla variedade de tecnologias modernas, como telefones celulares, tablets, dispositivos vestíveis, TVs e outros dispositivos inteligentes. As versões típicas do Android são distribuídas com uma variedade de aplicativos pré-instalados ("stock") e suportam a instalação de aplicativos de terceiros por meio da Google Play Store e de outras lojas.

A pilha de software do Android é composta por várias camadas diferentes. Cada camada define interfaces e oferece serviços específicos.

Kernel: No nível mais baixo, o Android é baseado em uma variação do Kernel Linux contendo algumas adições significativas, incluindo Low Memory Killer, wake locks, o driver Binder IPC, etc. Para os propósitos do MASTG, focaremos na parte do SO em modo de usuário, onde o Android difere significativamente de uma distribuição Linux típica. Os dois componentes mais importantes para nós são o runtime gerenciado usado pelos aplicativos (ART/Dalvik) e o Bionic, a versão do Android da glibc, a biblioteca GNU C.

HAL: Acima do kernel, a Camada de Abstração de Hardware (HAL) define uma interface padrão para interação com componentes de hardware integrados. Várias implementações HAL são empacotadas em módulos de biblioteca compartilhada que o sistema Android chama quando necessário. Essa é a base para permitir que aplicativos interajam com o hardware do dispositivo. Por exemplo, permite que um aplicativo de telefone stock use o microfone e o alto-falante do dispositivo.

Ambiente de Execução: Os aplicativos Android são escritos em Java e Kotlin e depois compilados para bytecode Dalvik, que pode então ser executado usando um runtime que interpreta as instruções do bytecode e as executa no dispositivo de destino. Para o Android, esse é o Android Runtime (ART). Isso é semelhante à JVM (Máquina Virtual Java) para aplicativos Java, ou ao Mono Runtime para aplicativos .NET.

O bytecode Dalvik é uma versão otimizada do bytecode Java. Ele é criado primeiro compilando o código Java ou Kotlin para bytecode Java, usando os compiladores javac e kotlinc respectivamente, produzindo arquivos .class. Finalmente, o bytecode Java é convertido em bytecode Dalvik usando a ferramenta d8. O bytecode Dalvik é empacotado dentro de arquivos APK e AAB na forma de arquivos .dex e é usado por um runtime gerenciado no Android para executá-lo no dispositivo.

Antes do Android 5.0 (nível de API 21), o Android executava bytecode na Máquina Virtual Dalvik (DVM), onde era traduzido para código de máquina no momento da execução, um processo conhecido como compilação just-in-time (JIT). Isso permite que o runtime se beneficie da velocidade do código compilado, mantendo a flexibilidade da interpretação de código.

Desde o Android 5.0 (nível de API 21), o Android executa bytecode no Android Runtime (ART), que é o sucessor da DVM. O ART oferece desempenho aprimorado, bem como informações de contexto em relatórios de falhas nativas de aplicativos, incluindo informações de pilha Java e nativa. Ele usa a mesma entrada de bytecode Dalvik para manter a compatibilidade com versões anteriores. No entanto, o ART executa o bytecode Dalvik de forma diferente, usando uma combinação híbrida de compilação ahead-of-time (AOT), just-in-time (JIT) e guiada por perfil.

  • AOT pré-compila o bytecode Dalvik em código nativo, e o código gerado será salvo em disco com a extensão .oat (binário ELF). A ferramenta dex2oat pode ser usada para realizar a compilação e pode ser encontrada em /system/bin/dex2oat em dispositivos Android. A compilação AOT é executada durante a instalação do aplicativo. Isso faz com que o aplicativo inicie mais rapidamente, pois não é mais necessária compilação. No entanto, isso também significa que o tempo de instalação aumenta em comparação com a compilação JIT. Além disso, como os aplicativos são sempre otimizados em relação à versão atual do SO, isso significa que as atualizações de software recompilarão todos os aplicativos previamente compilados, resultando em um aumento significativo no tempo de atualização do sistema. Finalmente, a compilação AOT compilará o aplicativo inteiro, mesmo que certas partes nunca sejam usadas pelo usuário.
  • JIT ocorre em tempo de execução.
  • Compilação guiada por perfil é uma abordagem híbrida que foi introduzida no Android 7 (nível de API 24) para combater as desvantagens do AOT. Inicialmente, o aplicativo usará compilação JIT, e o Android acompanha todas as partes do aplicativo que são frequentemente usadas. Essas informações são armazenadas em um perfil de aplicativo e, quando o dispositivo está ocioso, um daemon de compilação (dex2oat) é executado, que compila via AOT os caminhos de código frequentes identificados a partir do perfil.

Fonte: https://lief-project.github.io/doc/latest/tutorials/10_android_formats.html

Sandboxing: Os aplicativos Android não têm acesso direto aos recursos de hardware, e cada aplicativo é executado em sua própria máquina virtual ou sandbox. Isso permite que o SO tenha controle preciso sobre os recursos e o acesso à memória no dispositivo. Por exemplo, um aplicativo que trava não afeta outros aplicativos em execução no mesmo dispositivo. O Android controla o número máximo de recursos do sistema alocados para aplicativos, impedindo que qualquer aplicativo monopolize muitos recursos. Ao mesmo tempo, esse design de sandbox pode ser considerado um dos muitos princípios na estratégia global de defesa em profundidade do Android. Um aplicativo de terceiros malicioso, com baixos privilégios, não deve ser capaz de escapar de seu próprio runtime e ler a memória de um aplicativo vítima no mesmo dispositivo. Na seção a seguir, examinamos mais de perto as diferentes camadas de defesa no sistema operacional Android. Saiba mais na seção "Isolamento de Software")).

Você pode encontrar informações mais detalhadas no artigo do Google Source "Android Runtime (ART)", no livro "Android Internals" de Jonathan Levin e no post do blog "Android 101" de @_qaz_qaz.

Segurança no Android: Abordagem de Defesa em Profundidade

A arquitetura do Android implementa diferentes camadas de segurança que, em conjunto, permitem uma abordagem de defesa em profundidade. Isso significa que a confidencialidade, integridade ou disponibilidade de dados sensíveis do usuário ou aplicativos não depende de uma única medida de segurança. Esta seção traz uma visão geral das diferentes camadas de defesa que o sistema Android fornece. A estratégia de segurança pode ser categorizada aproximadamente em quatro domínios distintos, cada um com foco na proteção contra determinados modelos de ataque.

  • Segurança em todo o sistema
  • Isolamento de software
  • Segurança de rede
  • Anti-exploração

Segurança em todo o sistema

Criptografia de dispositivo

O Android suporta criptografia de dispositivo a partir do Android 2.3.4 (API level 10) e passou por grandes mudanças desde então. O Google determinou que todos os dispositivos executando Android 6.0 (API level 23) ou superior deveriam suportar criptografia de armazenamento, embora alguns dispositivos de baixo custo tenham sido isentos porque isso impactaria significativamente seu desempenho.

  • Criptografia de disco completo (FDE): Android 5.0 (API level 21) e superior suportam criptografia de disco completo. Esta criptografia usa uma única chave protegida pela senha do dispositivo do usuário para criptografar e descriptografar a partição de dados do usuário. Este tipo de criptografia é considerado obsoleto atualmente e a criptografia baseada em arquivo deve ser usada sempre que possível. A criptografia de disco completo tem desvantagens, como não poder receber chamadas ou não ter alarmes operacionais após uma reinicialização se o usuário não inserir a senha para desbloquear.

  • Criptografia baseada em arquivo (FBE): Android 7.0 (API level 24) suporta criptografia baseada em arquivo. A criptografia baseada em arquivo permite que diferentes arquivos sejam criptografados com chaves diferentes para que possam ser decifrados independentemente. Dispositivos que suportam este tipo de criptografia também suportam Direct Boot. O Direct Boot permite que o dispositivo tenha acesso a recursos como alarmes ou serviços de acessibilidade mesmo se o usuário não tiver desbloqueado o dispositivo.

Nota: você pode ouvir falar do Adiantum, que é um método de criptografia projetado para dispositivos executando Android 9 (API level 28) e superior cujas CPUs não possuem instruções AES. O Adiantum é relevante apenas para desenvolvedores de ROM ou fabricantes de dispositivos, o Android não fornece uma API para desenvolvedores usarem o Adiantum a partir de aplicativos. Como recomendado pelo Google, o Adiantum não deve ser usado ao enviar dispositivos baseados em ARM com ARMv8 Cryptography Extensions ou dispositivos baseados em x86 com AES-NI. O AES é mais rápido nessas plataformas.

Mais informações estão disponíveis na documentação do Android.

Ambiente Confiável de Execução (TEE)

Para que o sistema Android execute criptografia, ele precisa de uma maneira de gerar, importar e armazenar chaves criptográficas com segurança. Estamos essencialmente transferindo o problema de manter dados sensíveis seguros para o de manter uma chave criptográfica segura. Se um invasor conseguir extrair ou adivinhar a chave criptográfica, os dados sensíveis criptografados podem ser recuperados.

O Android oferece um ambiente confiável de execução em hardware dedicado para resolver o problema de gerar e proteger chaves criptográficas com segurança. Isso significa que um componente de hardware dedicado no sistema Android é responsável por manipular material de chave criptográfica. Três módulos principais são responsáveis por isso:

  • Hardware-backed KeyStore: Este módulo oferece serviços criptográficos ao sistema operacional Android e a aplicativos de terceiros. Ele permite que aplicativos realizem operações sensíveis de criptografia em um TEE sem expor o material da chave criptográfica.

  • StrongBox: No Android 9 (Pie), o StrongBox foi introduzido, outra abordagem para implementar um KeyStore com suporte de hardware. Enquanto antes do Android 9 Pie, um KeyStore com suporte de hardware seria qualquer implementação de TEE que está fora do kernel do sistema operacional Android. O StrongBox é um chip de hardware separado completo que é adicionado ao dispositivo no qual o KeyStore é implementado e é claramente definido na documentação do Android. Você pode verificar programaticamente se uma chave reside no StrongBox e, se residir, você pode ter certeza de que está protegida por um módulo de segurança de hardware que possui sua própria CPU, armazenamento seguro e Gerador Verdadeiro de Números Aleatórios (TRNG). Todas as operações criptográficas sensíveis acontecem nesse chip, dentro dos limites seguros do StrongBox.

  • GateKeeper: O módulo GateKeeper permite a autenticação por padrão e senha do dispositivo. As operações sensíveis de segurança durante o processo de autenticação acontecem dentro do TEE disponível no dispositivo. O GateKeeper consiste em três componentes principais: (1) gatekeeperd, que é o serviço que expõe o GateKeeper, (2) GateKeeper HAL, que é a interface de hardware, e (3) a implementação do TEE, que é o software real que implementa a funcionalidade do GateKeeper no TEE.

Inicialização Verificada

Precisamos ter uma forma de garantir que o código que está sendo executado em dispositivos Android venha de uma fonte confiável e que sua integridade não esteja comprometida. Para alcançar isso, o Android introduziu o conceito de inicialização verificada (verified boot). O objetivo da inicialização verificada é estabelecer uma relação de confiança entre o hardware e o código real que executa nesse hardware. Durante a sequência de inicialização verificada, uma cadeia completa de confiança é estabelecida, começando da Raiz de Confiança (Root-of-Trust - RoT) protegida por hardware até o sistema final em execução, passando por e verificando todas as fases de inicialização necessárias. Quando o sistema Android é finalmente inicializado, você pode ter certeza de que o sistema não foi adulterado. Você tem prova criptográfica de que o código em execução é aquele pretendido pelo OEM e não um que foi maliciosamente ou acidentalmente alterado.

Mais informações estão disponíveis na documentação do Android.

Isolamento de Software

Usuários e Grupos do Android

Embora o sistema operacional Android seja baseado em Linux, ele não implementa contas de usuário da mesma forma que outros sistemas Unix-like. No Android, o suporte a múltiplos usuários do kernel Linux é usado para isolar aplicativos em sandbox: com poucas exceções, cada aplicativo é executado como se estivesse sob um usuário Linux separado, efetivamente isolado de outros aplicativos e do restante do sistema operacional.

O arquivo android_filesystem_config.h inclui uma lista dos usuários e grupos predefinidos aos quais os processos do sistema são atribuídos. UIDs (userIDs) para outros aplicativos são adicionados conforme estes são instalados.

Por exemplo, o Android 9.0 (API level 28) define os seguintes usuários do sistema:

    #define AID_ROOT             0  /* traditional unix root user */
    #...
    #define AID_SYSTEM        1000  /* system server */
    #...
    #define AID_SHELL         2000  /* adb and debug shell user */
    #...
    #define AID_APP_START          10000  /* first app user */
    ...

SELinux

O Security-Enhanced Linux (SELinux) utiliza um sistema de Controle de Acesso Obrigatório (MAC) para reforçar ainda mais o bloqueio de quais processos devem ter acesso a quais recursos. Cada recurso recebe um rótulo no formato user:role:type:mls_level, que define quais usuários podem executar quais tipos de ações sobre ele. Por exemplo, um processo pode ter permissão apenas para ler um arquivo, enquanto outro processo pode ter permissão para editar ou excluir o arquivo. Dessa forma, ao operar sob o princípio de privilégio mínimo, processos vulneráveis tornam-se mais difíceis de explorar por meio de escalação de privilégios ou movimento lateral.

Mais informações estão disponíveis na documentação do Android.

Permissões

O Android implementa um sistema extensivo de permissões que é utilizado como mecanismo de controle de acesso. Ele garante o acesso controlado a dados sensíveis do usuário e recursos do dispositivo. O Android categoriza as permissões em diferentes tipos oferecendo vários níveis de proteção.

Antes do Android 6.0 (nível de API 23), todas as permissões solicitadas por um aplicativo eram concedidas durante a instalação (Permissões de instalação). A partir do nível de API 23, o usuário deve aprovar algumas solicitações de permissão durante a execução (Permissões de tempo de execução).

Mais informações estão disponíveis na documentação do Android incluindo várias considerações e melhores práticas.

Para aprender como testar permissões de aplicativos, consulte a seção Testando Permissões de Aplicativos no capítulo "APIs da Plataforma Android".

Segurança de rede

TLS por Padrão

Por padrão, desde o Android 9 (nível de API 28), toda a atividade de rede é tratada como sendo executada em um ambiente hostil. Isso significa que o sistema Android permitirá apenas que os aplicativos se comuniquem por um canal de rede estabelecido usando o protocolo Transport Layer Security (TLS). Este protocolo efetivamente criptografa todo o tráfego de rede e cria um canal seguro para um servidor. Pode ser o caso de você querer usar conexões de tráfego não criptografado por razões de legado. Isso pode ser alcançado adaptando o arquivo res/xml/network_security_config.xml no aplicativo.

Mais informações estão disponíveis na documentação do Android.

DNS sobre TLS

O suporte a DNS sobre TLS em todo o sistema foi introduzido a partir do Android 9 (nível de API 28). Ele permite que você realize consultas a servidores DNS usando o protocolo TLS. Um canal seguro é estabelecido com o servidor DNS através do qual a consulta DNS é enviada. Isso garante que nenhum dado sensível seja exposto durante uma pesquisa DNS.

Mais informações estão disponíveis no blog Android Developers.

Anti-exploração

ASLR, KASLR, PIE e DEP

O Address Space Layout Randomization (ASLR), que faz parte do Android desde o Android 4.1 (nível de API 15), é uma proteção padrão contra ataques de estouro de buffer, que garante que tanto o aplicativo quanto o sistema operacional sejam carregados em endereços de memória aleatórios, dificultando a obtenção do endereço correto para uma região de memória ou biblioteca específica. No Android 8.0 (nível de API 26), essa proteção também foi implementada para o kernel (KASLR). A proteção ASLR só é possível se o aplicativo puder ser carregado em um local aleatório na memória, o que é indicado pela flag Position Independent Executable (PIE) do aplicativo. Desde o Android 5.0 (nível de API 21), o suporte para bibliotecas nativas não habilitadas para PIE foi removido. Por fim, o Data Execution Prevention (DEP) impede a execução de código na stack e na heap, sendo também utilizado para combater explorações de estouro de buffer.

Mais informações estão disponíveis no blog do Android Developers.

Filtro SECCOMP

Aplicativos Android podem conter código nativo escrito em C ou C++. Esses binários compilados podem se comunicar tanto com o Android Runtime por meio de ligações Java Native Interface (JNI), quanto com o sistema operacional por meio de chamadas de sistema. Algumas chamadas de sistema não são implementadas ou não devem ser chamadas por aplicativos normais. Como essas chamadas de sistema se comunicam diretamente com o kernel, elas são um alvo principal para desenvolvedores de exploits. A partir do Android 8 (nível de API 26), o Android introduziu suporte para filtros Secure Computing (SECCOMP) para todos os processos baseados no Zygote (ou seja, aplicativos de usuário). Esses filtros restringem as syscalls disponíveis àquelas expostas através do bionic.

Mais informações estão disponíveis no blog de desenvolvedores Android.

Estrutura de Aplicativos Android

Comunicação com o Sistema Operacional

Os aplicativos Android interagem com os serviços do sistema por meio do Android Framework, uma camada de abstração que oferece APIs Java de alto nível. A maioria desses serviços é invocada por meio de chamadas normais de métodos Java e são traduzidas para chamadas IPC para serviços do sistema que estão sendo executados em segundo plano. Exemplos de serviços do sistema incluem:

  • Conectividade (Wi-Fi, Bluetooth, NFC, etc.)
  • Arquivos
  • Câmeras
  • Geolocalização (GPS)
  • Microfone

O framework também oferece funções de segurança comuns, como criptografia.

As especificações da API mudam a cada nova versão do Android. Correções críticas de bugs e patches de segurança geralmente são aplicados também a versões anteriores.

Versões de API notáveis API versions. Consulte Use Up-to-Date minSdkVersion para obter mais informações sobre recursos de segurança e privacidade introduzidos em diferentes versões do Android.

Os lançamentos de desenvolvimento do Android seguem uma estrutura única. Eles são organizados em famílias e recebem codinomes alfabéticos inspirados em guloseimas saborosas. Você pode encontrá-los todos aqui.

O Sandbox do Aplicativo

Os aplicativos são executados no Android Application Sandbox, que separa os dados e a execução do código do aplicativo de outros aplicativos no dispositivo. Como mencionado anteriormente, essa separação adiciona uma primeira camada de defesa.

A instalação de um novo aplicativo cria um novo diretório nomeado de acordo com o pacote do aplicativo, resultando no seguinte caminho: /data/data/[nome-do-pacote]. Este diretório mantém os dados do aplicativo. As permissões de diretório do Linux são configuradas de forma que o diretório possa ser lido e escrito apenas com o UID exclusivo do aplicativo.

Podemos confirmar isso observando as permissões do sistema de arquivos na pasta /data/data. Por exemplo, podemos ver que o Google Chrome e o Calendário possuem um diretório cada e são executados sob diferentes contas de usuário:

drwx------  4 u0_a97              u0_a97              4096 2017-01-18 14:27 com.android.calendar
drwx------  6 u0_a120             u0_a120             4096 2017-01-19 12:54 com.android.chrome

Desenvolvedores que desejam que seus aplicativos compartilhem um sandbox comum podem contornar o isolamento. Quando dois aplicativos são assinados com o mesmo certificado e compartilham explicitamente o mesmo ID de usuário (possuindo o sharedUserId em seus arquivos AndroidManifest.xml), cada um pode acessar o diretório de dados do outro. Veja o exemplo a seguir para alcançar isso no aplicativo NFC:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.android.nfc"
  android:sharedUserId="android.uid.nfc">

Gerenciamento de Usuários Linux

O Android aproveita o gerenciamento de usuários do Linux para isolar aplicativos. Essa abordagem é diferente do uso do gerenciamento de usuários em ambientes Linux tradicionais, onde múltiplos aplicativos geralmente são executados pelo mesmo usuário. O Android cria um UID exclusivo para cada aplicativo Android e executa o aplicativo em um processo separado. Consequentemente, cada aplicativo pode acessar apenas seus próprios recursos. Essa proteção é aplicada pelo kernel do Linux.

Geralmente, os aplicativos recebem UIDs na faixa entre 10000 e 99999. Os aplicativos Android recebem um nome de usuário baseado em seu UID. Por exemplo, o aplicativo com UID 10188 recebe o nome de usuário u0_a188. Se as permissões solicitadas por um aplicativo forem concedidas, o ID do grupo correspondente é adicionado ao processo do aplicativo. Por exemplo, o ID do usuário do aplicativo abaixo é 10188. Ele pertence ao grupo ID 3003 (inet). Esse grupo está relacionado à permissão android.permission.INTERNET. A saída do comando id é mostrada abaixo.

$ id
uid=10188(u0_a188) gid=10188(u0_a188) groups=10188(u0_a188),3003(inet),
9997(everybody),50188(all_a188) context=u:r:untrusted_app:s0:c512,c768

A relação entre IDs de grupo e permissões é definida no seguinte arquivo:

platform.xml

<permission name="android.permission.INTERNET" >
    <group gid="inet" />
</permission>

<permission name="android.permission.READ_LOGS" >
    <group gid="log" />
</permission>

<permission name="android.permission.WRITE_MEDIA_STORAGE" >
    <group gid="media_rw" />
    <group gid="sdcard_rw" />
</permission>

Zygote

O processo Zygote é iniciado durante a inicialização do Android. Zygote é um serviço do sistema responsável por iniciar aplicativos. O processo Zygote é um processo "base" que contém todas as bibliotecas principais necessárias para o aplicativo. Ao ser iniciado, o Zygote abre o socket /dev/socket/zygote e aguarda conexões de clientes locais. Quando recebe uma conexão, ele faz um fork (cria uma cópia) de um novo processo, que então carrega e executa o código específico do aplicativo.

Ciclo de Vida do Aplicativo

No Android, o tempo de vida de um processo de aplicativo é controlado pelo sistema operacional. Um novo processo Linux é criado quando um componente do aplicativo é iniciado e o mesmo aplicativo ainda não possui nenhum outro componente em execução. O Android pode encerrar esse processo quando ele não for mais necessário ou quando for necessário recuperar memória para executar aplicativos mais importantes. A decisão de encerrar um processo está principalmente relacionada ao estado da interação do usuário com o processo. Em geral, os processos podem estar em um de quatro estados.

  • Um processo em primeiro plano (por exemplo, uma atividade em execução no topo da tela ou um BroadcastReceiver em execução)
  • Um processo visível é um processo do qual o usuário tem conhecimento, portanto, encerrá-lo teria um impacto negativo perceptível na experiência do usuário. Um exemplo é executar uma atividade que está visível para o usuário na tela, mas não em primeiro plano.

  • Um processo de serviço é um processo que hospeda um serviço iniciado com o método startService. Embora esses processos não sejam diretamente visíveis para o usuário, geralmente são coisas com as quais o usuário se importa (como upload ou download de dados de rede em segundo plano), então o sistema sempre manterá esses processos em execução, a menos que não haja memória suficiente para manter todos os processos em primeiro plano e visíveis.

  • Um processo em cache é um processo que não é necessário no momento, portanto, o sistema pode encerrá-lo livremente quando a memória for necessária.

Os aplicativos devem implementar métodos de callback que reagem a vários eventos; por exemplo, o manipulador onCreate é chamado quando o processo do aplicativo é criado pela primeira vez. Outros métodos de callback incluem onLowMemory, onTrimMemory e onConfigurationChanged.

Pacotes de Aplicativos (App Bundles)

Os aplicativos Android podem ser distribuídos de duas formas: o arquivo Android Package Kit (APK) ou um Android App Bundle (.aab). Os Android App Bundles fornecem todos os recursos necessários para um aplicativo, mas adiam a geração do APK e sua assinatura para o Google Play. Os App Bundles são binários assinados que contêm o código do aplicativo em vários módulos. O módulo base contém o núcleo do aplicativo. O módulo base pode ser estendido com vários módulos que contêm novos aprimoramentos/funcionalidades para o aplicativo, conforme explicado mais detalhadamente na documentação do desenvolvedor sobre app bundle.

Se você possui um Android App Bundle, pode utilizar a ferramenta de linha de comando bundletool do Google para construir APKs não assinados e assim usar as ferramentas existentes para APK. Você pode criar um APK a partir de um arquivo AAB executando o seguinte comando:

bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

Se desejar criar APKs assinados prontos para implantação em um dispositivo de teste, use:

$ bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks
--ks=/MyApp/keystore.jks
--ks-pass=file:/MyApp/keystore.pwd
--ks-key-alias=MyKeyAlias
--key-pass=file:/MyApp/key.pwd

Recomendamos que você teste tanto o APK com quanto sem os módulos adicionais, para que fique claro se os módulos adicionais introduzem e/ou corrigem problemas de segurança para o módulo base.

Android Manifest

Todo aplicativo Android contém um arquivo AndroidManifest.xml na raiz do APK, armazenado em formato binário XML. Este arquivo define a estrutura do aplicativo e as propriedades-chave utilizadas pelo sistema operacional Android durante a instalação e execução.

Elementos relevantes para segurança incluem:

  • Permissões: Declara permissões necessárias usando <uses-permission>, como acesso à internet, câmera, armazenamento, localização ou contatos. Estas definem os limites de acesso do aplicativo e devem seguir o princípio do menor privilégio. Permissões personalizadas podem ser definidas usando <permission> e devem incluir um protectionLevel adequado, como signature ou dangerous, para evitar uso indevido por outros aplicativos.
  • Componentes: O manifesto lista todos os componentes do aplicativo)) declarados no app que servem como pontos de entrada. Eles podem ser expostos a outros aplicativos (via filtros de intent ou o atributo exported), sendo críticos para determinar como um atacante pode interagir com o aplicativo. Os principais tipos de componentes são:
    • Activities: definem telas de interface do usuário.
    • Services: executam tarefas em segundo plano.
    • Broadcast Receivers: lidam com mensagens externas.
    • Content Providers: expõem dados estruturados.
  • Deep Links: Deep links são configurados via filtros de intent com a ação VIEW, categoria BROWSABLE e um elemento data especificando um padrão URI. Estes podem expor activities a links web ou de aplicativos e devem ser verificados cuidadosamente para evitar riscos de injeção ou falsificação. Adicionar android:autoVerify="true" habilita App Links, que restringem o tratamento de links verificados ao aplicativo declarado, reduzindo o risco de sequestro de links.
  • Tráfego em Texto Limpo: O atributo android:usesCleartextTraffic controla se o aplicativo permite tráfego HTTP não criptografado. A partir do Android 9 (API 28), o tráfego em texto limpo é desabilitado por padrão, a menos que explicitamente permitido. Este atributo também pode ser substituído pelo networkSecurityConfig.
  • Configuração de Segurança de Rede: Um arquivo XML opcional definido via android:networkSecurityConfig, disponível desde o Android 7.0 (nível de API 24), que fornece controle granular sobre o comportamento de segurança de rede. Permite especificar autoridades certificadoras confiáveis, requisitos TLS por domínio e exceções de tráfego em texto limpo, substituindo configurações globais definidas em android:usesCleartextTraffic.
  • Comportamento de Backup: O atributo android:allowBackup permite ou impede que os dados do aplicativo sejam backupeados.
  • Afiniades de Tarefa e Modos de Inicialização: Essas configurações influenciam como as activities são agrupadas e iniciadas. Más configurações podem permitir sequestro de tarefas ou ataques do tipo phishing se o aplicativo de um atacante imitar componentes legítimos.

A lista completa de opções disponíveis no manifesto pode ser encontrada na documentação oficial do arquivo Android Manifest.

No momento da compilação, o manifesto é mesclado com aqueles de todas as bibliotecas e dependências incluídas. O manifesto mesclado final pode incluir permissões, componentes ou configurações adicionais não declaradas explicitamente pelo desenvolvedor. Revisões de segurança devem analisar a saída mesclada para entender a exposição real do aplicativo.

Aqui está um exemplo de um arquivo de manifesto conforme definido por um desenvolvedor. Ele declara várias permissões, permite backup e define a activity principal do aplicativo:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MASTestApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Se você obtiver o arquivo AndroidManifest.xml de um APK ( Obtenção de Informações do AndroidManifest), verá que ele inclui elementos adicionais, como o atributo package, que define o identificador único do aplicativo, o elemento <uses-sdk> que especifica o android:minSdkVersion e android:targetSdkVersion, novas activities, providers e receivers, e outros atributos como android:debuggable="true", que indica que o aplicativo está em modo de depuração.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0"
    android:compileSdkVersion="35"
    android:compileSdkVersionCodename="15"
    package="org.owasp.mastestapp"
    platformBuildVersionCode="35"
    platformBuildVersionName="15">
    <uses-sdk
        android:minSdkVersion="29"
        android:targetSdkVersion="35"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <permission
        android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
        android:protectionLevel="signature"/>
    <uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application
        android:theme="@style/Theme.MASTestApp"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:debuggable="true"
        android:testOnly="true"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:extractNativeLibs="false"
        android:fullBackupContent="@xml/backup_rules"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:dataExtractionRules="@xml/data_extraction_rules">
        <activity
            android:theme="@style/Theme.MASTestApp"
            android:name="org.owasp.mastestapp.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
            android:name="androidx.compose.ui.tooling.PreviewActivity"
            android:exported="true"/>
        <activity
            android:name="androidx.activity.ComponentActivity"
            android:exported="true"/>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            android:authorities="org.owasp.mastestapp.androidx-startup">
            <meta-data
                android:name="androidx.emoji2.text.EmojiCompatInitializer"
                android:value="androidx.startup"/>
            ...
        </provider>
        <receiver
            android:name="androidx.profileinstaller.ProfileInstallReceiver"
            android:permission="android.permission.DUMP"
            android:enabled="true"
            android:exported="true"
            android:directBootAware="false">
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
            </intent-filter>
            ...
        </receiver>
    </application>
</manifest>

Componentes de Aplicativos

Os aplicativos Android são compostos por diversos componentes de alto nível. Os principais componentes são:

  • Activities
  • Fragments
  • Intents
  • Broadcast receivers
  • Content providers e services

Todos esses elementos são fornecidos pelo sistema operacional Android, na forma de classes predefinidas disponíveis por meio de APIs.

Atividades

As atividades compõem a parte visível de qualquer aplicativo. Existe uma atividade por tela, portanto, um aplicativo com três telas diferentes implementa três atividades distintas. As atividades são declaradas estendendo a classe Activity. Elas contêm todos os elementos da interface do usuário: fragments, views e layouts.

Cada atividade precisa ser declarada no Android Manifest com a seguinte sintaxe:

<activity android:name="ActivityName">
</activity>

Atividades não declaradas no manifesto não podem ser exibidas, e tentar iniciá-las irá gerar uma exceção.

Assim como os aplicativos, as atividades possuem seu próprio ciclo de vida e precisam monitorar mudanças do sistema para lidar com elas. As atividades podem estar nos seguintes estados: ativa, pausada, parada e inativa. Esses estados são gerenciados pelo sistema operacional Android. Consequentemente, as atividades podem implementar os seguintes gerenciadores de eventos:

  • onCreate
  • onSaveInstanceState
  • onStart
  • onResume
  • onRestoreInstanceState
  • onPause
  • onStop
  • onRestart
  • onDestroy

Um aplicativo pode não implementar explicitamente todos os gerenciadores de eventos, caso em que ações padrão são tomadas. Normalmente, pelo menos o gerenciador onCreate é sobrescrito pelos desenvolvedores do aplicativo. É assim que a maioria dos componentes da interface do usuário são declarados e inicializados. onDestroy pode ser sobrescrito quando recursos (como conexões de rede ou conexões com bancos de dados) precisam ser explicitamente liberados ou ações específicas devem ocorrer quando o aplicativo é encerrado.

Fragmentos

Um fragmento representa um comportamento ou uma porção da interface do usuário dentro de uma atividade. Os fragmentos foram introduzidos no Android com a versão Honeycomb 3.0 (API level 11).

Os fragmentos são destinados a encapsular partes da interface para facilitar a reutilização e adaptação a diferentes tamanhos de tela. Os fragmentos são entidades autônomas no sentido de que incluem todos os seus componentes necessários (possuem seu próprio layout, botões, etc.). No entanto, eles devem ser integrados com atividades para serem úteis: fragmentos não podem existir por conta própria. Eles possuem seu próprio ciclo de vida, que está vinculado ao ciclo de vida das atividades que os implementam.

Como os fragmentos possuem seu próprio ciclo de vida, a classe Fragment contém gerenciadores de eventos que podem ser redefinidos e estendidos. Esses gerenciadores de eventos incluem onAttach, onCreate, onStart, onDestroy e onDetach. Existem vários outros; o leitor deve consultar a especificação de Fragment do Android para mais detalhes.

Os fragmentos podem ser facilmente implementados estendendo a classe Fragment fornecida pelo Android:

Exemplo em Java:

public class MyFragment extends Fragment {
    ...
}

Exemplo em Kotlin:

class MyFragment : Fragment() {
    ...
}

Os fragmentos não precisam ser declarados em arquivos de manifesto porque dependem de atividades.

Para gerenciar seus fragmentos, uma atividade pode usar um Gerenciador de Fragmentos (classe FragmentManager). Esta classe facilita a localização, adição, remoção e substituição de fragmentos associados.

Os Gerenciadores de Fragmentos podem ser criados através dos seguintes métodos:

Exemplo em Java:

FragmentManager fm = getFragmentManager();

Exemplo em Kotlin:

var fm = fragmentManager

Os fragmentos não necessariamente possuem uma interface de usuário; eles podem ser uma maneira conveniente e eficiente de gerenciar operações em segundo plano relacionadas à interface do usuário do aplicativo. Um fragmento pode ser declarado persistente para que o sistema preserve seu estado mesmo que sua Activity seja destruída.

Provedores de Conteúdo

O Android utiliza SQLite para armazenar dados permanentemente: assim como no Linux, os dados são armazenados em arquivos. O SQLite é uma tecnologia de armazenamento relacional de dados leve, eficiente e de código aberto que não requer muito poder de processamento, tornando-a ideal para uso móvel. Uma API completa com classes específicas (Cursor, ContentValues, SQLiteOpenHelper, ContentProvider, ContentResolver, etc.) está disponível. O SQLite não é executado como um processo separado; ele faz parte do aplicativo. Por padrão, um banco de dados pertencente a um determinado aplicativo é acessível apenas por esse aplicativo. No entanto, os provedores de conteúdo oferecem um excelente mecanismo para abstrair fontes de dados (incluindo bancos de dados e arquivos simples); eles também fornecem um mecanismo padrão e eficiente para compartilhar dados entre aplicativos, incluindo aplicativos nativos. Para ser acessível por outros aplicativos, um provedor de conteúdo precisa ser explicitamente declarado no arquivo de manifesto do aplicativo que o compartilhará. Enquanto os provedores de conteúdo não forem declarados, eles não serão exportados e só poderão ser chamados pelo aplicativo que os criou.

Os provedores de conteúdo são implementados por meio de um esquema de endereçamento URI: todos usam o modelo content://. Independentemente do tipo de fonte (banco de dados SQLite, arquivo simples, etc.), o esquema de endereçamento é sempre o mesmo, abstraindo assim as fontes e oferecendo ao desenvolvedor um esquema único. Os provedores de conteúdo oferecem todas as operações regulares de banco de dados: criar, ler, atualizar e excluir. Isso significa que qualquer aplicativo com os direitos adequados em seu arquivo de manifesto pode manipular os dados de outros aplicativos.

Serviços

Serviços são componentes do sistema operacional Android (baseados na classe Service) que executam tarefas em segundo plano (processamento de dados, inicialização de intents, notificações, etc.) sem apresentar uma interface de usuário. Os serviços são projetados para executar processos de longa duração. Suas prioridades no sistema são menores do que as de aplicativos ativos e maiores do que as de aplicativos inativos. Portanto, eles têm menor probabilidade de serem encerrados quando o sistema precisa de recursos, e podem ser configurados para reiniciar automaticamente quando recursos suficientes estiverem disponíveis. Isso torna os serviços excelentes candidatos para executar tarefas em segundo plano. Observe que os serviços, assim como as Activities, são executados na thread principal do aplicativo. Um serviço não cria sua própria thread e não é executado em um processo separado, a menos que especificado de outra forma.

Comunicação entre Processos

Como já aprendemos, cada processo Android possui seu próprio espaço de endereçamento isolado (sandbox). Os recursos de comunicação entre processos permitem que aplicativos troquem sinais e dados de forma segura. Em vez de depender dos recursos padrão de IPC do Linux, o IPC do Android é baseado no Binder, uma implementação personalizada do OpenBinder. A maioria dos serviços do sistema Android e todos os serviços de IPC de alto nível dependem do Binder.

O termo Binder representa muitas coisas diferentes, incluindo:

  • Driver Binder: o driver em nível de kernel
  • Protocolo Binder: protocolo de baixo nível baseado em ioctl usado para se comunicar com o driver binder
  • Interface IBinder: um comportamento bem definido que os objetos Binder implementam
  • Objeto Binder: implementação genérica da interface IBinder
  • Serviço Binder: implementação do objeto Binder; por exemplo, serviço de localização e serviço de sensores
  • Cliente Binder: um objeto que utiliza o serviço Binder

O framework Binder inclui um modelo de comunicação cliente-servidor. Para usar IPC, os aplicativos chamam métodos IPC em objetos proxy. Os objetos proxy transparentemente empacotam (marshall) os parâmetros da chamada em um parcel e enviam uma transação para o servidor Binder, que é implementado como um driver de caractere (/dev/binder). O servidor mantém um pool de threads para lidar com solicitações recebidas e entrega mensagens para o objeto de destino. Da perspectiva do aplicativo cliente, tudo isso parece uma chamada de método comum, todo o trabalho pesado é feito pelo framework Binder.

Os serviços que permitem que outros aplicativos se conectem a eles são chamados de serviços vinculados (bound services). Esses serviços devem fornecer uma interface IBinder para os clientes. Os desenvolvedores usam a Android Interface Descriptor Language (AIDL) para escrever interfaces para serviços remotos.

O ServiceManager é um daemon do sistema que gerencia o registro e a localização de serviços do sistema. Ele mantém uma lista de pares nome/Binder para todos os serviços registrados. Os serviços são adicionados com addService e recuperados por nome com o método estático getService em android.os.ServiceManager:

Exemplo em Java:

public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }

Exemplo em Kotlin:

companion object {
        private val sCache: Map<String, IBinder> = ArrayMap()
        fun getService(name: String): IBinder? {
            try {
                val service = sCache[name]
                return service ?: getIServiceManager().getService(name)
            } catch (e: RemoteException) {
                Log.e(FragmentActivity.TAG, "error in getService", e)
            }
            return null
        }
    }

Você pode consultar a lista de serviços do sistema com o comando service list.

$ adb shell service list
Found 99 services:
0 carrier_config: [com.android.internal.telephony.ICarrierConfigLoader]
1 phone: [com.android.internal.telephony.ITelephony]
2 isms: [com.android.internal.telephony.ISms]
3 iphonesubinfo: [com.android.internal.telephony.IPhoneSubInfo]

Intents

O Intent messaging é um framework de comunicação assíncrona construído sobre o Binder. Este framework permite tanto mensagens ponto a ponto quanto de publicação-assinatura. Um Intent é um objeto de mensagem que pode ser usado para solicitar uma ação de outro componente de aplicativo. Embora os intents facilitem a comunicação entre componentes de várias maneiras, existem três casos de uso fundamentais:

  • Iniciar uma activity
    • Uma activity representa uma única tela em um aplicativo. Você pode iniciar uma nova instância de uma activity passando um intent para startActivity. O intent descreve a activity e carrega os dados necessários.
  • Iniciar um serviço
    • Um Service é um componente que executa operações em segundo plano, sem uma interface de usuário. Com Android 5.0 (API nível 21) e versões posteriores, você pode iniciar um serviço com JobScheduler.
  • Entregar um broadcast
    • Um broadcast é uma mensagem que qualquer aplicativo pode receber. O sistema entrega broadcasts para eventos do sistema, incluindo inicialização do sistema e início de carregamento. Você pode entregar um broadcast para outros aplicativos passando um intent para sendBroadcast ou sendOrderedBroadcast.

Existem dois tipos de intents. Intents explícitos nomeiam o componente que será iniciado (o nome de classe totalmente qualificado). Por exemplo:

Exemplo em Java:

Intent intent = new Intent(this, myActivity.myClass);

Exemplo em Kotlin:

var intent = Intent(this, myActivity.myClass)

Intents implícitos são enviados para o SO para executar uma determinada ação em um determinado conjunto de dados (a URL do site da OWASP em nosso exemplo abaixo). Cabe ao sistema decidir qual aplicativo ou classe executará o serviço correspondente. Por exemplo:

Exemplo em Java:

Intent intent = new Intent(Intent.MY_ACTION, Uri.parse("https://www.owasp.org"));

Exemplo em Kotlin:

var intent = Intent(Intent.MY_ACTION, Uri.parse("https://www.owasp.org"))

Um intent filter é uma expressão nos arquivos Android Manifest que especifica o tipo de intents que o componente gostaria de receber. Por exemplo, ao declarar um intent filter para uma activity, você possibilita que outros aplicativos iniciem diretamente sua activity com um certo tipo de intent. Da mesma forma, sua activity só pode ser iniciada com um intent explícito se você não declarar nenhum intent filter para ela.

O Android usa intents para transmitir mensagens para aplicativos (como uma chamada recebida ou SMS), informações importantes sobre alimentação de energia (bateria fraca, por exemplo) e alterações de rede (perda de conexão, por exemplo). Dados extras podem ser adicionados aos intents (através de putExtra/getExtras).

Aqui está uma pequena lista de intents enviados pelo sistema operacional. Todas as constantes são definidas na classe Intent, e a lista completa está na documentação oficial do Android:

  • ACTION_CAMERA_BUTTON
  • ACTION_MEDIA_EJECT
  • ACTION_NEW_OUTGOING_CALL
  • ACTION_TIMEZONE_CHANGED

Para melhorar a segurança e a privacidade, um Local Broadcast Manager é usado para enviar e receber intents dentro de um aplicativo sem que eles sejam enviados para o restante do sistema operacional. Isso é muito útil para garantir que dados sensíveis e privados não saiam do perímetro do aplicativo (dados de geolocalização, por exemplo).

Broadcast Receivers

Os Broadcast Receivers são componentes que permitem que aplicativos recebam notificações de outros aplicativos e do próprio sistema. Com eles, os aplicativos podem reagir a eventos (internos, iniciados por outros aplicativos ou iniciados pelo sistema operacional). Eles são geralmente usados para atualizar interfaces de usuário, iniciar serviços, atualizar conteúdo e criar notificações para o usuário.

Existem duas maneiras de tornar um Broadcast Receiver conhecido pelo sistema. Uma delas é declará-lo no arquivo Android Manifest. O manifesto deve especificar uma associação entre o Broadcast Receiver e um filtro de intent para indicar as ações que o receiver deve escutar.

Um exemplo de declaração de Broadcast Receiver com um filtro de intent em um manifesto:

<receiver android:name=".MyReceiver" >
    <intent-filter>
        <action android:name="com.owasp.myapplication.MY_ACTION" />
    </intent-filter>
</receiver>

Observe que neste exemplo, o Broadcast Receiver não inclui o atributo android:exported. Como pelo menos um filtro foi definido, o valor padrão será definido como "true". Na ausência de qualquer filtro, será definido como "false".

A outra maneira é criar o receiver dinamicamente no código. O receiver pode então se registrar com o método Context.registerReceiver.

Um exemplo de registro dinâmico de um Broadcast Receiver:

Exemplo em Java:

// Define um broadcast receiver
BroadcastReceiver myReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "Intent received by myReceiver");
    }
};
// Define um filtro de intent com ações que o broadcast receiver escuta
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("com.owasp.myapplication.MY_ACTION");
// Para registrar o broadcast receiver
registerReceiver(myReceiver, intentFilter);
// Para cancelar o registro do broadcast receiver
unregisterReceiver(myReceiver);

Exemplo em Kotlin:

// Define um broadcast receiver
val myReceiver: BroadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.d(FragmentActivity.TAG, "Intent received by myReceiver")
    }
}
// Define um filtro de intent com ações que o broadcast receiver escuta
val intentFilter = IntentFilter()
intentFilter.addAction("com.owasp.myapplication.MY_ACTION")
// Para registrar o broadcast receiver
registerReceiver(myReceiver, intentFilter)
// Para cancelar o registro do broadcast receiver
unregisterReceiver(myReceiver)

Observe que o sistema inicia um aplicativo com o receiver registrado automaticamente quando um intent relevante é disparado.

De acordo com a Visão Geral de Broadcasts, um broadcast é considerado "implícito" se não tiver como alvo um aplicativo específico. Após receber um broadcast implícito, o Android listará todos os aplicativos que registraram uma determinada ação em seus filtros. Se mais de um aplicativo tiver registrado a mesma ação, o Android solicitará que o usuário selecione entre a lista de aplicativos disponíveis.

Uma característica interessante dos Broadcast Receivers é que eles podem ser priorizados; dessa forma, um intent será entregue a todos os receivers autorizados de acordo com sua prioridade. Uma prioridade pode ser atribuída a um filtro de intent no manifesto através do atributo android:priority, bem como programaticamente através do método IntentFilter.setPriority. No entanto, observe que receivers com a mesma prioridade serão executados em uma ordem arbitrária.

Se seu aplicativo não deve enviar broadcasts entre aplicativos, use um Local Broadcast Manager (LocalBroadcastManager). Eles podem ser usados para garantir que os intents sejam recebidos apenas do aplicativo interno, e qualquer intent de qualquer outro aplicativo será descartado. Isso é muito útil para melhorar a segurança e a eficiência do aplicativo, pois não envolve comunicação entre processos. No entanto, observe que a classe LocalBroadcastManager está obsoleta e o Google recomenda o uso de alternativas como LiveData.

Para mais considerações de segurança sobre Broadcast Receiver, consulte Considerações de Segurança e Melhores Práticas.

Limitação de Broadcast Receiver Implícito

De acordo com Otimizações em Segundo Plano, aplicativos direcionados ao Android 7.0 (nível de API 24) ou superior não recebem mais o broadcast CONNECTIVITY_ACTION, a menos que registrem seus Broadcast Receivers com Context.registerReceiver(). O sistema também não envia os broadcasts ACTION_NEW_PICTURE e ACTION_NEW_VIDEO.

De acordo com Limitações de Execução em Segundo Plano, aplicativos que direcionam ao Android 8.0 (nível de API 26) ou superior não podem mais registrar Broadcast Receivers para broadcasts implícitos em seu manifesto, exceto para aqueles listados em Exceções de Broadcast Implícito. Os Broadcast Receivers criados em tempo de execução através da chamada Context.registerReceiver não são afetados por esta limitação.

De acordo com Alterações em Broadcasts do Sistema, a partir do Android 9 (nível de API 28), o broadcast NETWORK_STATE_CHANGED_ACTION não recebe informações sobre a localização do usuário ou dados pessoalmente identificáveis.

Publicação de Aplicativos Android

Após o desenvolvimento bem-sucedido de um aplicativo, o próximo passo é publicá-lo e compartilhá-lo com outros usuários. No entanto, os aplicativos não podem simplesmente ser adicionados a uma loja e compartilhados — eles precisam primeiro ser assinados. A assinatura criptográfica serve como uma marca verificável colocada pelo desenvolvedor do aplicativo. Ela identifica o autor do aplicativo e garante que o aplicativo não foi modificado desde sua distribuição inicial.

Processo de Assinatura

Durante o desenvolvimento, os aplicativos são assinados com um certificado gerado automaticamente. Este certificado é inerentemente inseguro e destina-se apenas à depuração. A maioria das lojas não aceita esse tipo de certificado para publicação; portanto, deve ser criado um certificado com recursos mais seguros.

Quando um aplicativo é instalado em um dispositivo Android, o Gerenciador de Pacotes (Package Manager) garante que ele foi assinado com o certificado incluído no APK correspondente. Se a chave pública do certificado corresponder à chave usada para assinar qualquer outro APK no dispositivo, o novo APK pode compartilhar um UID com o APK pré-existente. Isso facilita as interações entre aplicativos de um mesmo fornecedor. Alternativamente, é possível especificar permissões de segurança no nível de proteção por Assinatura (Signature protection level); isso restringirá o acesso apenas aos aplicativos que foram assinados com a mesma chave.

Esquemas de Assinatura de APK

O Android suporta múltiplos esquemas de assinatura de aplicativos:

  • Abaixo do Android 7.0 (nível de API 24): os aplicativos só podem usar o esquema de assinatura JAR (v1), que não protege todas as partes do APK. Este esquema é considerado inseguro.
  • Android 7.0 (nível de API 24) e superior: os aplicativos podem usar o esquema de assinatura v2, que assina o APK como um todo, fornecendo proteção mais forte em comparação com o método de assinatura v1 (JAR) mais antigo.
  • Android 9 (nível de API 28) e superior: é recomendado usar tanto o esquema de assinatura v2 quanto o v3. O esquema v3 suporta rotação de chaves, permitindo que os desenvolvedores substituam chaves em caso de comprometimento sem invalidar assinaturas antigas.
  • Android 11 (nível de API 30) e superior: os aplicativos podem opcionalmente incluir o esquema de assinatura v4 para permitir atualizações incrementais mais rápidas.

Para compatibilidade com versões anteriores, um APK pode ser assinado com múltiplos esquemas de assinatura para fazer o aplicativo funcionar tanto em versões mais recentes quanto mais antigas do SDK. Por exemplo, plataformas mais antigas ignoram assinaturas v2 e verificam apenas assinaturas v1.

Assinatura JAR (Esquema v1)

A versão original de assinatura de aplicativos implementa o APK assinado como um JAR assinado padrão, que deve conter todas as entradas em META-INF/MANIFEST.MF. Todos os arquivos devem ser assinados com um certificado comum. Este esquema não protege algumas partes do APK, como os metadados ZIP. A desvantagem deste esquema é que o verificador de APK precisa processar estruturas de dados não confiáveis antes de aplicar a assinatura, e o verificador descarta dados que as estruturas não abrangem. Além disso, o verificador de APK deve descomprimir todos os arquivos compactados, o que consome tempo e memória consideráveis.

Este esquema de assinatura é considerado inseguro, sendo afetado, por exemplo, pela vulnerabilidade Janus (CVE-2017-13156), que pode permitir que agentes maliciosos modifiquem arquivos APK sem invalidar a assinatura v1. Portanto, o v1 nunca deve ser confiado para dispositivos que executam Android 7.0 e superior.

Esquema de Assinatura APK (Esquema v2)

Com o esquema de assinatura APK, o APK completo é submetido a hash e assinado, e um Bloco de Assinatura APK é criado e inserido no APK. Durante a validação, o esquema v2 verifica as assinaturas de todo o arquivo APK. Essa forma de verificação de APK é mais rápida e oferece proteção mais abrangente contra modificações. Você pode ver o processo de verificação de assinatura APK para o Esquema v2 abaixo.

Esquema de Assinatura APK (Esquema v3)

O formato do Bloco de Assinatura APK v3 é o mesmo do v2. O v3 adiciona informações sobre as versões de SDK suportadas e uma estrutura de prova de rotação (proof-of-rotation) ao bloco de assinatura APK. No Android 9 (nível de API 28) e superior, os APKs podem ser verificados de acordo com o Esquema de Assinatura APK v3, v2 ou v1. Plataformas mais antigas ignoram assinaturas v3 e tentam verificar a assinatura v2 e depois a v1.

O atributo de prova de rotação (proof-of-rotation) nos dados assinados do bloco de assinatura consiste em uma lista encadeada individual, onde cada nó contém um certificado de assinatura usado para assinar versões anteriores do aplicativo. Para garantir a compatibilidade com versões anteriores, os certificados de assinatura antigos assinam o novo conjunto de certificados, fornecendo assim a cada nova chave evidências de que ela deve ser tão confiável quanto a(s) chave(s) antiga(s). Não é mais possível assinar APKs independentemente, pois a estrutura de prova de rotação deve ter os certificados de assinatura antigos assinando o novo conjunto de certificados, em vez de assiná-los um por um. Você pode ver o processo de verificação do esquema de assinatura APK v3 abaixo.

Esquema de Assinatura APK (Esquema v4)

O Esquema de Assinatura APK v4 foi introduzido junto com o Android 11 (API nível 30) e exige que todos os dispositivos lançados com Android 11 ou superior tenham o fs-verity habilitado por padrão. O fs-verity é um recurso do kernel Linux usado principalmente para autenticação de arquivos (detecção de modificações maliciosas) devido ao seu cálculo extremamente eficiente de hash de arquivos. As solicitações de leitura só terão sucesso se o conteúdo for verificado em relação a certificados digitais confiáveis que foram carregados no keyring do kernel durante o tempo de inicialização.

A assinatura v4 requer uma assinatura complementar v2 ou v3 e, em contraste com os esquemas de assinatura anteriores, a assinatura v4 é armazenada em um arquivo separado <nome do apk>.apk.idsig. Lembre-se de especificá-la usando a flag --v4-signature-file ao verificar um APK assinado com v4 usando apksigner verify.

Você pode encontrar informações mais detalhadas na documentação para desenvolvedores Android.

Criando Seu Certificado

O Android utiliza certificados públicos/privados para assinar aplicativos Android (arquivos .apk). Certificados são conjuntos de informações; em termos de segurança, as chaves são a parte mais importante desse conjunto. Certificados públicos contêm as chaves públicas dos usuários, e certificados privados contêm as chaves privadas dos usuários. Certificados públicos e privados estão vinculados. Os certificados são únicos e não podem ser regenerados. Observe que, se um certificado for perdido, ele não poderá ser recuperado, tornando impossível atualizar quaisquer aplicativos assinados com esse certificado.

Os criadores de aplicativos podem reutilizar um par de chaves público/privado existente que esteja em um KeyStore disponível ou gerar um novo par.

No Android SDK, um novo par de chaves é gerado com o comando keytool. O seguinte comando cria um par de chaves RSA com um comprimento de chave de 2048 bits e um tempo de expiração de 7300 dias = 20 anos. O par de chaves gerado é armazenado no arquivo 'myKeyStore.jks', que está no diretório atual:

keytool -genkey -alias myDomain -keyalg RSA -keysize 2048 -validity 7300 -keystore myKeyStore.jks -storepass myStrongPassword

Armazenar com segurança sua chave secreta e garantir que ela permaneça secreta durante todo o seu ciclo de vida é de suma importância. Qualquer pessoa que obtiver acesso à chave poderá publicar atualizações para seus aplicativos com conteúdo que você não controla (adicionando assim recursos inseguros ou acessando conteúdo compartilhado com permissões baseadas em assinatura). A confiança que um usuário deposita em um aplicativo e em seus desenvolvedores é baseada totalmente nesses certificados; portanto, a proteção do certificado e o gerenciamento seguro são vitais para a reputação e a retenção de clientes, e as chaves secretas nunca devem ser compartilhadas com outras pessoas. As chaves são armazenadas em um arquivo binário que pode ser protegido com uma senha; tais arquivos são referidos como KeyStores. As senhas do KeyStore devem ser fortes e conhecidas apenas pelo criador da chave. Por esse motivo, as chaves geralmente são armazenadas em uma máquina de build dedicada à qual os desenvolvedores têm acesso limitado.

Um certificado Android deve ter um período de validade maior que o do aplicativo associado (incluindo versões atualizadas do aplicativo). Por exemplo, o Google Play exigirá que os certificados permaneçam válidos até pelo menos 22 de outubro de 2033.

Assinatura de um Aplicativo

O objetivo do processo de assinatura é associar o arquivo do aplicativo (.apk) à chave pública do desenvolvedor. Para alcançar isso, o desenvolvedor calcula um hash do arquivo APK e o criptografa com sua própria chave privada. Terceiros podem então verificar a autenticidade do aplicativo (por exemplo, o fato de que o aplicativo realmente vem do usuário que afirma ser o originador) ao descriptografar o hash criptografado com a chave pública do autor e verificar se ele corresponde ao hash real do arquivo APK.

Muitos ambientes de desenvolvimento integrado (IDE) integram o processo de assinatura de aplicativos para facilitar o uso pelo usuário. Esteja ciente de que alguns IDEs armazenam chaves privadas em texto claro em arquivos de configuração; verifique isso cuidadosamente caso outras pessoas tenham acesso a tais arquivos e remova as informações se necessário. Aplicativos podem ser assinados a partir da linha de comando com a ferramenta 'apksigner' fornecida pelo Android SDK (API nível 24 e superior). Ela está localizada em [SDK-Path]/build-tools/[version]. Para API 24.0.2 e abaixo, você pode usar 'jarsigner', que faz parte do Java JDK. Detalhes sobre todo o processo podem ser encontrados na documentação oficial do Android; no entanto, um exemplo é fornecido abaixo para ilustrar o ponto.

apksigner sign --out mySignedApp.apk --ks myKeyStore.jks myUnsignedApp.apk

Neste exemplo, um aplicativo não assinado ('myUnsignedApp.apk') será assinado com uma chave privada do KeyStore do desenvolvedor 'myKeyStore.jks' (localizado no diretório atual). O aplicativo se tornará um aplicativo assinado chamado 'mySignedApp.apk' e estará pronto para ser lançado nas lojas.

Zipalign

A ferramenta zipalign deve ser sempre utilizada para alinhar o arquivo APK antes da distribuição. Esta ferramenta alinha todos os dados não comprimidos (como imagens, arquivos brutos e limites de 4 bytes) dentro do APK, o que ajuda a melhorar o gerenciamento de memória durante a execução do aplicativo.

O Zipalign deve ser utilizado antes que o arquivo APK seja assinado com o apksigner.

Processo de Publicação

A distribuição de aplicativos a partir de qualquer lugar (seu próprio site, qualquer loja, etc.) é possível porque o ecossistema Android é aberto. No entanto, o Google Play é a loja mais conhecida, confiável e popular, sendo fornecida pela própria Google. A Amazon Appstore é a loja padrão confiável para dispositivos Kindle. Se os usuários desejarem instalar aplicativos de terceiros de uma fonte não confiável, eles devem permitir isso explicitamente nas configurações de segurança do dispositivo.

Os aplicativos podem ser instalados em um dispositivo Android a partir de várias fontes: localmente via USB, pela loja oficial de aplicativos da Google (Google Play Store) ou por meio de lojas alternativas.

Enquanto outros fornecedores podem revisar e aprovar aplicativos antes de serem publicados, a Google simplesmente verifica a presença de assinaturas de malware conhecidas; isso minimiza o tempo entre o início do processo de publicação e a disponibilidade pública do aplicativo.

Publicar um aplicativo é bastante simples; a operação principal é disponibilizar o arquivo APK assinado para download. No Google Play, a publicação começa com a criação de uma conta e é seguida pelo envio do aplicativo por meio de uma interface dedicada. Os detalhes estão disponíveis na documentação oficial do Android.