Beerus Framework – Um Novo Framework Mobile Surge

Escrito por  Tricta, Daniel Franca Lima

Abstract

O Beerus Framework é uma ferramenta ofensiva mobile desenvolvida para facilitar todo o processo de pentest em dispositivos Android. Com uma interface unificada diretamente no dispositivo, o Beerus permite realizar desde a instrumentação de aplicações de forma built-in no dispositivo com Frida Core, exfiltração de dados do sandbox, memory dumping, proxying, controle de módulos Magisk, manipulação de propriedades e muito mais.

Construído sobre Frida e Magisk, o Beerus é modular, extensível e projetado para testes em dispositivos com root, otimizando tarefas comuns de pentest e habilitando automações a partir de um único app.

Neste paper, exploramos as principais funcionalidades do framework, com foco especial em algumas delas. A proposta não é detalhar exaustivamente seu funcionamento, mas oferecer uma visão ampla do que ele abrange e do que é capaz. Para complementar, disponibilizamos abaixo um vídeo demonstrativo que mostra como utilizar o Beerus Framework no dia a dia.

Lembrando que o Beerus Framework já está disponível para download diretamente no repositório oficial no GitHub.

The Development of Beerus Framework

Durante avaliações de segurança em dispositivos móveis, é comum depender de diversas ferramentas externas e setups complexos. O Beerus Framework nasceu com o objetivo de unificar as funcionalidades essenciais de um pentester mobile em um único aplicativo Android. A proposta é oferecer uma solução prática e eficiente, que torne o processo de testagem mais fluido e ágil.

Pensando nisso, decidimos expandir o projeto Beerus, idealizado inicialmente por Lucas “luriel” Carmo e Daniel “d3v” Chactoura, dando origem ao Beerus Framework. O objetivo é oferecer funcionalidades que simplifiquem e agilizem o trabalho diário do analista de segurança.

Introdução Técnica

O Beerus Framework funciona como um hub de funcionalidades para analistas de segurança que trabalham com dispositivos Android. O aplicativo fornece uma interface gráfica intuitiva com acesso a diversas ferramentas, aproveitando recursos do próprio dispositivo e privilégios root (desenhado para funcionar em melhor sincronia com o Magisk).

Entre as funcionalidades oferecidas, estão:

Frida Server Setup

A funcionalidade Frida Server Setup permite baixar e inicializar o Frida Server diretamente no seu dispositivo mobile. Basta escolher uma versão e clicar em “Run” para que o servidor seja instalado e executado. Após a instalação, não é necessário reinstalar a mesma versão, a menos que você opte por trocar. Por padrão, o Beerus Framework exibirá as 10 versões mais recentes, mas também é possível inserir manualmente a versão desejada. Isso torna o processo de configuração do ambiente de instrumentação muito mais simples.

Frida Auto Inject

A funcionalidade Frida Auto Inject possibilita a injeção de scripts Frida diretamente do dispositivo mobile por meio do Frida Core, motor de interação via RPC utilizado tanto nas versões Python quanto JavaScript do Frida. Ele se comunica com o Frida Server, que pode ser iniciado pela própria funcionalidade do Beerus Framework, facilitando a portabilidade e o compartilhamento de exploits para instrumentação de aplicações.

Podemos acessar o Frida Setup, iniciar o Frida Server e, logo em seguida, criar ou importar um script para o armazenamento do dispositivo. Após adicionar o script, é possível editá-lo, selecionar o APK alvo e clicar em “Run” para que o app seja iniciado já com o script injetado via Frida Core.

Dentro do próprio repositório do Frida, podemos ver, em releases, um pacote chamado frida-core-devkit, disponível para diferentes arquiteturas. O Frida Core é utilizado para interagir com o Frida Server da seguinte forma:

  • O cliente (seu programa em Python/Node/C usando frida-core) abre um socket para o frida-server.
  • Envia mensagens no formato JSON-RPC encapsuladas em um framing binário (protocolo Frida).
  • O frida-server recebe a mensagem e a repassa para sua instância de Frida Core local.
  • O cliente envia attach(pid)serverCore injeta o Frida Gadget no alvo.
  • O Gadget, que também roda o Frida Core, abre um canal RPC interno com o server.
  • Agora o server atua como proxy entre:
    • Cliente Core (sua tool)
    • Gadget Core (dentro do alvo)

Então o Beerus possui uma versão modificada desse devkit, para aceitar que passemos os scripts JS como parâmetro para um binário compilado do Frida Core:

/*
 * Compile with:
 *
 * clang -DANDROID -ffunction-sections -fdata-sections frida-core-example.c -o frida-core-example -L. -lfrida-core -llog -ldl -lm -latomic -pthread -Wl,--export-dynamic
 *
 * Visit https://frida.re to learn more about Frida.
 */

#include "frida-core.h"

#include <stdlib.h>
#include <string.h>

