MASTG-TECH-0112: Engenharia Reversa de Aplicações Flutter
O Flutter é um SDK de UI de código aberto do Google para a criação de aplicativos compilados nativamente para dispositivos móveis, web e desktop a partir de uma única base de código. Dart, a linguagem de programação utilizada no Flutter, é fundamental para seu funcionamento, oferecendo recursos de linguagem e otimizações de desempenho que possibilitam o desenvolvimento eficiente de aplicativos multiplataforma de alta qualidade.
Um snapshot do Dart é uma representação pré-compilada de um programa Dart que permite tempos de inicialização mais rápidos e execução eficiente. O desenvolvimento de aplicativos Flutter concentra-se no snapshot AOT (Ahead-of-Time), utilizado em todos os aplicativos móveis Flutter.
Existem desafios significativos na engenharia reversa de snapshots AOT do Dart devido a vários fatores:
- Código Assembly Distintivo: O código assembly gerado utiliza registradores, convenções de chamada e codificação de inteiros únicos, complicando a análise.
- Informações Sequenciais de Classes: As informações sobre cada classe no snapshot AOT do Dart devem ser lidas sequencialmente, impedindo acesso aleatório e tornando demorada a localização de classes específicas.
- Falta de Documentação: O formato do snapshot Dart carece de documentação abrangente e evoluiu ao longo do tempo, aumentando a complexidade.
- Ofuscação e Otimização: O processo de build do Flutter pode incluir técnicas de ofuscação e otimização que dificultam os esforços de engenharia reversa.
Devido a esses desafios, a análise eficaz de aplicativos Flutter requer ferramentas e métodos especializados.
Usando o Blutter¶
Para usar Blutter, você precisa:
- Extrair o APK: Descompacte o arquivo APK e localize o arquivo libapp.so.
- Executar o Blutter: Execute o Blutter com o caminho para o arquivo libapp.so e especifique um diretório de saída.
python3 blutter.py caminho/para/app/lib/arm64-v8a diretorio_saida
O Blutter gera vários arquivos:
asm/*: Arquivos assembly com símbolos.blutter_frida.js: Um template de script Frida para instrumentar o aplicativo.objs.txt: Um dump completo e aninhado de objetos do pool de objetos.pp.txt: Todos os objetos Dart no pool de objetos.
Os arquivos assembly em asm/* contêm funções reconstruídas com nomes, facilitando o rastreamento da lógica do aplicativo. Aqui está um trecho de uma função main:
static _ main(/* Sem informações */) async {
// ** endereço: 0x5961e0, tamanho: 0x230
// 0x5961e0: EnterFrame
// 0x5961e0: stp fp, lr, [SP, #-0x10]!
// 0x5961e4: mov fp, SP
// 0x5961e8: AllocStack(0x28)
// 0x5961e8: sub SP, SP, #0x28
// 0x5961ec: SetupParameters()
// 0x5961ec: stur NULL, [fp, #-8]
// 0x5961f0: CheckStackOverflow
// 0x5961f0: ldr x16, [THR, #0x38] ; THR::stack_limit
// 0x5961f4: cmp SP, x16
// 0x5961f8: b.ls #0x596400
// 0x5961fc: InitAsync() -> Future<void?>
// 0x5961fc: ldr x0, [PP, #0x80] ; [pp+0x80] TypeArguments: <void?>
// 0x596200: bl #0x3a5d48
// 0x596204: r0 = ensureInitialized()
// 0x596204: bl #0x570d8c ; [package:flutter/src/widgets/binding.dart] WidgetsFlutterBinding::ensureInitialized
// 0x596208: r0 = init()
// 0x596208: bl #0x59a98c ; [package:get_secure_storage/src/storage_impl.dart] GetSecureStorage::init
// 0x59620c: mov x1, x0
// 0x596210: stur x1, [fp, #-0x10]
// 0x596214: r0 = Await()
Embora esse código não seja tão fácil de entender quanto o código Java descompilado típico, muitas informações ainda estão disponíveis. No topo, podemos ver o nome da função (main), bem como a localização da função no binário original libapp.so. As diferentes instruções de salto (bl) são acompanhadas por informações de símbolos, facilitando a compreensão do que o código está fazendo. Por exemplo, podemos ver que o aplicativo primeiro garante que as vinculações do Flutter estejam corretamente inicializadas (WidgetsFlutterBinding::ensureInitialized), seguido pela inicialização do plugin get_secure_storage (GetSecureStorage::init).