Beerus Framework – A New Mobile Framework Arises - Hakai
JOIN
JOIN
RESEARCH ENTRY 2025.09.19
Insights Blog

Beerus Framework – A New Mobile Framework Arises

Daniel Franca Lima Tricta
RESEARCHER Daniel Franca Lima, Tricta
READ TIME18 MINUTES
PUBLISHED19 Sep 2025
Beerus Framework – A New Mobile Framework Arises

Abstract

The Beerus Framework is an offensive mobile tool developed to simplify the entire pentesting process on Android devices. With a unified interface directly on the device, Beerus allows performing tasks ranging from application instrumentation in a built-in way on the device with Frida Core, sandbox data exfiltration, memory dumping, proxying, Magisk module control, property manipulation, and much more.

Built on Frida and Magisk, Beerus is modular, extensible, and designed for testing on rooted devices, optimizing common pentesting tasks and enabling automations from a single app.

In this paper, we explore the main functionalities of the framework, with a special focus on some of them. The aim is not to detail its operation exhaustively, but rather to provide a broad overview of what it covers and what it is capable of. To complement this, we provide below a demonstrative video showing how to use the Beerus Framework in practice.

Remember that the Beerus Framework is already available for download directly from the official GitHub repository.

The Development of Beerus Framework

During mobile security assessments, it is common to rely on several external tools and complex setups. The Beerus Framework was created with the goal of unifying the essential functionalities of a mobile pentester into a single Android application. The proposal is to offer a practical and efficient solution that makes the testing process more fluid and agile.

With this in mind, we decided to expand the Beerus project, initially conceived by Lucas "luriel" Carmo and Daniel "d3v" Chactoura, giving rise to the Beerus Framework. The objective is to provide functionalities that simplify and accelerate the daily work of the security analyst.

Introdução Técnica

The Beerus Framework works as a hub of functionalities for security analysts who work with Android devices. The application provides an intuitive graphical interface with access to several tools, leveraging the device’s own resources and root privileges (designed to work in better sync with Magisk).

Among the functionalities offered are:

Frida Server Setup

The Frida Server Setup functionality allows downloading and initializing the Frida Server directly on your mobile device. Simply choose a version and click "Run" for the server to be installed and executed. After installation, it is not necessary to reinstall the same version unless you choose to switch. By default, the Beerus Framework will display the 10 most recent versions, but it is also possible to manually enter the desired version. This makes the process of configuring the instrumentation environment much simpler.

Frida Auto Inject

The Frida Auto Inject functionality enables the injection of Frida scripts directly from the mobile device through Frida Core, the interaction engine via RPC used in both Python and JavaScript versions of Frida. It communicates with the Frida Server, which can be started by the Beerus Framework itself, facilitating portability and the sharing of exploits for application instrumentation.

We can access the Frida Setup, start the Frida Server, and then create or import a script into the device’s storage. After adding the script, it can be edited, the target APK selected, and by clicking "Run", the app will start already with the script injected via Frida Core.

Within the official Frida repository, under releases, we can find a package called frida-core-devkit, available for different architectures. The Frida Core is used to interact with the Frida Server in the following way:

  • The client (your program in Python/Node/C using frida-core) opens a socket to the frida-server.
  • It sends messages in the JSON-RPC format encapsulated in a binary framing (the Frida protocol).
  • The frida-server receives the message and forwards it to its local instance of Frida Core.
  • The client sends attach(pid)serverCore injects the Frida Gadget into the target.
  • The Gadget, which also runs Frida Core, opens an internal RPC channel with the server.
  • Now the server acts as a proxy between:
    • Client Core (your tool)
    • Gadget Core (inside the target)

So, Beerus has a modified version of this devkit to allow passing JS scripts as a parameter to a compiled binary of 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 (Line: 9)

  • Provides the main APIs of Frida through "frida-core.h".

Static callbacks declarations (LINES: 14...17)

  • on_detached(...): callback to handle when the session is disconnected.
  • on_message(...): callback for messages coming from the JS script.
  • on_signal(...): captures signals such as SIGINT and SIGTERM.
  • stop(...): used to stop the main loop.