static void on_detached (FridaSession * session, FridaSessionDetachReason reason, FridaCrash * crash, gpointer user_data);
static void on_message (FridaScript * script, const gchar * message, GBytes * data, gpointer user_data);
static void on_signal (int signo);
static gboolean stop (gpointer user_data);

static GMainLoop * loop = NULL;

int
main (int argc,
      char * argv[])
{
    guint target_pid;
    FridaDeviceManager * manager;
    GError * error = NULL;
    FridaDeviceList * devices;
    gint num_devices, i;
    FridaDevice * local_device;
    FridaSession * session;
    gchar * script_source;
    gsize script_size;

    frida_init ();

    if (argc != 3 || (target_pid = atoi (argv[1])) == 0)
    {
        g_printerr ("Usage: %s <pid> <script.js>\n", argv[0]);
        return 1;
    }

    if (!g_file_get_contents(argv[2], &script_source, &script_size, &error)) {
        g_printerr ("Failed to read script: %s\n", error->message);
        g_error_free (error);
        return 1;
    }

    loop = g_main_loop_new (NULL, TRUE);

    signal (SIGINT, on_signal);
    signal (SIGTERM, on_signal);

    manager = frida_device_manager_new ();

    devices = frida_device_manager_enumerate_devices_sync (manager, NULL, &error);
    g_assert (error == NULL);

    local_device = NULL;
    num_devices = frida_device_list_size (devices);
    for (i = 0; i != num_devices; i++)
    {
        FridaDevice * device = frida_device_list_get (devices, i);
        g_print ("[*] Found device: \"%s\"\n", frida_device_get_name (device));

        if (frida_device_get_dtype (device) == FRIDA_DEVICE_TYPE_LOCAL)
            local_device = g_object_ref (device);

        g_object_unref (device);
    }
    g_assert (local_device != NULL);

    frida_unref (devices);
    devices = NULL;

    session = frida_device_attach_sync (local_device, target_pid, NULL, NULL, &error);
    if (error == NULL)
    {
        FridaScript * script;
        FridaScriptOptions * options;

        g_signal_connect (session, "detached", G_CALLBACK (on_detached), NULL);
        if (frida_session_is_detached (session))
            goto session_detached_prematurely;

        g_print ("[*] Attached\n");

        options = frida_script_options_new ();
        frida_script_options_set_name (options, "example");
        frida_script_options_set_runtime (options, FRIDA_SCRIPT_RUNTIME_QJS);

        script = frida_session_create_script_sync (session, script_source, options, NULL, &error);
        g_assert (error == NULL);

        g_clear_object (&options);
        g_free (script_source);

        g_signal_connect (script, "message", G_CALLBACK (on_message), NULL);

        frida_script_load_sync (script, NULL, &error);
        g_assert (error == NULL);

        g_print ("[*] Script loaded\n");

        if (g_main_loop_is_running (loop))
            g_main_loop_run (loop);

        g_print ("[*] Stopped\n");

        frida_script_unload_sync (script, NULL, NULL);
        frida_unref (script);
        g_print ("[*] Unloaded\n");

        frida_session_detach_sync (session, NULL, NULL);
        session_detached_prematurely:
        frida_unref (session);
        g_print ("[*] Detached\n");
    }
    else
    {
        g_printerr ("Failed to attach: %s\n", error->message);
        g_error_free (error);
    }

    frida_unref (local_device);
    frida_device_manager_close_sync (manager, NULL, NULL);
    frida_unref (manager);
    g_print ("[*] Closed\n");

    g_main_loop_unref (loop);

    return 0;
}

...

Frida Header (LINHA: 9)

  • fornece as APIs principais do Frida através do “frida-core.h”.

Static callbacks declarations (LINHAS: 14…17)

  • on_detached(...): callback para tratar quando a sessão é desconectada.
  • on_message(...): callback para mensagens vindas do script JS.
  • on_signal(...): captura sinais como SIGINT e SIGTERM.
  • stop(...): usado para encerrar o loop principal.

Global Variable GMainLoop (LINHA: 19)

  • Armazena o loop principal GLib que processa eventos assíncronos.

Inicialization (LINHAS: 21…52)

  • Declara variáveis locais (PID alvo, script JS, manager, devices, sessão, etc.).
  • Chama frida_init().
  • Valida argumentos: espera <pid> <script.js>. Se inválido, imprime Usage e encerra.
  • Lê o script JS (argv[2]) para a variável script_source com g_file_get_contents.
  • Cria o loop principal g_main_loop_new.
  • Registra signal handlers para SIGINT e SIGTERM.

Device Management (LINHAS: 54…74)

  • Cria FridaDeviceManager com frida_device_manager_new().
  • Enumera dispositivos com frida_device_manager_enumerate_devices_sync.
  • Itera dispositivos encontrados: imprime nome (frida_device_get_name).
  • Se for FRIDA_DEVICE_TYPE_LOCAL, guarda referência como local_device.
  • Libera lista de devices com frida_unref(devices).

