Aplicações em Flutter

Por Ramon Martins

Frida - Flutter & Kotlin - Parte 1

Essa série de artigos é sobre métodos de análise estática e dinâmica em aplicativos criados pelo framework Flutter, utilizando as linguagens Dart e Kotlin para construir aplicações para dispositivos móveis, com o sistema operacional Android.

Aplicação Flutter

Foi criado uma aplicação em Flutter, utilizando a linguagem Dart e Kotlin chamando funções nativas de uma lib criada em c.

Vamos analisar como o aplicativo se apresenta no Mobsf, é utilizado apktool e jadx em linha de comando em conjunto com várias outras ferramentas.

centralized Apresentação da aplicação no Mobsf

centralized Activity

centralized Carregamento de shared objects

Bibliotecas nativa em C que foram reportados pela ferramenta:

centralized Library Flutter

centralized Library JNI

A aplicação não tem muita funcionalidade, existe um botão quando clicado que manda um número aleátorio para tela do celular, esse botão também é responsável por chamar uma função via JNI (Java Native Interface)

Aplicação Android

Esse é a forma com que a função via JNI foi chamada pelo Kotlin e como ela se torna disponível para o Flutter.

centralized Decompilação código Kotlin em Java

Código fonte direto do Android Studio:

centralized Código fonte kt - Kotlin

Esse é o mesmo trecho de código mas o primeiro seria da decompilação feita pelo Mobsf e o último é o código fonte do aplicativo.

Por enquanto temos funções simples e podemos vê o funcionamento delas utilizando Frida, sem maiores mudanças pois até agora não temos razão para aprofundar a análise estática.

Testando com Frida

adb connect 192.168.107.45:40097

adb -s 192.168.107.45:40097 install app-debug.apk

Vamos deixar o servidor rodando por meio do termux

adb -s 192.168.107.45:40097 shell 

run-as com.termux files/usr/bin/bash -lic 'export PATH=/data/data/com.termux/files/usr/bin:$PATH; export LD_PRELOAD=/data/data/com.termux/files/usr/lib/libtermux-exec.so; bash -i'

$ su
$ cd /data/loca/tmp
$ ./frida-server -l 0.0.0.0

Shell utilizando python para acessar com Frida.


$ source /Users/ramonmartins/.frida-env/bin/activate
$ frida version
     ____
    / _  |   Frida 16.1.8 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Local System (id=local)

Aqui podemos ver Frida funcionando corretamente com o dispositivo Android utilizado.

centralized image_caption

$ frida -l main.js  -H 192.168.107.45:27042 -f com.example.testing_apis

[Remote::com.example.testing_apis ]-> message: {'type': 'send', 'payload': 'Android Version: 13'} data: None

Vamos dar uma olhada em que classes e métodos estão disponíveis para nós.

Process.enumerateModules({
   onMatch: function(module){
      if(modName){
            if(module.name.includes(modName)){
               send('Module name: ' + module.name + " (" + "Base Address: " + module.base.toString() + ")");
            }
      }
   },
   onComplete: function(){}
});
if(Java.available){
    Java.perform(()=>{
        send("Android Version: "+Java.androidVersion)
        Java.enumerateLoadedClasses({
         onMatch: function(className) {
               send("className - "+className);
         },
         onComplete: function() {}
      });
        
    })
}

Se você executou esse código possivelmente nenhuma das classes, modulos que procuramos apareceu, como o libtesting_apis e MainActivity ou FlutterActivity

Podemos adicionar um timeout para finalmente aparecer o que procuramos.

function moduleEnumerate(){
    Process.enumerateModules({
        onMatch: function(module){
            console.log('Module name: ' + module.name + " (" + "Base Address: " + module.base.toString() + ")");
        },
        onComplete: function(){}
    });
}
function enumerateLoadedClasses(lookingClass){
    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            send("className - "+className);
            var classVar = Java.use(className)
            send(classVar.class.getClassLoader())
        },
        onComplete: function() {}
    });
}
setTimeout(function() {
	Java.performNow(function() {
        enumerateLoadedClasses("MainActivity");
        moduleEnumerate("testing_api");
	});
}, 4000);

Resposta: centralized Output do código acima da imagem

Isso ocorre porque não estamos no controle do carregamento das classes e funções do aplicativo, para isso temos que pensar sobre os “carregadores de classe” (ClassLoader etc.) pela nossa análise estática temos apenas um arquivo .dex, isso facilita a procura de quem está responsável pelo carregamento de classes do nosso .dex

dex Imagem do classe.dex da aplicação

Carregadores

O Java Class Loaders é um componente do Java Runtime Environment (JRE) que carrega classes Java em uma Java Virtual Machine (JVM)/Dalvik Virtual Machine (DVM)/Android Runtime (ART). Nem todas as classes são carregadas simultaneamente, nem com o mesmo ClassLoader.

Existem vários tipos de carregamento de classes no Android, sendo eles:

  • PathClassLoader - É usado pelo sistema Android para seus carregadores de classes de sistema e aplicativos
  • DexClassLoader - Carrega tipos de arquivos contendo um arquivo .dex (por exemplo, .jar e .apk ou arquivo .dex diretamente). Esses arquivos .dex (executável Dalvik) contêm bytecode Dalvik.
  • URLClassLoader - É usado para recuperar classes ou recursos através de caminhos URL. Os caminhos que terminam com / são considerados diretórios, caso contrário, são considerados arquivos .jar.

O nosso app mostra três carregadores:

['<instance: java.lang.ClassLoader, $className: dalvik.system.PathClassLoader>', '<instance: java.lang.ClassLoader, $className: java.lang.BootClassLoader>', '<instance: java.lang.ClassLoader, $className: dalvik.system.PathClassLoader>']

Dessa forma podemos esperar os Carregadores de Classe ou a classe responsável por carregar arquivos .so ( Shared Objects ).

Esperando pelo carregamento .so:

function hookClassLoader(){
    var baseDex = Java.use("dalvik.system.BaseDexClassLoader")
    baseDex.findLibrary.implementation = function(name){      
        var ret = this.findLibrary(name)
        if(ret.includes("libflutter.so")){
            send(name)  
            send("Carregando: Lib flutter")  
            send(ret)
            enumerateMethods("MainActivity","*")
        }
        return ret
    }
}

if(Java.available){
    Java.perform(()=>{
        hookClassLoader();
        send("Android Version: "+Java.androidVersion)
        
    })
}
function enumerateMethods(className,method){
    const groups = Java.enumerateMethods('*'+className+'*!'+method)
    console.log(JSON.stringify(groups, null, 2));
}

Resposta: centralized Output do código acima da imagem

De acordo com a Documentação o dalvik.system.BaseDexClassLoader é super classe de dalvik.system.PathClassLoader dessa forma olhamos para o findLibrary e esperamos o libflutter ser carregado, a lógica é de que se o libflutter responsável pelo Layout Visual da aplicação está sendo carregado então as classes em Kotlin já existem na JVM.

Esperando pelo carregamento da Classe:

centralized Código esperando carregamento da Classe

Resposta: Output do código acima da imagem

Conclusão

Temos diversas maneiras para procurar uma classe, método, módulo ou função e aprendemos um pouco sobre a arquitetura das aplicações Android.