Global Variable GMainLoop (LINE: 19)

  • Stores the GLib main loop that processes asynchronous events.

Inicialization (LINES: 21...52)

  • Declares local variables (target PID, JS script, manager, devices, session, etc.).
  • Calls frida_init().
  • Validates arguments: expects <pid> <script.js>. If invalid, prints Usage and exits.
  • Reads the JS script (argv[2]) into the variable script_source with g_file_get_contents.
  • Creates the main loop with g_main_loop_new.
  • Registers signal handlers for SIGINT and SIGTERM.

Device Management (LINES: 54...74)

  • Creates FridaDeviceManager with frida_device_manager_new().
  • Enumerates devices with frida_device_manager_enumerate_devices_sync.
  • Iterates over the devices found: prints name (frida_device_get_name).
  • If it is FRIDA_DEVICE_TYPE_LOCAL, stores the reference as local_device.
  • Releases the device list with frida_unref(devices).

Attaching to Session (LINES: 76...86)

  • Calls frida_device_attach_sync to attach to the target process.
  • If successful, connects the callback "detached".
  • Checks if the session was prematurely disconnected.
  • Prints [*] Attached upon success.

Script Creation and Loading (LINES: 88...106)

  • Creates script options (FridaScriptOptions): defines the name "example" and runtime QJS.
  • Creates the script with frida_session_create_script_sync.
  • Connects the callback "message" to on_message.
  • Loads the script with frida_script_load_sync.
  • Prints [*] Script loaded.
  • If the loop is active, enters g_main_loop_run(loop).

Session End (LINES: 108...123)

  • After leaving the loop, unloads the script (frida_script_unload_sync, frida_unref(script)).
  • Prints [*] Unloaded.
  • Detaches the session (frida_session_detach_sync, frida_unref(session)).
  • Prints [*] Detached.
  • If there was an error in attach, prints the error message and frees GError

The APK uses this binary in the file AutoInject.kt as follows:

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 (LINES: 17...29)

  • Retrieves the launch intent of the target app through the packageName.
  • Defines Activity flags:
    • NEW_TASK: starts in a new task.
    • NO_HISTORY: the Activity does not remain in the history.
  • Starts the target application using the launchIntent.
  • Waits 5 seconds (time for the app to start).
  • Executes the Frida Core binary, attaching it to the target app process and loading the selected script.

Note: To use this functionality, it is necessary to install the Magisk module of the Beerus Framework, available when opening the app or when accessing a functionality that depends on it.

ADB Over Network

The ADB Over Network functionality allows starting the Android Debug Bridge (ADB/adbd) via the TCP protocol, enabling remote interaction with the device without the need for a cable. This feature is essential for dynamic analysis, allowing the installation of applications, access to system information and directories such as /data/data, as well as the execution of tools like Frida Server and other binaries.

Boot Options

The Boot Options functionality allows configuring the automatic startup of tools such as Frida Server and ADB Over Network along with the device boot, avoiding repetitive manual procedures. It is also possible to promote user certificates to system certificates, which helps bypass protections such as SSL Pinning. By using the scripts post-fs-data.sh and service.sh from the Magisk module, it is possible to execute binaries and change configurations at the very beginning of the boot.

We can see the following code in post-fs-data.sh:

#!/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 (LINES: 7...8)

  • Extraction of the feature flags set by the Beerus Framework APK.

System Trusted Certs Condition (LINES: 10...17)

  • Checks if the feature flag of System Trusted Certs is enabled.
  • Gets the user certificate directory and moves its certificates to a replica directory of the filesystem (/system).
  • Magisk will identify the replica and place the certificates as if they were part of the Android system itself.
  • This makes some insecure SSL Pinning approaches bypassed.

ADB Over Network Condition (LINES: 19...27)

  • Checks if the feature flag of ADB Over Network is enabled or not.
  • If yes, changes the value of the property service.adb.tcp.port to the desired port number and restarts the adbd service.
  • If not, changes the value of the property service.adb.tcp.port to empty and restarts the adbd service, in order to stop execution on the next device restart.