Attaching to Session (LINHAS: 76…86)

  • Chama frida_device_attach_sync para anexar ao processo alvo.
  • Se sucesso, conecta callback "detached".
  • Verifica se a sessão foi desconectada prematuramente.
  • Imprime [*] Attached em caso de sucesso.

Script Creation and Loading (LINHAS: 88…106)

  • Cria opções de script (FridaScriptOptions): define nome "example" e runtime QJS.
  • Cria script com frida_session_create_script_sync.
  • Conecta callback "message" para on_message.
  • Carrega script com frida_script_load_sync.
  • Imprime [*] Script loaded.
  • Se o loop está ativo, entra em g_main_loop_run(loop).

Session End (LINHAS: 108…123)

  • Após sair do loop, descarrega script (frida_script_unload_sync, frida_unref(script)).
  • Imprime [*] Unloaded.
  • Desanexa sessão (frida_session_detach_sync, frida_unref(session)).
  • Imprime [*] Detached.
  • Se houve erro em attach, imprime mensagem de erro e libera GError.

O APK usa esse binário no arquivo AutoInject.kt da seguinte forma:

package io.hakaisecurity.beerusframework.core.functions.frida

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import io.hakaisecurity.beerusframework.core.utils.CommandUtils.Companion.runSuCommand
import java.io.File
import java.util.concurrent.ConcurrentHashMap

