Nos últimos anos, o mercado de dispositivos móveis experimentou um crescimento exponencial, revolucionando a forma como as pessoas interagem com a tecnologia. Atualmente, há mais de 6,5 bilhões de dispositivos móveis em uso em todo o mundo, e a previsão é que esse número alcance 7,7 bilhões até 2027. O gráfico abaixo ilustra a participação de mercado dos sistemas operacionais móveis globalmente. Segundo os dados, entre 2009 e 2024, a proporção de dispositivos Android representa 71,65%, mais do que o dobro dos dispositivos iOS, que detêm 27,62% do mercado. Juntamente com o crescimento dos sistemas operacionais móveis, o mercado de aplicações móveis também se expandiu significativamente. No Google Play Store, que distribui aplicativos para Android, estima-se que haja atualmente mais de 3,7 milhões de opções disponíveis, criadas por mais de 1,15 milhão de desenvolvedores.
O vasto número de aplicações móveis para Android no mercado exige uma atenção constante à segurança, uma vez que a proliferação de aplicativos pode aumentar a vulnerabilidade dos usuários a ameaças cibernéticas. Muitas aplicações podem conter vulnerabilidades que comprometem dados pessoais, como senhas e informações financeiras, além de expor dispositivos a riscos de segurança. Essas vulnerabilidades podem ser exploradas por atacantes, tornando essencial que desenvolvedores adotem práticas rigorosas de segurança ao criar e atualizar suas aplicações.
As aplicações para Android são comumente desenvolvidas em Java e Kotlin. A linguagem Java, conhecida por sua segurança e portabilidade, foi projetada com uma abordagem cuidadosa em relação à codificação segura. No entanto, as aplicações Android também podem incluir componentes desenvolvidos em linguagens nativas, como C, C++ e Assembly, que, embora proporcionem melhor desempenho, apresentam riscos de segurança mais elevados. Embora exista uma vasta quantidade de pesquisas focadas na parte em Java das aplicações Android, seja por meio de análise estática ou dinâmica, há uma lacuna notável quando se trata de considerar os componentes desenvolvidos nativamente. Esses componentes, por serem menos monitorados e frequentemente menos seguros, podem apresentar vulnerabilidades críticas que podem ser exploradas.
É nesse contexto que surge o fuzzing, uma técnica poderosa para testar e identificar vulnerabilidades em software. Ao aplicar fuzzing a componentes nativos do Android, como explorado neste documento, os profissionais podem prevenir falhas críticas que comprometeriam milhões de dispositivos ao redor do mundo. A combinação de ferramentas como o AFL++ e o QEMU permite testar, de maneira controlada e eficaz, cenários de vulnerabilidade em aplicações Android, promovendo uma segurança cada vez mais robusta e preparada para enfrentar as ameaças cibernéticas modernas.
Este artigo dá início a uma série sobre fuzzing de código nativo em aplicações Android. Neste primeiro artigo, abordaremos os conceitos fundamentais de fuzzing, o funcionamento dos componentes nativos em aplicações Android, o uso do fuzzer AFL++, e, por fim, criaremos um harness para realizar fuzzing em uma biblioteca de exemplo.
UMA INTRODUÇÃO AO FUZZING
Fuzzing é uma técnica de teste de software que consiste na injeção de entradas malformadas ou imprevisíveis, de modo a detectar vulnerabilidades e problemas de segurança. Essas entradas são geradas e enviadas para o software a ser testado através de uma ferramenta chamada fuzzer. As diferentes estratégias de geração de entrada podem categorizar os fuzzers em:
- Black-box fuzzers: Não requerem acesso ao código fonte da aplicação, focando somente na sua entrada, através da geração de entradas aleatórias, e saída, operando sem compreender os detalhes internos do programa.
- Grey-box fuzzers: Podem operar tanto com acesso parcial ao código fonte quanto com conhecimento limitado sobre o programa, integrando aspectos de black-box fuzzers e white-box fuzzers.
- White-box fuzzers: Dependem do acesso ao código fonte ou a informações internas do programa possibilitando a geração de entradas que exploram o código de maneira direcionada e estruturada.
Além disso, essas entradas podem ser geradas de duas principais maneiras: através do Mutation-Based Fuzzing e do Generation-Based Fuzzing:
- Mutation-Based Fuzzing: As entradas são geradas a partir de modificações de entradas válidas já existentes, explorando variações pequenas e graduais para testar diferentes caminhos de execução do programa.
- Generation-Based Fuzzing: As entradas são geradas completamente novas com base em um modelo ou especificação do formato de entrada. Para isso, é necessário um entendimento do formato de arquivo ou protocolo usado pela aplicação a ser testada.
Como discutido anteriormente, o fuzzer é uma ferramenta essencial para identificar vulnerabilidades e bugs em aplicativos, utilizando entradas variadas e frequentemente inesperadas. Entre suas principais características e funções, destacam-se:
- Geração de entradas: O fuzzer cria ou manipula entradas de dados para o software em teste, utilizando um corpus como base. O corpus é um conjunto de entradas de teste que pode incluir exemplos válidos e inválidos, e serve como ponto de partida para gerar novas entradas. As entradas podem ser criadas aleatoriamente, com base em um modelo específico ou modificando as existentes, dependendo da estratégia adotada pelo fuzzer.
- Execução do Software: O fuzzer alimenta as entradas no software para observar como ele responde a diferentes tipos de dados. Isso pode ser feito de duas formas: diretamente, executando funções específicas ou enviando dados através de protocolos de rede, ou indiretamente, utilizando um harness. O harness é responsável por criar um ambiente válido para a execução da função alvo, ler as entradas fornecidas pelo mecanismo de fuzzing e direcionar a execução para a função alvo com essas entradas como parâmetros. Veremos adiante que como os componentes nativos em aplicações Android são gerados por meio de bibliotecas dinâmicas, é função do harness carregar essas bibliotecas e direcionar as entradas do fuzzer para a função alvo a ser testada.
- Cobertura de Código: O fuzzer mede a cobertura de código para avaliar quais partes do programa são executadas durante os testes. Bitmaps são frequentemente utilizados para representar essa cobertura, permitindo uma visualização clara das áreas analisadas. O objetivo é maximizar a cobertura para explorar o maior número possível de caminhos e condições do software, ajudando a descobrir falhas em áreas que podem não ser testadas com entradas mais comuns.
- Detecção de falhas: Através de técnicas de instrumentação ou monitoramento de eventos, o fuzzer monitora o comportamento do software para detectar falhas como crashes, exceções não tratadas, ou comportamentos inesperados.
- Relatório de Resultados: Quando uma falha é encontrada, o fuzzer gera relatórios que incluem informações sobre a entrada que causou o problema e o tipo de falha detectada. Isso ajuda os desenvolvedores a identificar e corrigir vulnerabilidades ou bugs.
As interações entre esses componentes podem ser vistas na figura abaixo.
Para os nossos testes, usaremos o fuzzer AFL++ juntamente com o QEMU. Como veremos, o AFL++ é uma ferramenta de fuzzing que automatiza a geração de entradas para testar a segurança de programas, identificando falhas e vulnerabilidades. O QEMU é um emulador de hardware e uma máquina virtual que permite executar sistemas operacionais e aplicativos em um ambiente virtualizado, simulando diferentes arquiteturas e configurações de hardware. Combinar o QEMU com o AFL++ permite realizar fuzzing em um ambiente virtualizado, oferecendo a flexibilidade de simular diversas configurações e arquiteturas sem a necessidade de hardware físico. Isso é especialmente importante para o cenário de testes de aplicações para dispositivos móveis, que utilizam arquiteturas de processadores e requisitos distintos dos comumente encontrados em PCs, permitindo que o fuzzing seja realizado em plataformas que replicam com precisão as condições reais dos dispositivos alvo.
A vantagem de usar o QEMU para fuzzing em vez de um dispositivo Android real está na capacidade de simulação e controle que ele oferece. O QEMU permite criar um ambiente virtualizado onde podemos testar a aplicação em um sistema simulado, com a flexibilidade de modificar a configuração e o estado do sistema sem a necessidade de hardware físico. Isso facilita a execução de testes em diferentes configurações e cenários sem riscos de danificar dispositivos reais, além de permitir instrumentação detalhada e análise em tempo real. Adicionalmente, o uso do QEMU permite escalar os testes facilmente, executando múltiplos testes simultaneamente em diferentes instâncias virtuais, o que aumenta a eficiência e a abrangência da análise de segurança.
COMPONENTES NATIVOS DE UMA APLICAÇÃO ANDROID
Uma aplicação escrita em Java ou Kotlin para Android é executada pelo Android Runtime (ART). O Android Runtime é um ambiente de execução que utiliza compilação Just-In-Time (JIT) para converter bytecode de aplicativos Android em código nativo durante a execução, otimizando o desempenho através da pré-compilação de métodos frequentemente utilizados. Embora o Android Runtime ofereça otimizações significativas com a compilação Just-In-Time para o bytecode de aplicativos Android, há situações onde são necessárias operações intensivas de computação, como processamento de sinais, operações de criptografia, operações de rede e simulação física encontradas em engines de jogos como Unity e Unreal Engine. Nestes casos, o Android Runtime pode não proporcionar o desempenho necessário. Felizmente, é possível integrar componentes que executam código nativo diretamente na aplicação, o que pode resultar em melhorias substanciais de performance. Esse recurso é viabilizado pelo Android NDK (Native Development Kit).
O Android NDK é um conjunto de ferramentas que permite ao desenvolvedor implementar partes do código da aplicação em código nativo, usando linguagens como C ou C++. Essas partes podem ser conectadas com códigos escritos em Java ou Kotlin através do uso da JNI (Java Native Interface). O código nativo é compilado em bibliotecas dinâmicas (*.so) e armazenado no diretório lib, dentro do pacote da aplicação Android, onde pode ser carregado dinamicamente durante a execução conforme necessário.
Como mencionado anteriormente, a JNI permite a integração de componentes da aplicação Java se conectarem com componentes escritos em C ou C++. No lado do componente Java, a classe que utilizará a interface JNI carrega a biblioteca dinâmica através do método System.LoadLibrary() em seu inicializador de classe estático, passando seu nome como parâmetro, sem o prefixo lib e a extensão do arquivo. As referências aos métodos implementados nativamente dentro da classe possuem a palavra-chave native antes de sua declaração.
package com.conviso.example.jni;
public class HelloJni {
static {
System.loadLibrary(“hello-jni”);
}
public native String stringFromJNI();
…
}
Do lado do componente nativo, a interação com os componentes Java acontecem através de chamadas de funções JNI. Normalmente, métodos nativos possuem o seguinte formato de assinatura, com todos esses campos separados pelo símbolo underscore (_):
- O prefixo Java;
- O nome do pacote;
- O nome da classe;
- O nome do método, como definido na classe Java.
Vale ressaltar que esses todos os métodos nativos possuem sempre como os dois primeiros parâmetros o ponteiro de ambiente JNI e o objeto Java ao qual o método está anexado, respectivamente JNIEnv * e jobject.
JNIEXPORT jstring JNICALL
Java_com_conviso_example_jni_HelloJni_stringFromJNI(
JNIEnv* env,
jobject thiz
)
{
return (*env)->NewStringUTF(env, "Hello from JNI!");
}
O núcleo do JNI está contido na biblioteca libart.so, que é responsável pela implementação do Android Runtime (ART). Para que o código nativo possa acessar as funções e definições do JNI, é necessário incluir o cabeçalho jni.h. A figura abaixo mostra a interação entre componentes Java e C/C++ através da JNI, implementado na libart.so.
Para aplicar o processo de fuzzing nas bibliotecas dinâmicas mencionadas, será utilizado o Android NDK para o desenvolvimento do harness, em conjunto com o AFL++, o qual será detalhado na próxima seção. A seguir estão os passos para instalação do Android NDK:
- Baixe a última versão do Android NDK:
- Acesse a página de Downloads do Android NDK e baixe a versão mais recente do Android NDK.
- Extraia o arquivo android-ndk-<version>-linux.zip em um diretório adequado
- Configure a variável de ambiente PATH:
- Para facilitar o acesso ao Android NDK de qualquer local no sistema, adicione o caminho para o diretório <Android NDK path>/android-ndk-r27/toolchains/llvm/prebuilt/linux-x86_64/bin à variável de ambiente PATH. Isso pode ser feito editando seu arquivo de configuração de perfil, como .bashrc ou .profile.
- Certifique-se de substituir <Android NDK path> pelo caminho real onde o Android NDK foi extraído.
Pode-se verificar a conclusão bem-sucedida de todas as etapas ao executar o CLANG do Android NDK.
Ao compilar um arquivo de código-fonte com o compilador aarch64-linux-android35-clang, é gerado um arquivo executável destinado à arquitetura ARM64. O comando para compilação pode ser visto abaixo:
$ aarch64-linux-android35-clang helloworld.c -o helloworld
Podemos confirmar que o binário gerado é um arquivo ELF de 64 bits para a arquitetura ARM64 através do comando readelf. Após sua execução, identificamos que o binário depende de duas bibliotecas dinâmicas: libdl.so e libc.so, além do linker dinâmico linker64. Essas dependências são essenciais para a execução do arquivo compilado e podem ser obtidas através do Qiling Framework.
O Qiling Framework é um framework de emulação e instrumentação binária de código aberto, desenvolvido sobre o Unicorn, um emulador de CPU que se limita a emular instruções brutas, sem o contexto do sistema operacional. Enquanto o Unicorn lida com a emulação de baixo nível, o Qiling Framework é responsável pelas tarefas de alto nível, incluindo o suporte a diferentes formatos de arquivos executáveis, linkers dinâmicos e tratadores de chamadas de sistema e entrada/saída. Isso permite que o Qiling execute binários que normalmente requerem um sistema operacional nativo. Como o Qiling Framework possui suporte a arquivos binários ARM64 do Android, podemos encontrar essas bibliotecas dinâmicas dentro da estrutura do projeto. Os passos para o download do projeto estão descritos abaixo.
$ git clone https://github.com/qilingframework/qiling
$ cd qiling
$ git submodule update --init --recursive
Após a instalação do Qiling Framework, podemos verificar a presença dos arquivos necessários para a execução da nossa aplicação no diretório examples/rootfs/arm64_android/system.
AFL++ (AMERICAN FUZZY LOP PLUS PLUS)
O AFL++ é um fuzzer avançado derivado do AFL, originalmente criado por Michael Zalewski para análise de cobertura de código e pesquisa de vulnerabilidades enquanto estava no Google. Como um fork aprimorado do AFL, o AFL++ proporciona maior velocidade, uma variedade mais ampla de opções de configuração, mutações mais eficazes e uma instrumentação de código aprimorada. Além disso, oferece suporte a módulos personalizados e outras funcionalidades avançadas. Uma vantagem adicional é o gerenciamento eficiente dos crash dumps, facilitando a análise e a triagem de falhas detectadas durante o fuzzing.
Para mais detalhes sobre o AFL++, consulte a documentação oficial do projeto. Entre as diversas opções de instrumentação de código disponíveis, o AFL++ inclui suporte para módulos como QEMU, Unicorn e Frida, que são significativamente úteis para o fuzzing de componentes nativos do Android. A seguir, estão os passos para a compilação do AFL++:
$ git clone https://github.com/AFLplusplus/AFLplusplus
$ cd AFLplusplus
$ make distrib
Após compilar o AFL++ e habilitar o suporte ao QEMU para a arquitetura ARM64, você estará pronto para iniciar os testes com o AFL++. Durante o processo de fuzzing, é necessário instruir o AFL++ a forçar o QEMU a ignorar os manipuladores de sinal registrados para a aplicação alvo. Caso contrário, o AFL++ não será capaz de detectar quando a aplicação sofrer um crash. Para isso, você pode habilitar a flag AFL_QEMU_FORCE_DFL (definindo AFL_QEMU_FORCE_DFL=1) antes de executar o afl-fuzz, ou então aplicar um patch para desabilitar permanentemente os manipuladores de sinal registrados. Neste artigo, optamos por aplicar o patch para garantir que os manipuladores de sinal sejam permanentemente desabilitados.
A dica sobre o uso da flag AFL_QEMU_FORCE_DFL foi fornecida por Andrea Fioraldi, a quem agradecemos por sua valiosa contribuição.
Se optar por aplicar o patch, será necessário modificar os dois arquivos abaixo, cujos procedimentos estão detalhados a seguir. Vale ressaltar que, caso seja necessário fazer debugging da aplicação alvo, a linha da chamada para signal_init() deve ser descomentada. Caso contrário, o debugger não será capaz de receber os sinais emitidos durante a execução da aplicação.
- AFLplusplus/qemu_mode/qemuafl/linux-user/main.c: comentar a chamada para a função signal_init(). A função signal_init() no QEMU configura o gerenciamento de sinais das aplicações do usuário, incluindo sinais fatais, e define manipuladores básicos para eles. A emulação de sinais é importante para garantir que o QEMU simule e responda corretamente a eventos e interrupções, como se a aplicação estivesse sendo executada pelo sistema operacional da arquitetura emulada. A emulação e gerenciamento de sinais feitos pelo QEMU impedem que o AFL++ receba sinais fatais do harness, dificultando a identificação de crashes para determinadas entradas.
Para isso, precisamos fazer uma pequena alteração em dois arquivos do código-fonte do QEMU:
- AFLplusplus/qemu_mode/build_qemu_support.sh: comentar todos os comandos git no script de build do QEMU. O script executa comandos que podem sobrescrever modificações locais, por isso, para garantir que suas alterações no main.c não sejam perdidas, é essencial comentar esses comandos antes de rodar o script.
Depois de fazer as alterações nesses arquivos, podemos seguir para a compilação do suporte ao QEMU utilizando os comandos abaixo:
$ sudo apt install ninja-build
$ cd qemu_mode
$ CPU_TARGET=aarch64 ./build_qemu_support.sh
Podemos testar a instalação do AFL++ executando o comando afl-fuzz. A saída do comando é descrita abaixo.
CRIAÇÃO DO HARNESS E FUZZING COM AFL++
Com o AFL++ devidamente compilado e em funcionamento, podemos prosseguir com a criação do harness e iniciar o processo de fuzzing. Para os nossos testes, assumiremos que temos acesso à biblioteca dinâmica de uma aplicação Android, cujo código em C é apresentado abaixo. Optamos por essa abordagem para simplificar o processo e evitar a complexidade da engenharia reversa de código ARM64, o que poderá ser abordado em um artigo futuro. Num cenário real, seria necessário identificar uma função alvo apropriada na biblioteca dinâmica e realizar a análise do código utilizando ferramentas de disassembly, como IDA Pro, Ghidra, radare2 ou Hopper. A função strcpy copia uma string de origem para um destino, mas pode causar corrupção de memória se o destino não tiver espaço suficiente para armazenar a string de origem, resultando em sobreposições de memória e possíveis vulnerabilidades de segurança.
#include <stdio.h>
#include <string.h>
int checkBuffer(const char *data)
{
char localBuffer[256];
if (data[0] == 'c') {
if (data[1] == 'o') {
if (data[2] == 'n') {
if (data[3] == 'v') {
strcpy(localBuffer, data);
return 0;
}
}
}
}
return 1;
}
Representação do código de checkBuffer em C, contido na biblioteca libfuzzconviso.so
Caso o leitor deseje replicar o experimento deste artigo e não possua uma biblioteca alvo, é possível gerar a biblioteca dinâmica libfuzzconviso.so, que usaremos como demonstração neste artigo, através do comando abaixo.
$ aarch64-linux-android35-clang libfuzzconviso.c -o libfuzzconviso.so -shared -fPIC
Depois de obter e analisar a biblioteca dinâmica, criaremos um harness que carregará a biblioteca dinâmica e passará as entradas mutadas geradas pelo AFL++ como argumentos para a função alvo. No caso do nosso experimento, a função checkBuffer. O AFL++ irá monitorar o comportamento do harness para identificar possíveis falhas ou crashes na aplicação. Abaixo está o código do harness desenvolvido. O harness irá obter o caminho do arquivo gerado pelo AFL++, fará a leitura do seu conteúdo em um buffer e o passará para a função alvo.
#include <stdio.h>
extern int checkBuffer(char *);
int main(int argc, char *argv[])
{
char buffer[4096];
FILE *fp = fopen(argv[1], "r");
fread(buffer, 4096, 1, fp);
checkBuffer(buffer);
fclose(fp);
return 0;
}
harness.c
Podemos compilar o harness usando o comando abaixo. Isso também vincula o harness com a biblioteca dinâmica libfuzzconviso.so, que deve estar localizada no diretório atual.
$ aarch64-linux-android35-clang harness.c -o harness -lfuzzconviso -L .
De posse da biblioteca dinâmica da aplicação e do harness, precisamos configurar duas variáveis de ambiente:
- QEMU_LD_PREFIX: configura o local onde o QEMU busca bibliotecas compartilhadas para a arquitetura emulada. É necessário definir essa variável com o caminho para o diretório /system do Android, incluído no Qiling Framework, permitindo a execução de binários compilados para Android no QEMU. O diretório /system no Android contém arquivos essenciais do sistema operacional, incluindo binários, bibliotecas e aplicativos do sistema.
- QEMU_SET_ENV: configura as variáveis de ambiente para o processo que será emulado pelo QEMU. No contexto de fuzzing com AFL++ e QEMU, iremos especificar a variável de ambiente LD_LIBRARY_PATH, que define o caminho para as bibliotecas dinâmicas adicionais usadas pelo seu harness, como o diretório que contém a libfuzzconviso.so.
$ export QEMU_LD_PREFIX="/home/thiago/Conviso/qiling/examples/rootfs/arm64_android/"
$ export QEMU_SET_ENV=LD_LIBRARY_PATH="/home/thiago/Conviso/workspace/"
Após compilar o harness e configurar as variáveis de ambiente necessárias para a execução do QEMU com o AFL++, criaremos um diretório para armazenar o corpus utilizado no fuzzing. Em um cenário real, o corpus geralmente é escolhido com base no tipo de target que será fuzzeado, para maximizar a cobertura e a eficiência do fuzzing. No entanto, para o exemplo do checkBuffer, um arquivo de texto simples é suficiente. Inicialmente, o diretório conterá apenas um arquivo de texto que representará o conteúdo do buffer a ser passado para a função checkBuffer. Esse arquivo será modificado pelo AFL++ e utilizado como entrada para a função alvo na biblioteca dinâmica da aplicação Android através do harness.
$ mkdir afl_in
$ echo "AAAA" > afl_in/input.txt
Com o corpus devidamente preparado, podemos começar o processo de fuzzing utilizando o afl-fuzz através do comando abaixo.
$ AFL_INST_LIBS=1 afl-fuzz -Q -i afl_in/ -o afl_out/ -- ./harness @@
Antes de executarmos o afl-fuzz, vamos decompor as opções passadas para afl-fuzz em partes para entender o que cada parte faz:
- AFL_INST_LIBS=1: esta variável de ambiente configura o AFL++ para instrumentar código contido em bibliotecas dinâmicas. Como o nosso objetivo é instrumentar a libfuzzconviso.so, precisamos configurar essa variável de ambiente.
- -Q: esta opção diz ao AFL++ para usar o modo de instrumentação do QEMU para monitorar e analisar o comportamento da aplicação alvo durante a execução.
- -i afl_in/: especifica o diretório de entrada onde estão os arquivos de corpus que o AFL++ usará para o processo de fuzzing.
- -o afl_out/: define o diretório onde o AFL++ armazenará os resultados do fuzzing, como casos de teste que causaram falhas e logs de execução.
- —: o – é um separador que indica o fim das opções do afl-fuzz e o início da linha de comando da aplicação alvo que será executada, no caso, o harness.
- ./harness: especifica o caminho para a aplicação a ser executada durante o processo de fuzzing.
- @@: é um placeholder utilizado pelo AFL++ que será substituído pelo caminho do arquivo de entrada gerado pelo fuzzer durante a execução.
Ao executar o AFL++ pela primeira vez, você pode encontrar o seguinte erro. A mensagem indica que, devido à configuração do sistema para enviar notificações de core dump para um serviço externo, o AFL++ pode interpretar erroneamente falhas (crashes) como timeouts, devido à maneira como a função `waitpid()` opera. Para corrigir esse problema, execute o comando sugerido: echo core > /proc/sys/kernel/core_pattern.
Após um período de execução do AFL++, você começará a observar que os crashes são registrados no campo “total crashes” da interface do AFL++. A presença desses crashes indica que o AFL++ encontrou condições de falha na aplicação testada, que podem ser analisadas para identificar vulnerabilidades ou comportamentos indesejados no código.
As entradas que causam crashes na aplicação são armazenadas no diretório ./afl_out/default/crashes. Este diretório contém exemplos dos dados de entrada que levaram a falhas durante o processo de fuzzing. Na imagem abaixo, vemos um exemplo de uma dessas entradas. Vale ressaltar que a entrada em questão começa com a string “conv”. Isso indica que o fuzzer conseguiu satisfazer todas as checagens e condições necessárias até atingir o trecho de código potencialmente inseguro da nossa biblioteca — neste caso, a função strcpy. Essa capacidade do fuzzer de explorar diferentes caminhos de execução foi possível graças à cobertura de código, que permite ao fuzzer exercitar diversas partes do programa. Quanto mais caminhos de execução o fuzzer conseguir testar, maior a probabilidade de encontrar vulnerabilidades e problemas ocultos.
Para testar uma entrada gerada pelo AFL++ e verificar a resposta da aplicação, usamos o afl-qemu-trace para executá-la com uma entrada específica. O afl-qemu-trace instrumenta a execução da aplicação, permitindo monitorar se a entrada causa algum erro ou falha. No caso abaixo, podemos confirmar que a entrada causou uma falha de segmentação na aplicação.
CONCLUSÃO
A crescente diversidade de aplicações móveis para Android enfatiza a necessidade urgente de priorizar a segurança em um ambiente cada vez mais suscetível a ameaças cibernéticas. Embora linguagens como Java e Kotlin proporcionem uma base sólida em termos de segurança, a inclusão de componentes nativos em C, C++ e Assembly introduz riscos adicionais que não podem ser ignorados. Nesse contexto, o uso de fuzzing com ferramentas como AFL++ e QEMU se destaca como uma abordagem eficaz para identificar e mitigar vulnerabilidades comuns em códigos de baixo nível, que muitas vezes são difíceis de detectar.
No próximo artigo, exploraremos um cenário real, onde identificaremos os componentes nativos da aplicação, criaremos e discutiremos estratégias para a elaboração de harnesses, além de analisarmos como as vulnerabilidades se manifestam em uma aplicação Android.
REFERÊNCIAS
https://www.iprog.it/blog/sicurezza-informatica/mobile-security-harnessing-afl-for-fuzz-testing/
https://asrp.darkwolf.io/ASRP-Plays/fuzz
https://aflplus.plus/building/
https://www.statista.com/statistics/272698/global-market-share-held-by-mobile-operating-systems-since-2009/
https://www.mobiloud.com/blog/mobile-app-statistics
https://www.sidechannel.blog/en/afl-and-an-introduction-to-feedback-based-fuzzing/
https://developer.android.com/ndk/samples
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