We can also see the following code in the script service.sh:

#!/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 (LINES: 7...8)

  • Extraction of the feature flag set by the Beerus Framework APK.
  • Value of the path of the Frida binary created by the Beerus APK.

Frida Server Setup Condition (LINES: 10...12)

  • Checks if the feature flag of Frida Server Setup is enabled and if the binary exists.
  • Executes the binary in the background.

Note: The use of this functionality requires the installation of the Magisk module of the Beerus Framework, available when opening the app or when accessing a functionality that depends on it.

SandBox Exfiltration

The Sandbox Exfiltration functionality allows extracting from the device not only the information saved by the application but also the .apk itself. This not only facilitates the analysis process but also demonstrates that, in cases of RATs (Remote Access Trojan), it can be used to extract information from the device. Likewise, in cases of theft or loss, the attacker can perform root on the device and obtain this data.

When accessing this functionality, we are presented with some applications installed on the device. By selecting one of them, we can send it to the Beerus server or, by selecting the USB option, it generates the file with the command to extract directly using: adb pull /data/local/tmp/tmp/{package_name}.tar.gz

It is worth noting that we have a detailed blog post explaining this functionality in the very first version of Beerus, written by Lucas "luriel" Carmo and Daniel "d3v" Chactoura, which can be accessed at the following link: https://hakaisecurity.io/beerus-apk-spotlighting-sandbox-exfiltration/insights-blog/

Properties Changes

The Properties Changes functionality was created to allow the addition and modification of system properties directly through the graphical interface, in a permanent way, that is, even after rebooting the device. This functionality is especially useful for changing properties used by applications to identify devices with root or running in emulators, such as: "ro.hardware=unknown" and "ro.kernel.qemu=0".

When accessing the functionality, we are presented with a button to add a new property. This button can be used either to create a non-existent property or to modify an already existing property. When clicking to add, a field is displayed to enter the property name (new or existing) and another field to set the desired value.

After filling and confirming, the property will be successfully added through the use of the system.prop file from the Magisk module. Finally, simply reboot the device so that the changes are applied permanently.

Note: To use this functionality, it is necessary to install the Magisk module of the Beerus Framework, available when opening the app or when accessing a functionality that depends on it.

Manifest Decoding

The Manifest Decoding functionality performs the parsing of the AndroidManifest.xml to extract information relevant to the analysis of an APK, such as exported Activities and Receivers, declared permissions, identified Main Activity, and other sensitive or useful settings for security analysis.

To implement this module, we used the apk-parser library, which allows extracting and decoding the Android Manifest directly from the APK. From there, we perform the parsing of the necessary information.

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 (LINES: 11...20)

  • Defines the data class ComponentInfo, used for the activities, providers, services, and receivers components.
  • Defines the data class Manifest, where the components, user permissions, and general information will be stored.

parseManifest Mеthod (LINES: 22...109)

  • Creates mutable variables for the components, general information, and application permissions.
  • Uses regex to obtain information from the app, such as: package name, expected SDK version, Main Activity, among others.
  • Executes a regex to capture the permissions required by the application.
  • Retrieves the application components.

getManifest Mеthod (LINES: 112...126)

  • Uses ApkFile to obtain the application’s manifest.
  • Uses parseManifest to extract the manifest information.
  • Returns the result of the parse.

Proxy Profiles

The Proxy Profiles functionality allows creating different proxy profiles and enabling or disabling them at any time, without the need to manually configure or remove the proxy when changing networks or using applications normally.

To create a profile, simply provide a name and the IP address with a port. After that, it is possible to enable/disable, edit, or delete the profile whenever necessary.

When a profile is enabled, all device traffic automatically passes through the configured proxy. This is done by the selectProfile function, defined in ProxyProfiles.kt, which updates the proxyLists.json file by marking the selected profile and sets the global proxy of the device via shell context incorporated with root privileges:

...

    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