class AutoInject {
    companion object {
        private val scriptCache = ConcurrentHashMap<String, String>()
        private var lastCacheUpdate = 0L
        private const val CACHE_EXPIRY_MS = 5000L

        fun injectFridaCore(context: Context, packageName: String, script: String) {
            val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
            if (launchIntent != null) {
                launchIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY
                context.startActivity(launchIntent)

                val scriptsFullPath = File(context.filesDir, "scripts").absolutePath + "/" + script

                try {
                    runSuCommand("sleep 5 && fridaCore \$(pidof $packageName) $scriptsFullPath") {}
                } catch (e: Exception) {}
            }
        }

...

injectFridaCore Function (LINHAS: 17…29)

  • Recupera a intent de lançamento do app alvo pelo packageName.
  • Define flags da Activity:
    • NEW_TASK: inicia em uma nova task.
    • NO_HISTORY: a Activity não fica no histórico.
  • Inicia o aplicativo alvo usando o launchIntent.
  • Espera 5 segundos (tempo para o app iniciar).
  • Executa o binário do Frida Core anexando no processo do app alvo e carregando o script selecionado.

Observação: Para utilizar esta funcionalidade, é necessário instalar o módulo Magisk do Beerus Framework, disponível ao abrir o app ou ao acessar uma funcionalidade que dependa dele.

ADB Over Network

A funcionalidade ADB Over Network permite iniciar o Android Debug Bridge (ADB/adbd) via protocolo TCP, possibilitando a interação remota com o dispositivo sem necessidade de cabo. Essa função é essencial para análise dinâmica, permitindo instalar aplicativos, acessar informações do sistema e diretórios como /data/data, além de executar ferramentas como Frida Server e outros binários.

Boot Options

A funcionalidade Boot Options permite configurar a inicialização automática de ferramentas como Frida Server e ADB Over Network junto com o boot do dispositivo, evitando procedimentos manuais repetitivos. Também é possível promover certificados de usuário a certificados de sistema, o que ajuda a contornar proteções como SSL Pinning. Utilizando os scripts post-fs-data.sh e service.sh do módulo Magisk, é possível executar binários e alterar configurações ainda no início do boot.

Podemos ver no post-fs-data.sh o seguinte código:

#!/system/bin/sh
MODDIR=${0%/*}

STATUS_FILE="$MODDIR/status"
[ ! -f "$STATUS_FILE" ] && exit 0

systemTrustedCerts=$(grep '^systemTrustedCerts=' "$STATUS_FILE" | cut -d'=' -f2)
adbOverNetwork=$(grep '^adbOverNetwork=' "$STATUS_FILE" | cut -d'=' -f2)

if [ "$systemTrustedCerts" = "true" ]; then
    CERT_SRC="/data/misc/user/0/cacerts-added"
    CERT_DST="$MODDIR/system/etc/security/cacerts"

    mkdir -p "$CERT_DST"
    rm -f "$CERT_DST"/*
    cp -f "$CERT_SRC"/* "$CERT_DST/" 2>/dev/null
fi

if [ "$adbOverNetwork" = "true" ]; then
    resetprop -n service.adb.tcp.port 5555
    stop adbd
    start adbd
elif [ "$adbOverNetwork" = "false" ]; then
    resetprop -n service.adb.tcp.port ""
    stop adbd
    start adbd
fi

Variables (LINHAS: 7…8)

  • Extração das feature flags setadas pelo APK do Beerus Framework.

System Trusted Certs Condition (LINHAS: 10…17)

  • Faz a checagem se a feature flag do System Trusted Certs está habilitada.
  • Pega o diretório de certificados do usuário e move seus certificados para um diretório réplica do filesystem (/system).
  • O Magisk identificará a réplica e colocará os certificados como se fossem do próprio sistema Android.
  • Isso faz com que algumas abordagens inseguras de SSL Pinning sejam bypassed.

ADB Over Network Condition (LINHAS: 19…27)

  • Faz a checagem se a feature flag do ADB Over Network está habilitada ou não.
  • Caso sim, troca o valor da propriedade service.adb.tcp.port para o número da porta desejada e reinicia o serviço do adbd.
  • Caso não, troca o valor da propriedade service.adb.tcp.port para vazio e reinicia o serviço do adbd, com a finalidade de parar a execução no próximo restart do dispositivo.

Podemos ver também no script service.sh o seguinte código:

#!/system/bin/sh
MODDIR=${0%/*}

STATUS_FILE="$MODDIR/status"
[ ! -f "$STATUS_FILE" ] && exit 0

fridaProp=$(grep '^frida=' "$STATUS_FILE" | cut -d'=' -f2)
fridaBin="/data/local/tmp/hiddenBin"

if [ "$fridaProp" = "true" ] && [ -x "$fridaBin" ]; then
    "$fridaBin" &
fi

...

Variables (LINHAS: 7…8)

  • Extração da feature flag setada pelo APK do Beerus Framework.
  • Valor do path do binário do Frida criado pelo APK do Beerus.

Frida Server Setup Condition (LINHAS: 10…12)

  • Faz a checagem se a feature flag do Frida Server Setup está habilitada e se há a existência do binário.
  • Realiza a execução do binário em background.

Observação: O uso dessa funcionalidade requer a instalação do módulo Magisk do Beerus Framework, disponível ao abrir o app ou ao acessar uma funcionalidade que dependa dele.

SandBox Exfiltration

A funcionalidade Sandbox Exfiltration permite extrair do dispositivo informações salvas pelo aplicativo além do próprio .apk, não só facilitando na hora de fazer a análise, mas também demonstrando que, em casos de RATs (Remote Access Trojan), pode ser utilizada para extrair informações do dispositivo. Da mesma forma, em casos de roubo ou furto, o invasor pode efetuar o root do dispositivo e obter esses dados.

Ao acessarmos essa funcionalidade, nos deparamos com algumas aplicações instaladas no dispositivo. Ao selecionarmos uma delas, podemos enviar para o servidor do beerus ou, ao selecionarmos a opção de USB, ele gera o arquivo com o comando para extrair diretamente usando: adb pull /data/local/tmp/tmp/{package_name}.tar.gz

Lembrando que temos um blog post detalhado explicando a funcionalidade ainda na primeira versão do Beerus, escrito por Lucas “luriel” Carmo e Daniel “d3v” Chactoura, que pode ser acessado neste link: https://hakaisecurity.io/beerus-apk-spotlighting-sandbox-exfiltration/insights-blog/

Properties Changes

A funcionalidade Properties Changes foi criada para permitir a adição e modificação de propriedades do sistema diretamente pela interface gráfica, de forma permanente, ou seja, mesmo após reiniciar o dispositivo. Essa funcionalidade é especialmente útil para alterar propriedades utilizadas por aplicativos para identificar dispositivos com root ou executando em emuladores, como por exemplo: "ro.hardware=unknown" e "ro.kernel.qemu=0".

Ao acessarmos a funcionalidade, nos deparamos com um botão para adicionar uma nova propriedade. Esse botão pode ser utilizado tanto para criar uma propriedade inexistente quanto para modificar uma já existente. Ao clicarmos para adicionar, será exibido um campo para inserir o nome da propriedade (nova ou existente) e outro campo para definir o valor desejado.

Após preencher e confirmar, a propriedade será adicionada com sucesso através do uso do arquivo system.prop do módulo Magisk. Por fim, basta reiniciar o dispositivo para que as alterações sejam aplicadas permanentemente.

Observação: Para utilizar esta funcionalidade, é necessário instalar o módulo Magisk do Beerus Framework, disponível ao abrir o app ou ao acessar uma funcionalidade que dependa dele.

Manifest Decoding

A funcionalidade Manifest Decoding realiza o parsing do AndroidManifest.xml para extrair informações relevantes à análise de um APK, como Activities e Receivers exportados, permissões declaradas, Main Activity identificada e outras configurações sensíveis ou úteis para análise de segurança.

Para implementar esse módulo, utilizamos a biblioteca apk-parser, que permite extrair e decodificar o Android Manifest diretamente do APK. A partir disso, fazemos o parsing das informações necessárias.

package io.hakaisecurity.beerusframework.core.functions.Manifest

import android.util.Log
import io.hakaisecurity.beerusframework.core.functions.Properties.Properties.PropertyData
import io.hakaisecurity.beerusframework.core.utils.CommandUtils.Companion.runSuCommand
import net.dongliu.apk.parser.ApkFile
import java.io.File
import java.util.Dictionary

class Manifest {
    data class ComponentInfo(
        val name: String,
        val exported: Boolean
    )

    data class Manifest(
        val userPermissions: List<String>,
        val components: Map<String, List<ComponentInfo>>,
        val General: Map<String, String>
    )

    fun parseManifest(androidManifest: String, callback: (Manifest) -> Unit) {
        val general = mutableMapOf<String, String>()
        val permissions = mutableListOf<String>()
        val components = mutableMapOf(
            "activities" to mutableListOf<ComponentInfo>(),
            "providers" to mutableListOf<ComponentInfo>(),
            "services" to mutableListOf<ComponentInfo>(),
            "receivers" to mutableListOf<ComponentInfo>()
        )

        val tagToKey = mapOf(
            "activity" to "activities",
            "provider" to "providers",
            "service" to "services",
            "receiver" to "receivers"
        )
        Regex(
            "<activity\b[^>]*android:name=\"([^\"]+)\"[^>]*>(?:(?!</activity>).)*?<intent-filter>(?:(?!</intent-filter>).)*?<action[^>]*android:name=\"android.intent.action.MAIN\"[^>]*/>(?:(?!</intent-filter>).)*?<category[^>]*android:name=\"android.intent.category.LAUNCHER\"[^>]*/>(?:(?!</intent-filter>).)*?</intent-filter>",
            RegexOption.DOT_MATCHES_ALL
        ).find(androidManifest)?.let {
            general["Main Activity"] = it.groupValues[1]
        }

        Regex("<manifest[^>]*package=\"([^\"]+)").find(androidManifest)?.let {
            general["Package Name"] = it.groupValues[1]
        }


        Regex("android:versionName=\"([^\"]+)\"").find(androidManifest)?.let {
            general["Version Name"] = it.groupValues[1]
        }

        Regex("<uses-sdk[^>]*minSdkVersion=\"([^\"]+)\"").find(androidManifest)?.let {
            general["Min SDK"] = it.groupValues[1]
        }
        Regex("<uses-sdk[^>]*targetSdkVersion=\"([^\"]+)\"").find(androidManifest)?.let {
            general["Target SDK"] = it.groupValues[1]
        }
        Regex("<application[^>]*android:name=\"([^\"]+)\"").find(androidManifest)?.let {
            general["Application"] = it.groupValues[1]
        }

        // Permissions
        var regex = Regex("<uses-permission[^>]*android:name=\"([^\"]+)\"")
        var matches = regex.findAll(androidManifest)
        for (i in matches) {
            var perm = i.groupValues[1]
            if (perm.startsWith("android.permission.")) {
                perm = perm.split("android.permission.", limit = 2)[1]
            }

            permissions.add(perm)
        }

        // Components
        for (tag in tagToKey.keys) {
            regex = Regex(
                "<$tag\b([^>]*)>(.*?)</$tag>",
                RegexOption.DOT_MATCHES_ALL
            )
            matches = regex.findAll(androidManifest)
            Log.d("{OUTPUT}", "TAG: $tag")
            for (match in matches) {
                val attributes = match.groupValues[1]
                val innerContent = match.groupValues[2]

                val nameRegex = Regex("android:name\s*=\s*\"([^\"]+)\"")
                val name = nameRegex.find(attributes)?.groupValues?.get(1) ?: continue

                val exportedRegex = Regex("android:exported\s*=\s*\"([^\"]+)\"")
                val exportedRaw = exportedRegex.find(attributes)?.groupValues?.get(1)
                var exported = when (exportedRaw?.lowercase()) {
                    "true" -> true
                    "false" -> false
                    else -> null
                }

                if (exported != true && innerContent.contains("<intent-filter")) {
                    exported = true
                }

                val list = components.getValue(tagToKey[tag]!!)
                list.add(ComponentInfo(name, exported == true))
            }
        }

        callback(Manifest(permissions, components, general))
    }


    fun getManifest(artifactPath: String): Manifest? {
        val latch = java.util.concurrent.CountDownLatch(1)
        val apkFile = File(artifactPath)
        val parser = ApkFile(apkFile)
        val manifestXml = parser.manifestXml
        var ParsedManifest: Manifest? = null
        parseManifest(manifestXml) { result ->
            ParsedManifest = result
            latch.countDown()

        }

        latch.await()
        return ParsedManifest
    }
}

Data Class Dеclaration (LINHAS: 11…20)

  • Define o data class ComponentInfo, utilizado para os componentes activities, providers, services e receivers.
  • Define o data class Manifest, onde serão armazenados os componentes, permissões dos usuários e informações gerais.

parseManifest Mеthod (LINHAS: 22…109)

  • Cria variáveis mutáveis para os componentes, informações gerais e permissões do aplicativo.
  • Utiliza regex para obter informações do app, como: nome do pacote, versão do SDK esperado, Main Activity, entre outros.
  • Executa um regex para capturar as permissões requeridas pelo aplicativo.
  • Obtém os componentes da aplicação.

getManifest Mеthod (LINHAS: 112…126)

  • Utiliza o ApkFile para obter o manifest do aplicativo.
  • Usa o parseManifest para extrair as informações do manifest.
  • Retorna o resultado do parse.