The Magisk Manager functionality acts as an extension of Magisk, offering full control over its modules. With it, it is possible to enable or disable already installed modules, as well as install or remove new ones as needed.

When accessing the interface, all existing modules are listed, along with the option to add new ones from local files, making management practical and centralized.

In the MagiskModule.kt file, we see the following code:

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 (LINES: 17...39)

  • Takes a .zip file and defines its content’s destination as a folder with the same name inside /data/adb/modules/<Module Name>.
  • Checks if the module already exists in the directory / if the module is already installed.
  • Performs the unzip of the module files into the folder using root privileges.
  • Displays a pop-up to restart the device and apply the module changes, since modifications are only applied after the device is rebooted.

getAllModules Function (LINES: 41...47)

  • Lists the modules installed in /data/adb/modules and displays their names through the metadata contained in each module.prop file.

getStatusModule Function (LINES: 49...67)

  • Checks for the existence of files in the module directory that may identify its current status as inactive or whether it will be removed at the next reboot.

moduleOpsFunction (LINES: 70...78)

  • Creates a file inside the desired module directory.
  • This function exists because, according to the nature of Magisk, files such as disable or remove cause the modules to be disabled or removed on the device’s next reboot, respectively. This can be confirmed in the official documentation.

Memory Dump

The Memory Dump functionality locally performs a memory dump of a running process and saves everything into a .tar.gz file. This package can be downloaded via USB or sent to a remote server for later analysis.

During this process, several relevant pieces of information are collected, including:

  • Loaded .so: list of dynamic libraries present in the process (names and paths of the .so files).
  • Maps: process memory mappings (addresses, permissions, and paths of mapped files).
  • Stacks: execution stacks in user space (per thread), useful for reconstructing context and call stacks.
  • Environment variables: content of the process environ (key-value pairs).

Note: When starting the memory dump, the process may take some time. Therefore, do not leave the screen and wait until the operation is complete.

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 (LINES: 21...28)

  • Checks if the "dumps" directory exists; if not, creates it.
  • Executes the quickDump function.

quickDump Function (LINES: 31...82)

  • Collects maps, stacks, environment variables, and loaded .so files.
  • Creates the process dump file.
  • Finalizes the dump by compressing it into a .tar.gz file.
  • If the transfer is not via USB, the file is sent to the server and then deleted from the device.
  • If the transfer is via USB, the file is moved to /data/local/tmp.

sendFile Function (LINES: 86...109)

  • Validates whether the memory dump will be exported via USB or server.
  • If the export is via USB, the server check is skipped.
  • Otherwise, a GET request is sent to the server, at the /check endpoint, to validate if it is compatible with Beerus.

verify Function (LINES: 111...141)

  • Sends the file to the Beerus server.
  • Checks if the file exists before sending.

Conclusion

The Beerus Framework is now available for download and build directly from the official Hakai Offensive Security repository.

Our goal is to provide a tool that facilitates the work of security analysts and mobile pentesters, offering a unified interface and practical resources.

The project is open for community contributions: if you wish to contribute, feel free to explore the repository, open issues, propose PRs, or even share ideas for new functionalities that can further strengthen the tool.

We are constantly working to bring new features, innovations, and automations that can further expand the possibilities of the Beerus Framework in the mobile security ecosystem.

We’ll be back soon with more updates! ฅ^•ﻌ•^ฅ

References / Bibliography

  • Frida – Dynamic instrumentation toolkit for developers, reverse engineers, and security researchers.
  • Frida Core – Core library for building Frida-based tools and instrumentation.
  • Fridump – Performs memory dump and extracts loaded modules from Android applications using Frida.
  • Wireless ADB: ADB over TCP/IP – Connects to Android devices via Wi-Fi for debugging and testing.
  • Magisk – A set of open source tools for Android customization, enabling root access and more.
  • Always Trust User Certs – Promotes user-installed certificates to the system certificate store.
  • Magisk Frida – Automatically starts the Frida Server on boot using Magisk.
  • JADX –Decompiler from Dex to Java for Android applications.
  • Metasploit – Penetration testing framework for developing and executing exploit code.