Proxy Profiles

A funcionalidade Proxy Profiles permite criar diferentes perfis de proxy e ativá-los ou desativá-los a qualquer momento, sem precisar configurar ou remover manualmente o proxy ao mudar de rede ou ao usar aplicativos normalmente.

Para criar um perfil, basta informar um nome e o endereço IP com porta. Após isso, é possível ativar/desativar, editar ou excluir o perfil sempre que necessário.

Quando um perfil é ativado, todo o tráfego do dispositivo passa automaticamente pelo proxy configurado. Isso é feito pela função selectProfile, definida em ProxyProfiles.kt, que atualiza o arquivo proxyLists.json marcando o perfil selecionado e define o proxy global do dispositivo via contexto shell incorporado por privilégios root:

...

    fun selectProfile(context: Context, conString: String) {
        val fileName = "proxyLists.json"
        val file = File(context.filesDir, fileName)

        try {
            if (file.exists()) {
                val jsonObject = JSONObject(file.readText())
                val connectionsArray = jsonObject.getJSONArray("connections")

                for (i in 0 until connectionsArray.length()) {
                    val obj = connectionsArray.getJSONObject(i)
                    val selected = obj.getString("conString") == conString
                    obj.put("selected", selected)
                }

                file.writeText(jsonObject.toString(4))
                runSuCommand("runcon u:r:shell:s0 sh -c 'settings put global http_proxy $conString'") { }
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

...

Magisk Manager

A funcionalidade Magisk Manager atua como uma extensão do Magisk, oferecendo controle total sobre seus módulos. Com ela, é possível ativar ou desativar módulos já instalados, além de instalar ou remover novos conforme necessário.

Ao acessar a interface, todos os módulos existentes ficam listados, acompanhados da opção de adicionar novos a partir de arquivos locais, tornando a gestão prática e centralizada.

No arquivo MagiskModule.kt, vemos o seguinte código:

package io.hakaisecurity.beerusframework.core.functions.magiskModuleManager

import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import io.hakaisecurity.beerusframework.core.models.MagiskManager.Companion.showsMagiskDialog
import io.hakaisecurity.beerusframework.core.utils.CommandUtils.Companion.runSuCommand
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream

class MagiskModule {
    companion object {
        fun startModuleManager(context: Context, zipUri: Uri) {
            val zipFile = getFileNameFromUri(context, zipUri)
            val dirDestination = zipFile?.split(".")?.get(0)

            val cacheDir: File = context.cacheDir
            val cacheFile = zipFile?.let { File(cacheDir, it).absoluteFile }

            if (zipFile != null) {
                copyFileToCache(context, zipUri, zipFile)
            }

            println(dirDestination)

            runSuCommand("ls /data/adb/modules") { result ->
                if (!result.contains("No such file or directory")) {
                    runSuCommand("mkdir /data/adb/modules/${dirDestination}") {
                        runSuCommand("unzip -o $cacheFile -d /data/adb/modules/${dirDestination}") {
                            showsMagiskDialog()
                        }
                    }
                }
            }
        }

        fun getAllModules(modulePropsList: MutableList<String>) {
            runSuCommand("find /data/adb/modules/ -type f -name \"module.prop\" -exec ls -l {} \\; | cut -d \" \" -f 8") { result ->
                result.split("\n").filter { it.isNotBlank() }.let { paths ->
                    modulePropsList.addAll(paths)
                }
            }
        }

        fun getStatusModule(modulePath: String, status: String): Boolean {
            val path = modulePath.replace("module.prop", status, ignoreCase = true)
            var result = false

            val lock = Object()

            runSuCommand("ls $path") { output ->
                result = output.trim() == path
                synchronized(lock) {
                    lock.notify()
                }
            }

            synchronized(lock) {
                lock.wait()
            }

            return result
        }


        fun moduleOps(modulePath: String, status: Boolean, file: String) {
            val path = modulePath.replace("module.prop", file, ignoreCase = true)

            if(status){
                runSuCommand("touch $path") {}
            }else{
                runSuCommand("rm -rf $path") {}
            }
        }

...

StartModuleManager Function (LINHAS: 17…39)

  • Pega um arquivo .zip e define o destino de seu conteúdo para uma pasta de mesmo nome dentro de /data/adb/modules/<Nome do Módulo>.
  • Checa se o módulo já existe dentro do diretório / se o módulo já está instalado.
  • Realiza o unzip dos arquivos do módulo para a pasta usando privilégios root.
  • Mostra um pop-up para reiniciar o dispositivo e aplicar as alterações do módulo, devido às mudanças só serem aplicadas após a inicialização do dispositivo.

getAllModules Function (LINHAS: 41…47)

  • Lista os módulos instalados em /data/adb/modules e exibe seus nomes através dos metadados contidos no arquivo module.prop de cada um.

getStatusModule Function (LINHAS: 49…67)

  • Checa a existência de arquivos no diretório do módulo que possam identificar seu status atual como inativo ou se será removido na próxima inicialização.

moduleOpsFunction (LINHAS: 70…78)

  • Cria um arquivo dentro do diretório do módulo desejado.
  • Essa função existe porque, de acordo com a natureza do Magisk, arquivos como disable ou remove fazem com que os módulos sejam desabilitados ou removidos na próxima inicialização do dispositivo, respectivamente. Isso pode ser confirmado na documentação oficial.

Memory Dump

A funcionalidade Memory Dump realiza localmente o dump da memória de um processo em execução e salva tudo em um arquivo .tar.gz. Esse pacote pode ser baixado via USB ou enviado para um servidor remoto para análise posterior.

Durante esse processo, são coletadas diversas informações relevantes, incluindo:

  • .so carregados: lista de bibliotecas dinâmicas presentes no processo (nomes e caminhos dos arquivos .so).
  • Maps: mapeamentos de memória do processo (endereços, permissões e caminhos de arquivos mapeados).
  • Stacks: pilhas de execução em espaço de usuário (por thread), úteis para reconstruir contexto e call stacks.
  • Variáveis de ambiente: conteúdo de environ do processo (pares chave-valor).

Observação: Ao iniciar o dump de memória, o processo pode levar algum tempo. Portanto, não saia da tela e aguarde até a finalização da operação.

package io.hakaisecurity.beerusframework.core.functions.memoryDump

import android.annotation.SuppressLint
import android.content.Context
import io.hakaisecurity.beerusframework.core.utils.CommandUtils.Companion.runSuCommand
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.Response
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date

object MemoryDump {
    fun collectionTriagge(context: Context, server:String, isUSB: Boolean, selectionData: String, onComplete: (String) -> Unit) {
        val dir = File(context.filesDir, "dumps")
        if (!dir.exists()) {
            dir.mkdirs()
        }

        quickDump(context, server, isUSB, selectionData, onComplete)
    }

    @SuppressLint("SimpleDateFormat")
    private fun quickDump(context: Context, server: String, isUSB: Boolean, PID: String, onComplete: (String) -> Unit) {
        runSuCommand("""
            echo -e "==== maps ====" && cat /proc/$PID/maps && \
            echo -e "\n==== stack ====" && cat /proc/$PID/stack && \
            echo -e "\n==== .so loaded ====" && cat /proc/$PID/maps | grep -oE '/[^ ]+\.so' | sort -u && \
            echo -e "\n==== envs ====" && cat /proc/$PID/environ
        """.trimIndent()) { output ->
            runSuCommand("cat /proc/$PID/cmdline") { processName ->
                val safeProcessName = processName.trim()
                    .replace(Regex("[^a-zA-Z0-9._-]+"), "_")
                    .removePrefix("_")
                    .removeSuffix("_")
                val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Date())
                val dir = File(context.filesDir, "dumps").apply { mkdirs() }
                val file = File(dir, "$date-$safeProcessName-quick-dump.txt")
                file.writeText(output)

                val pid = PID.trim()
                val stringDumpDirName = File(dir, "$date-$safeProcessName-string-dump")
                val tarFile = File(dir, "$date-$safeProcessName.tar.gz")
                runSuCommand("""
                    mkdir -p ${stringDumpDirName.absolutePath} && \
                    while IFS= read -r line; do \
                        RANGE=$(echo "${'$'}line" | awk '{print ${'$'}1}'); \
                        PERMS=$(echo "${'$'}line" | awk '{print ${'$'}2}'); \
                        if [[ "${'$'}PERMS" == *"r"* ]]; then \
                            START_HEX=0x${'$'}{RANGE%-*}; \
                            END_HEX=0x${'$'}{RANGE#*-}; \
                            START=$(printf "%u" ${'$'}START_HEX); \
                            END=$(printf "%u" ${'$'}END_HEX); \
                            SIZE=${'$'}((END - START)); \
                            FILE="${stringDumpDirName.absolutePath}/${'$'}RANGE"; \
                            dd if=/proc/$pid/mem bs=1 skip=${'$'}START count=${'$'}SIZE status=none 2>/dev/null | strings > "${'$'}FILE"; \
                        fi; \
                    done < /proc/$pid/maps && \
                    cd ${dir.absolutePath} && \
                    tar -czf ${tarFile.absolutePath} $date-$safeProcessName-quick-dump.txt $date-$safeProcessName-string-dump && \
                    rm -rf ${file.absolutePath} ${stringDumpDirName.absolutePath}
                """.trimIndent()) {
                    if (!isUSB) {
                        sendFile(tarFile.absolutePath, server) { R ->
                            runSuCommand("rm -rf ${tarFile.absolutePath}") {
                                onComplete("OK")
                            }
                        }
                    } else {
                        runSuCommand("cp -r ${tarFile.absolutePath} /data/local/tmp") {
                            runSuCommand("rm -rf ${tarFile.absolutePath}") {
                                onComplete("OK")
                            }
                        }
                    }
                }
            }
        }
    }

    private val client = OkHttpClient()

    private fun sendFile(fileName: String, server:String, onComplete: (String) -> Unit) {
        val sourceFile = File(fileName)
        if (!sourceFile.exists()) {
            onComplete("Compressed file not found: $fileName")
        }

        val fileBody = sourceFile.asRequestBody("application/octet-stream".toMediaTypeOrNull())
        var body = MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("file", sourceFile.name, fileBody).build()
        val request = Request.Builder().url(server).post(body).build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                onComplete("ERROR: Failed to send the file")
            }

            override fun onResponse(call: Call, response: Response) {
                if (response.isSuccessful) {
                    onComplete("SUCCESS: File sent successfully")
                } else {
                    onComplete("ERROR: Failed to send the file")
                }
            }
        })
    }

    fun verify(server: String, isUSB: Boolean, onComplete: (Boolean) -> Unit) {
        if (isUSB) {
            onComplete(true)
            return
        } else {
            val request = Request.Builder().url("$server/check").get().build()
            client.newCall(request).enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    onComplete(false)
                }

                override fun onResponse(call: Call, response: Response) {
                    if (response.isSuccessful) {
                        val jsonBody = response.body?.string()
                        val json = JSONObject(jsonBody)
                        if (json.has("app")) {
                            if (json.getString("app") == "Beerus Server") {
                                onComplete(true)
                                return
                            }
                        }
                        onComplete(false)
                        return
                    } else {
                        onComplete(false)
                        return
                    }
                }
            })
        }
    }
}

collectionTriagge Function (LINHAS: 21–28)

  • Verifica se o diretório "dumps" existe; caso contrário, cria-o.
  • Executa a função quickDump.

quickDump Function (LINHAS: 31–82)

  • Coleta os maps, stacks, variáveis de ambiente e arquivos .so carregados.
  • Cria o arquivo de dump do processo.
  • Finaliza o dump compactando-o em um arquivo .tar.gz.
  • Caso o envio não seja por USB, o arquivo é enviado para o servidor e posteriormente apagado do dispositivo.
  • Caso o envio seja por USB, o arquivo é movido para /data/local/tmp.

sendFile Function (LINHAS: 86–109)

  • Valida se o memory dump será exportado via USB ou servidor.
  • Se a exportação for via USB, a verificação do servidor é ignorada.
  • Caso contrário, é enviada uma requisição GET ao servidor, no endpoint /check, para validar se ele é compatível com o Beerus.

verify Function (LINHAS: 111–141)

  • Verifica se o arquivo existe antes do envio.
  • Envia o arquivo para o servidor do Beerus.

Conclusion

O Beerus Framework já está disponível para download e build diretamente no repositório oficial da Hakai Offensive Security.

Nosso objetivo é disponibilizar uma ferramenta que facilite o trabalho de analistas de segurança e pentesters mobile, oferecendo uma interface unificada e recursos práticos.

O projeto está aberto para contribuições da comunidade: se você deseja contribuir, fique à vontade para explorar o repositório, abrir issues, propor PRs ou até mesmo compartilhar ideias sobre novas funcionalidades que possam fortalecer ainda mais a ferramenta.

Estamos trabalhando constantemente para trazer novas features, inovações e automações que possam expandir ainda mais as possibilidades do Beerus Framework no ecossistema de segurança mobile.

Retornamos em breve com mais novidades! ฅ^•ﻌ•^ฅ

References / Bibliography

  • Frida – Toolkit de instrumentação dinâmica para desenvolvedores, engenheiros de reversa e pesquisadores de segurança.
  • Frida Core – Biblioteca central para construção de ferramentas e instrumentações baseadas em Frida.
  • Fridump – Faz dump da memória e dos módulos carregados de aplicativos Android usando Frida.
  • Wireless ADB: ADB over TCP/IP – Conecta a dispositivos Android via Wi-Fi para depuração e testes.
  • Magisk – Conjunto de ferramentas open source para customização do Android, habilitando acesso root e muito mais.
  • Always Trust User Certs – Promove certificados instalados pelo usuário para o repositório de certificados do sistema.
  • Magisk Frida – Inicia automaticamente o Frida Server no boot usando Magisk.
  • JADX – Decompiler de Dex para Java em aplicações Android.
  • Metasploit – Framework de testes de penetração para desenvolver e executar códigos de exploit.

Logo da Hakai.