> ## Content Index
> Fetch the complete content index at: https://yokai.hakaisecurity.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Beerus Framework: v1.1 - KernelSU, gRPC Transport and Fixes
- URL: https://yokai.hakaisecurity.io/beerus-framework-v1-1-kernelsu-grpc-transport-and-fixes/
- Published: 2026-07-02T20:39:39.000Z
- Updated: 2026-07-02T20:39:39.000Z
- Author: daniel chactoura
- Tags: Research Blog, #wp, en, #pair-beerus-v1.1

A few months after the initial release of the **Beerus Framework**, we are publishing version **v1.1**, a minor update that brings support for new root environments, a relevant architectural change in data transport, integrated auto-update, and a set of accumulated fixes since the first release.

In this post, we detail each change included in this version, the technical context behind each decision, and what it means in practice for those who use **Beerus** in their day-to-day Android testing.

---

**TL;DR:** Beerus Framework v1.1 expands compatibility and improves the operational experience for Android pentesting, introducing [KernelSU support](#h-kernelsu-support), fixes to [Trusted System Certificates](#h-fix-trusted-system-certificates), [Memory Dump performance optimizations](#h-fix-memory-dump-performance-optimization), and an optional [gRPC transport for memory dumps and sandbox exfiltration](#h-grpc-transport-for-memory-dump-and-sandbox-exfiltration). This release also fixes startup freezes in [Root Modules](#h-fix-root-modules-freezing-on-startup), upgrades the [Frida Auto Injector](#h-fix-frida-auto-injector-code-editor-console-and-fixes) with a native code editor, live console, and early-process injection, adds [integrated auto-updates](#h-integrated-auto-update), introduces [iptables as an alternative to Proxy Profiles](#h-iptables-as-an-alternative-to-proxy-profiles), and delivers a broader [UX/UI refactor](#h-ux-ui-refactor). Finally, we highlight the project’s first external contribution, improving [Magisk detection](#h-honorable-mention-magisk-detection-by-our-first-external-contributor), alongside the [full changelog](#h-full-changelog) and [installation](#h-how-to-get-it) and [contribution](#h-contribution) details.

---

# Context

The **Beerus Framework** was created to consolidate the main mobile pentest operations into a single interface installed directly on the target device. Instead of relying on multiple external tools, standalone scripts, and repetitive manual configurations, **Beerus** exposes these capabilities in a centralized way, from **Frida** injection to memory dumps and sandbox extraction.

Version 1.0 established this foundation. v1.1 fixes what was broken, expands compatibility, reshapes interfaces, and paves the way for upcoming developments.

---

# What's New

### KernelSU Support

**Beerus** assumed **Magisk** as the main root **backend**. This worked well for most lab devices but limited the framework to a specific **root manager** model. In practice, users with **KernelSU** could have root on the device, but **Beerus** still treated the environment as if it necessarily had to be **Magisk**\-based.

v1.1 changes this behavior by adding a specific validation for **KernelSU** in the initialization flow. Now, the application first tries to detect **Magisk**. If it is not present, **Beerus** runs a second validation using `ksud`, the daemon responsible for managing **KernelSU**. If this validation returns a valid version, the framework considers that a compatible root environment exists and normally enables all features that depend on elevated privileges.

```kotlin
fun detectKernelSu(callback: (Boolean) -> Unit) {
    val ok = try {
        val p = Runtime.getRuntime().exec(arrayOf("su", "-c", "ksud -V"))
        val output = p.inputStream.bufferedReader().readText()
        p.waitFor()

        Regex("""\bksud\s+\d+\.\d+\.\d+\b""").containsMatchIn(output)
    } catch (_: Throwable) {
        false
    }

    callback(ok)
}
```

The main difference between **Magisk** and **KernelSU** lies in the architecture. **Magisk** operates during the boot process, modifying the `initramfs` to create a **systemless** environment capable of providing root access, modules, and system changes without directly modifying the original partitions. **KernelSU**, on the other hand, implements privilege management directly in the **kernel**, using `ksud` to control the granting of root permissions to authorized applications.

For this reason, **Beerus** no longer depends exclusively on **Magisk** detection. The initialization flow was adjusted to treat **Magisk** and **KernelSU** as two supported **root managers**. When **Magisk** is found, the behavior remains unchanged. Otherwise, the application tries to detect **KernelSU** and, if it is available, enables the rest of the flow normally, allowing root-dependent features to be used regardless of the installed manager.

```kotlin
LaunchedEffect(Unit) {
    detectMagisk { isMagisk ->
        if (isMagisk) {
            updateHasRoot(true)

            getAllModules { paths ->
                mainHandler.post {
                    RootModulesState.setModulePaths(paths)
                }
            }

            detectRootModuleInstalled { isModuleInstalled ->
                if (!isModuleInstalled) {
                    showsRootModuleInstallerDialog()
                } else {
                    updateHasModule(true)
                }
            }
        } else {
            detectKernelSu { isKernelSu ->
                if (isKernelSu) {
                    updateHasRoot(true)

                    getAllModules { paths ->
                        mainHandler.post {
                            RootModulesState.setModulePaths(paths)
                        }
                    }

                    detectRootModuleInstalled { isModuleInstalled ->
                        if (!isModuleInstalled) {
                            showsRootModuleInstallerDialog()
                        } else {
                            updateHasModule(true)
                        }
                    }
                }
            }
        }
    }
}
```

For the user, the main practical difference is that the **Beerus Framework** is no longer exclusive to **Magisk**\-based environments. On devices with **KernelSU**, it is enough to grant the application **Root Access by UID** permission through the manager itself. After this authorization, **Beerus** gains the necessary privileges to execute its functionalities normally.

With this change, root-dependent features such as **Frida Core**, system property manipulation, **Boot Options**, **Trusted System Certificates**, and module management now also work in **KernelSU**\-based environments, expanding the framework's compatibility without altering the experience for those who continue using **Magisk**.

---

### Fix: Trusted System Certificates

One of the **Beerus Framework**'s features is the promotion of user-installed certificates to the trust store used by the system, enabling TLS interception in applications that do not accept certificates present only in the user trust store — a common behavior in apps with restrictive network security configurations or compiled for API level 24+.

In v1.0, this flow followed a simpler approach: certificates present in `/data/misc/user/0/cacerts-added` were copied to `/system/etc/security/cacerts`. Although this technique worked for many years, more recent versions of Android started using the trust store provided by the **Conscrypt** module through the APEX `/apex/com.android.conscrypt/cacerts`, making it insufficient to modify only the traditional system certificates path.

In practice, the certificate promoted by **Beerus** could be present in `/system/etc/security/cacerts`, but the mechanism responsible for TLS validation continued consulting the trust store exposed by **Conscrypt**, causing the certificate not to be recognized as a valid trust anchor.

The fix completely reworked the certificate promotion process. Now, when the **Trusted System Certificates** option is active, **Beerus** creates a temporary trust store containing the original system certificates and the user-installed certificates. After applying the proper permissions and **SELinux** context (`u:object_r:system_security_cacerts_file:s0`), the framework uses **bind mounts** to expose this consolidated trust store at the paths actually used by the device, including `/apex/com.android.conscrypt/cacerts` and, when applicable, `/system/etc/security/cacerts`.

```bash
...
if [ "$systemTrustedCerts" = "true" ]; then
    if [ -d /apex/com.android.conscrypt/cacerts ]; then
        tmp_cacerts_dir=$MODDIR/conscrypt_cacerts
        mkdir -p $tmp_cacerts_dir
        rm -f $tmp_cacerts_dir/*
        cp -f /apex/com.android.conscrypt/cacerts/* $tmp_cacerts_dir/
        cp -f /data/misc/user/0/cacerts-added/* $tmp_cacerts_dir/ 2>/dev/null
        set_perm_recursive $tmp_cacerts_dir root shell 755 644 u:object_r:system_security_cacerts_file:s0
        mount --bind $tmp_cacerts_dir /apex/com.android.conscrypt/cacerts
    fi

    if [ -d /system/etc/security/cacerts ]; then
        tmp_cacerts_dir=$MODDIR/etc_cacerts
        mkdir -p $tmp_cacerts_dir
        rm -f $tmp_cacerts_dir/*
        cp -f /system/etc/security/cacerts/* $tmp_cacerts_dir/
        cp -f /data/misc/user/0/cacerts-added/* $tmp_cacerts_dir/ 2>/dev/null
        set_perm_recursive $tmp_cacerts_dir root root 755 644 u:object_r:system_security_cacerts_file:s0
        mount --bind $tmp_cacerts_dir /system/etc/security/cacerts
    fi
fi
...
```

With this, **Beerus** now provides a unified view of the system trust store, preserving Android's original certificates and adding user-installed certificates in the locations actually consulted during the TLS validation process.

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2026/06/captura-de-tela-2026-06-24-as-23-50-06.png)

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2026/06/captura-de-tela-2026-06-24-as-23-49-08-1.png)

---

### Fix: Memory Dump Performance Optimization

Beyond the transport change, the **Memory Dump** process also received a direct optimization in the flow for collecting strings from the target process's memory.

In the previous implementation, **Beerus** iterated through all readable regions present in `/proc/<pid>/maps`. For each region, the code calculated the memory range and performed the read on `/proc/<pid>/mem` using `dd` with `bs=1`, meaning byte by byte. In processes with many mapped regions or very large regions, this approach could make the dump extremely slow, which is why the interface displayed a warning that the operation could take between 20 and 30 minutes.

The fix changed this strategy to reduce the volume of analyzed memory and make reading more efficient. Now, before executing the dump, **Beerus** filters `/proc/<pid>/maps` looking only for regions with read and write permissions related to areas more useful for string extraction, such as `heap`, anonymous regions, and Android runtime artifacts (`.dex`, `.odex`, and `.oat`).

Additionally, empty or invalid regions are ignored, and regions larger than **20MB** are no longer processed to prevent a single memory area from compromising the total operation time. Reading also stopped being done byte by byte: `dd` now uses **4096-byte** pages, calculating `skip` and `count` based on the page size, which significantly reduces the read overhead on `/proc/<pid>/mem`.

```kotlin
@SuppressLint("SimpleDateFormat")
    private fun quickDump(context: Context, server: String, isUSB: Boolean, PID: String, onComplete: (String) -> Unit) {
        runSuCommand("""
            echo "==== maps ====" && cat /proc/$PID/maps && \
            echo "\n==== stack ====" && cat /proc/$PID/stack && \
            echo "\n==== .so loaded ====" && cat /proc/$PID/maps | grep -oE '/[^ ]+\.so' | sort -u && \
            echo "\n==== envs ====" && tr '\0' '\n' < /proc/$PID/environ
        """.trimIndent()) { output ->
            runSuCommand("cat /proc/$PID/cmdline") { processName ->
                val safeProcessName = processName.trim()
                    .replace(Regex("[^a-zA-Z0-9._-]+"), "_")
                    .trim('_')

                val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Date())
                val baseDir = File(context.filesDir, "dumps").apply { mkdirs() }

                val quickFile = File(baseDir, "$date-$safeProcessName-quick-dump.txt")
                quickFile.writeText(output)

                val pid = PID.trim()
                val stringDumpDir = File(baseDir, "$date-$safeProcessName-string-dump")
                val tarFile = File(baseDir, "$date-$safeProcessName.tar.gz")

                runSuCommand("""
                    PAGE=4096
                    MAX_SIZE=$((20 * 1024 * 1024))
    
                    mkdir -p "${stringDumpDir.absolutePath}"
    
                    grep -E "rw.*(heap|anon|\.dex|\.odex|\.oat)" /proc/$pid/maps | \
                    while read -r line; do
                        RANGE=$(echo "${'$'}line" | awk '{print ${'$'}1}')
    
                        START_HEX=0x${'$'}{RANGE%-*}
                        END_HEX=0x${'$'}{RANGE#*-}
    
                        START=$(printf "%u" "${'$'}START_HEX")
                        END=$(printf "%u" "${'$'}END_HEX")
                        SIZE=$((END - START))
    
                        [ "${'$'}SIZE" -le 0 ] && continue
                        [ "${'$'}SIZE" -gt "${'$'}MAX_SIZE" ] && continue
    
                        SKIP=$((START / PAGE))
                        COUNT=$((SIZE / PAGE))
    
                        [ "${'$'}COUNT" -le 0 ] && continue
    
                        OUT_FILE="${stringDumpDir.absolutePath}/${'$'}RANGE"
    
                        dd if=/proc/$pid/mem \
                           bs=${'$'}PAGE \
                           skip=${'$'}SKIP \
                           count=${'$'}COUNT \
                           status=none 2>/dev/null | strings > "${'$'}OUT_FILE"
                    done
    
                    cd "${baseDir.absolutePath}" && \
                    tar -czf "${tarFile.absolutePath}" \
                        "${quickFile.name}" \
                        "${stringDumpDir.name}" && \
                    rm -rf "${quickFile.absolutePath}" "${stringDumpDir.absolutePath}"
                """.trimIndent()) {
                    if (!isUSB) {
                        sendFile(tarFile.absolutePath, server) {
                            runSuCommand("rm -f ${tarFile.absolutePath}") {
                                onComplete("OK")
                            }
                        }
                    } else {
                        runSuCommand("cp ${tarFile.absolutePath} /data/local/tmp") {
                            runSuCommand("rm -f ${tarFile.absolutePath}") {
                                onComplete("OK")
                            }
                        }
                    }
                }
            }
        }
    }
```

The result is a faster and more predictable **Memory Dump**. Instead of trying to extract strings from every readable mapping of the process, **Beerus** now focuses on regions with a higher chance of containing useful data for analysis, avoiding excessively large reads and reducing the operational cost of collection.

---

### gRPC Transport for Memory Dump and Sandbox Exfiltration

The two heaviest operations in the framework are **Memory Dump** and **Sandbox Exfiltration**. Until now, these features could already send collected artifacts to the **Beerus Server** through the existing **HTTP/HTTPS** flow, which remains available and continues as the default communication mode.

The v1.1 change does not remove the previous path. Instead, it adds a new optional transport based on **gRPC**, allowing the user to choose which protocol to use according to the test environment. In practice, when the address configured in the application uses `http://` or `https://`, **Beerus** maintains the traditional flow. When the address uses the `grpc://` prefix, operations switch to the new **gRPC** client.

This choice matters because **Memory Dump** and **Sandbox Exfiltration** typically produce large `.tar.gz` files. Memory dumps can easily reach hundreds of megabytes, while a sandbox exfiltration can package a large number of internal files from the target application. In these scenarios, a streaming-oriented transport tends to be more suitable than treating the transfer as a single regular **HTTP** request.

On the Android side, v1.1 adds a dedicated **gRPC** client for uploading these artifacts. After **Beerus** generates the `.tar.gz`, the framework checks the configured server type: if it is `grpc://`, the file is sent via `BeerusGrpcUploader`; otherwise, the existing **HTTP** upload continues to be used. This preserves compatibility with older environments while offering a more robust alternative for heavy operations.

The **gRPC** protocol was defined in a `.proto` file shared between client and server. It exposes a **BeerusTransfer** service with two main operations: `Check`, used to validate whether the **gRPC** server is available, and `Upload`, which receives a stream of `UploadChunk`. The first chunk carries the artifact's metadata, such as collection type, filename, related package, and whether the `.tar.gz` should be extracted. Subsequent chunks carry the file's binary data.

```protobuf
syntax = "proto3";

package beerus.transfer.v1;

option java_package = "io.hakaisecurity.beerusframework.grpc";
option java_multiple_files = true;
option java_outer_classname = "BeerusTransferProto";

service BeerusTransfer {
  rpc Check(CheckRequest) returns (CheckResponse);
  rpc Upload(stream UploadChunk) returns (UploadResult);
}

message CheckRequest {}

message CheckResponse {
  string app = 1;
  string version = 2;
}

enum ArtifactKind {
  ARTIFACT_KIND_UNSPECIFIED = 0;
  SANDBOX_EXFILTRATION_TAR_GZ = 1;
  MEMORY_DUMP_TAR_GZ = 2;
}

message UploadMetadata {
  ArtifactKind kind = 1;
  string filename = 2;
  string package_name = 3;
  string device_id = 4;
  bool extract_tar_gz = 5;
}

message UploadChunk {
  int64 seq = 1;
  oneof payload {
    UploadMetadata meta = 2;
    bytes data = 3;
  }
}

message UploadResult {
  bool success = 1;
  string message = 2;
  string saved_path = 3;
  string sha256_hex = 4;
  int64 bytes_received = 5;
}
```

This model brings some practical advantages. The server can receive the file incrementally, validate chunk ordering through a sequence number, calculate the **SHA256** hash during reception, and return information to the client such as operation success, saved path, bytes received, and the artifact's final hash. Furthermore, since the contract between client and server is described in **Protocol Buffers**, future transport evolution becomes more predictable and less dependent on ad-hoc formats.

On the **Beerus Server**, **HTTP** also continues to exist as the default mode. The new version adds a separate **gRPC** mode, started with `--grpc`, typically using a dedicated port. In this mode, the server brings up the **BeerusTransfer** service, receives uploads via streaming, saves artifacts to the output directory, and, when requested by the client, performs safe extraction of the received `.tar.gz` files.

With this, **Beerus** now offers two communication paths: the traditional **HTTP/HTTPS** flow, simple and compatible, and the new **gRPC** flow, more suitable for larger transfers and operations that benefit from streaming, typed contracts, and better control over the artifact reception lifecycle.

---

### Fix: Root Modules Freezing on Startup

In some situations, the interface could freeze or get stuck during the module list initialization, especially when opening the application and loading the modules installed by the **root manager**.

The problem was in how the module state was loaded and updated by the UI. In the previous implementation, module enumeration was only initiated when the **Root Modules** screen was opened. Since this operation depended on executing commands with root privileges, the interface composition could become blocked while the list was being discovered. Additionally, some queries, such as checking a module's state, used synchronous logic based on `wait()`/`notify()`, contributing to interface freezes.

The main change was moving module enumeration to the application initialization. Now, **MainActivity** performs module discovery during `onCreate()`, storing the result in a shared state (`RootModulesState`) that is then observed by Compose screens:

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    RootModules.getAllModules { modules ->
        Handler(Looper.getMainLooper()).post {
            RootModulesState.modules = modules
        }
    }

    setContent {
        BeerusTheme {
            ...
        }
    }
}
```

The function responsible for enumeration also stopped using a synchronous flow, now returning the result through an asynchronous callback:

```kotlin
fun getAllModules(callback: (List<String>) -> Unit) {
    runSuCommand("find /data/adb/modules -mindepth 1 -maxdepth 1 -type d") {
        callback(it.lines().filter { it.isNotBlank() })
    }
}
```

The same pattern was applied to querying each module's state. Instead of blocking the thread waiting for the root command to finish, the response is now delivered directly via the callback:

```kotlin
fun isModuleEnabled(module: String, callback: (Boolean) -> Unit) {
    runSuCommand(
        "test -f /data/adb/modules/$module/disable && echo false || echo true"
    ) {
        callback(it.trim() == "true")
    }
}
```

With this change, module enumeration now occurs only once during application initialization, while the interface simply observes an already-loaded state. Besides eliminating blocking operations based on `wait()`/`notify()`, this reduces repeated filesystem queries and makes the **Root Modules** screen significantly more responsive, in both **Magisk** and **KernelSU** environments.

---

### Fix: Frida Auto Injector - Code Editor, Console and Fixes

The **Frida Auto Injector** allows writing and injecting **Frida** scripts directly on the device, without depending on a connection to an external host. In v1.1, the built-in editor was reworked to fix usability and reliability issues when editing larger or multi-line scripts.

The previous implementation used a Compose `BasicTextField` combined with a custom `VisualTransformation` for JavaScript highlighting (`JsSyntaxHighlighter`). This approach worked for simple cases but was limited for larger code. The text field had to handle scroll, cursor, keyboard, selection, and syntax highlighting rendering on its own, making the experience unstable as the script grew.

The fix replaced this manual editor with an integration with **Sora Editor**, rendered inside Compose through `AndroidView`. The commit adds `io.github.Rosemoe.sora-editor` dependencies, enables Java 17/desugaring, includes TextMate grammars and theme in the assets (`javascript.tmLanguage.json`, `languages.json`, and `darcula.json`), and creates a dedicated initializer to load the JavaScript theme and language.

```kotlin
private object TextMateInitializer {
    private var initialized = false

    fun init(context: Context) {
        if (initialized) return

        synchronized(this) {
            if (initialized) return

            FileProviderRegistry.getInstance().addFileProvider(
                AssetsFileResolver(context.assets)
            )

            val themeInputStream = context.assets.open("textmate/darcula.json")
            val themeSource = IThemeSource.fromInputStream(themeInputStream, "darcula.json", null)
            val themeModel = ThemeModel(themeSource, "darcula")

            ThemeRegistry.getInstance().loadTheme(themeModel)
            ThemeRegistry.getInstance().setTheme("darcula")
            GrammarRegistry.getInstance().loadGrammars("textmate/languages.json")

            initialized = true
        }
    }
}
```

With this change, the screen now uses a native code editor for script editing, with line numbering, better scroll, keyboard, and syntax highlighting support via TextMate. When saving, **Beerus** also stops relying solely on the old Compose-maintained state and now retrieves the content directly from the editor instance (`editorRef?.text?.toString()`), normalizing line breaks before persisting the file.

```kotlin
Button(
    onClick = {
        val toPersist = (editorRef?.text?.toString() ?: selectedScriptContent.text)
            .replace("\r\n", "\n")

        saveScript(activity, selectedScript, toPersist)
        refreshScripts()
        Toast.makeText(activity, "Script saved successfully", Toast.LENGTH_SHORT).show()
    }
) {
    Text("Save", fontSize = 11.sp, color = Color.Red, fontFamily = ibmFont)
}
```

Beyond the editor, **v1.1 fixes a critical segfault** in the `fridaCore` binary (the frida-devkit C wrapper). The crash occurred in the `on_message` callback when Frida emitted a message that failed JSON parsing — `json_parser_get_root()` returned `NULL` and the subsequent call to `json_node_get_object(NULL)` accessed offset `0x10` of a null pointer, causing an **immediate `SIGSEGV` on the `frida-main-loop` thread**.

The fix adds validation of the parser's return before accessing the object. When the message is not valid **JSON**, `fridaCore` simply **prints the raw content and returns**, avoiding the **null pointer dereference**:

```c
static void on_message(
    FridaScript * script,
    const gchar * message,
    GBytes * data,
    gpointer user_data
) {
    JsonParser * parser;
    JsonObject * root;
    const gchar * type;

    parser = json_parser_new();

    if (!json_parser_load_from_data(parser, message, -1, NULL) ||
        json_parser_get_root(parser) == NULL) {
        g_print("on_message (raw): %s\n", message);
        g_object_unref(parser);
        return;
    }

    root = json_node_get_object(json_parser_get_root(parser));
    type = json_object_get_string_member(root, "type");

    if (strcmp(type, "log") == 0) {
        const gchar * log_message;
        log_message = json_object_get_string_member(root, "payload");
        g_print("%s\n", log_message);
    } else {
        g_print("on_message: %s\n", message);
    }

    g_object_unref(parser);
}
```

The `Android.mk` was also updated to include `-latomic` in `LOCAL_LDLIBS`, a flag required by frida-devkit itself but that was missing, which could cause additional crashes in **64-bit atomic operations on ARM32 devices**.

Previously, Beerus launched the target app via `startActivity`, waited 5 seconds with `sleep 5`, and only then attached by PID, which meant that any hook on `onCreate` or early in the Activity lifecycle simply did not work because the code had already executed.

Now `fridaCore` receives the **package name instead of PID** and uses `frida_device_spawn_sync` to **start the process suspended**, attaches and loads the script while the app is still paused, and only then calls `frida_device_resume_sync`.

```c
g_print("[*] Spawning %s...\n", argv[1]);

spawn_options = frida_spawn_options_new();
target_pid = frida_device_spawn_sync(local_device, argv[1], spawn_options, NULL, &error);
g_object_unref(spawn_options);

if (error != NULL) {
    g_printerr("Failed to spawn: %s\n", error->message);
    g_error_free(error);
    goto cleanup;
}

g_print("[*] Spawned PID: %u (suspended)\n", target_pid);

session = frida_device_attach_sync(local_device, target_pid, NULL, NULL, &error);
```

After the session is attached and the script is created, the **wrapper loads the script before releasing the target process execution**:

```c
g_signal_connect(script, "message", G_CALLBACK(on_message), NULL);
frida_script_load_sync(script, NULL, &error);

if (error != NULL) {
    g_printerr("Failed to load script: %s\n", error->message);
    g_error_free(error);
    frida_unref(script);
    frida_device_resume_sync(local_device, target_pid, NULL, NULL);
    frida_session_detach_sync(session, NULL, NULL);
    frida_unref(session);
    goto cleanup;
}

g_print("[*] Script loaded, resuming process...\n");
frida_device_resume_sync(local_device, target_pid, NULL, &error);
```

With this, hooks on `onCreate` and **at any point during initialization now work** because the script is already loaded **before the process leaves the suspended state**.

On the Kotlin side, `injectFridaCore` runs `am force-stop` **before each injection to ensure a clean spawn**, starts `fridaCore` passing the **package name**, and **streams** `stdout` and `stderr` on separate threads. This **replaces the previous flow based on `runSuCommand`**, which blocked until the process ended and prevented real-time feedback in the interface.

```kotlin
fun injectFridaCore(context: Context, packageName: String, script: String) {
    stopFridaCore()

    val scriptsFullPath = File(context.filesDir, "scripts").absolutePath + "/" + script
    isInjecting.value = true
    consoleLogs.clear()
    consoleLogs.add("[*] Starting injection: $packageName")

    Thread {
        try {
            val prep = Runtime.getRuntime().exec("su")
            val prepOut = DataOutputStream(prep.outputStream)
            prepOut.writeBytes("am force-stop $packageName\n")
            prepOut.writeBytes("exit\n")
            prepOut.flush()
            prep.waitFor()

            val process = Runtime.getRuntime().exec("su")
            fridaProcess = process

            val outputStream = DataOutputStream(process.outputStream)
            val inputStream = BufferedReader(InputStreamReader(process.inputStream))
            val errorStream = BufferedReader(InputStreamReader(process.errorStream))

            outputStream.writeBytes("fridaCore $packageName '$scriptsFullPath'\n")
            outputStream.flush()

            val stdoutThread = Thread {
                var line: String?
                while (inputStream.readLine().also { line = it } != null) {
                    line?.let { consoleLogs.add(it) }
                }
            }

            val stderrThread = Thread {
                var line: String?
                while (errorStream.readLine().also { line = it } != null) {
                    line?.let { consoleLogs.add("[err] $it") }
                }
            }

            stdoutThread.start()
            stderrThread.start()

            process.waitFor()
            stdoutThread.join()
            stderrThread.join()
            consoleLogs.add("[*] Process exited")
        } catch (e: Exception) {
            consoleLogs.add("[err] ${e.message}")
        } finally {
            isInjecting.value = false
            fridaProcess = null
        }
    }.start()
}
```

The stop flow was also adjusted. `stopFridaCore` first sends `SIGINT`, allowing graceful shutdown of the **GLib main loop**, script unload, and session detach. Only then does it fall back to `SIGKILL`. This avoids the previous problem where abruptly killing the process left an orphaned **Frida** agent inside the target app, and a second execution could cause instability or a device reboot.

```kotlin
fun stopFridaCore() {
    fridaProcess?.let { proc ->
        try {
            val kill = Runtime.getRuntime().exec("su")
            val out = DataOutputStream(kill.outputStream)

            // SIGINT triggers graceful shutdown (script unload + session detach)
            out.writeBytes("pkill -2 -f fridaCore\n")
            out.writeBytes("sleep 2\n")

            // force kill if still alive
            out.writeBytes("pkill -9 -f fridaCore\n")
            out.writeBytes("exit\n")
            out.flush()
            kill.waitFor()
            proc.destroy()
        } catch (_: Exception) {}

        fridaProcess = null
        isInjecting.value = false
        consoleLogs.add("[*] Stopped")
    }
}
```

Finally, the editor now includes an **integrated log console**. A terminal icon in the upper right corner of the screen indicates the console state (**white when inactive, red when active**). When clicking "Run", the console opens automatically and displays in real time the **stdout and stderr from `fridaCore`** — status messages in green, errors in red, and script output in gray.

```kotlin
@Composable
fun FridaConsoleView(modifier: Modifier = Modifier) {
    val listState = rememberLazyListState()

    LaunchedEffect(consoleLogs.size) {
        if (consoleLogs.isNotEmpty()) {
            listState.animateScrollToItem(consoleLogs.size - 1)
        }
    }

    LazyColumn(
        state = listState,
        modifier = modifier.fillMaxSize().padding(10.dp)
    ) {
        items(consoleLogs.size) { index ->
            val line = consoleLogs[index]
            val color = when {
                line.startsWith("[err]") -> Color(0xFFFF6B6B)
                line.startsWith("[*]") -> Color(0xFF69DB7C)
                else -> Color(0xFFD4D4D4)
            }

            Text(
                text = line,
                color = color,
                fontSize = 12.sp,
                fontFamily = ibmFont,
                modifier = Modifier.padding(vertical = 1.dp)
            )
        }
    }
}
```

In practice, the fix makes the **Frida Auto Injector** more reliable for running, writing, editing, and saving **Frida** scripts directly on the device, especially longer multi-line scripts, while also greatly improving the editor's operational and visual experience.

---

### Integrated Auto Update

v1.1 adds an **Auto Update** flow directly within **Beerus**. When launching the application, the framework queries the latest release from the official GitHub repository and compares the published version with the locally installed version.

This logic was centralized in a new `UpdateManager`, responsible for obtaining the current APK version via `PackageManager`, querying the GitHub releases endpoint, extracting the `tag_name` from the latest release, and locating, among the published assets, the `.apk` file available for download. Version comparison removes prefixes like `v`/`V` and compares numeric version components to decide whether a newer update is available.

When a new version is available, the app can display an automatic dialog at startup asking the user if they want to update at that moment. Additionally, the navigation gained a new **Update** screen, where it is possible to view the current version, the latest version, check status, error messages, download progress, and manually trigger the update check or installation.

The update process downloads the APK to the application's internal cache as `update.apk`, tracks download progress, and when the release provides a **SHA256** digest in the asset, validates file integrity before initiating installation. To deliver the APK to the Android installer, the commit adds the **REQUEST\_INSTALL\_PACKAGES** permission, configures a **FileProvider**, and declares the `file_paths.xml` file, allowing the downloaded APK to be shared via a secure URI.

In practice, this reduces the friction of keeping **Beerus** updated in lab environments — the user doesn't need to open the browser, manually search for the release, or download the APK externally. The app itself checks for a new version, downloads the correct asset, and forwards the installation through the standard Android flow.

---

### IPTables as an Alternative to Proxy Profiles

Traditionally, the framework configured a global proxy through Android settings, changing the system's `http_proxy` parameter to redirect traffic to tools like Burp Suite or mitmproxy. This approach remains available and works well for applications that respect the system proxy settings.

In practice, however, not all applications follow this behavior. Some clients implement their own network stacks, completely ignore global proxy settings, or make connections that do not go through the components responsible for consulting the `http_proxy` configured on Android. A recurring example is applications developed in **Flutter**, which use the engine's network stack based on **BoringSSL**. Depending on the implementation and configuration used by the application, connections may not automatically respect Android's global proxy definitions, making interception through `http_proxy` configuration alone difficult. In these scenarios, the traditional **Proxy Profiles** is no longer sufficient to guarantee traffic interception.

To work around this limitation, v1.1 adds an alternative based on **iptables**. When enabled, **Beerus** creates redirect rules in the `nat` table, intercepting outgoing connections and forwarding traffic to the configured proxy. Unlike the approach based solely on Android's global settings, the redirect occurs at a level closer to the operating system's network stack.

The new implementation was integrated directly into the **Proxy Profiles** interface, allowing switching between the traditional mode and the **iptables**\-based mode according to the test scenario. This offers greater flexibility for dealing with applications that ignore global proxies, manufacturer-modified environments, or specific behaviors of certain network libraries.

First, profiles now have an explicit mode, allowing each configuration to be saved as **HTTP** or **IPTABLES**:

```kotlin
enum class ProxyMode { HTTP, IPTABLES }

data class ProxyData(
    val name: String,
    val conString: String,
    val selected: Boolean,
    val mode: ProxyMode = ProxyMode.HTTP
)
```

When the mode is **HTTP**, **Beerus** applies Android's global proxy. When the mode is **IPTABLES**, it disables the global proxy and creates redirect rules for ports **80** and **443**:

```kotlin
when (mode) {
    ProxyMode.HTTP -> {
        runSuCommand("iptables -t nat -F") { }
        runSuCommand("runcon u:r:shell:s0 sh -c 'settings put global http_proxy $conString'") { }
    }

    ProxyMode.IPTABLES -> {
        runSuCommand("runcon u:r:shell:s0 sh -c 'settings put global http_proxy :0'") { }
        runSuCommand(
            "iptables -t nat -F" +
            " && iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination $conString" +
            " && iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination $conString" +
            " && iptables -t nat -A POSTROUTING -p tcp --dport 443 -j MASQUERADE" +
            " && iptables -t nat -A POSTROUTING -p tcp --dport 80 -j MASQUERADE"
        ) { }
    }
}
```

Finally, the interface now exposes this choice directly in the profile card. When switching between **HTTP Proxy** and **iptables**, the app saves the new mode, reapplies the profile if it is already selected, and syncs the value with the root module:

```kotlin
ProxyModeSelector(
    currentMode = proxy.mode,
    onModeChange = { newMode ->
        updateProfileMode(context, proxy.name, newMode)
        if (isSelected) {
            selectProfile(context, proxy.conString, newMode)
            if (hasModule) changeProperties("proxyMode", newMode.name)
        }
        refreshProxies()
    }
)
```

Another important detail is that this choice is no longer just an interface configuration. The selected mode is also persisted in the root module through the `proxyMode` property and automatically reapplied during device startup. So, when restarting Android, **Beerus** restores both the traditional `http_proxy`\-based mode and the **iptables** rules, preserving the user-configured behavior without requiring manual reconfiguration.

```bash
proxyModeProp=$(grep '^proxyMode=' "$STATUS_FILE" | cut -d'=' -f2)

if [ "$proxyModeProp" = "IPTABLES" ] && [ ! -z "$proxyProp" ] && [ "$proxyProp" != ":0" ]; then
    settings put global http_proxy ":0"
    iptables -t nat -F
    iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination "$proxyProp"
    iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination "$proxyProp"
    iptables -t nat -A POSTROUTING -p tcp --dport 443 -j MASQUERADE
    iptables -t nat -A POSTROUTING -p tcp --dport 80 -j MASQUERADE
else
    settings put global http_proxy "$proxyProp"
fi
```

In practice, **Beerus** now provides two complementary interception strategies: the traditional method based on Android proxy settings and an alternative mechanism using **iptables**, increasing compatibility with applications that would normally escape traffic instrumentation during a mobile test.

---

### UX/UI Refactor

v1.1 also received a visual refactor of the **Beerus Framework** interface, focused on improving navigation, separating mixed flows, and providing clearer feedback during interactive operations.

The first change adds a new visual identity to the application, with new backgrounds, animated images, and navigation adjustments to support screen transitions. For this, the project updates Compose image usage with **Glide** and **Landscapist**, in addition to adapting screens for this new visual behavior.

Another important adjustment was the separation between **Frida Setup** and **Frida Scripts**. Previously, **Frida** server configuration and script management were on the same screen. Now, server setup has its own dedicated screen, while the new **Frida Scripts** screen concentrates script creation, upload, search, editing, and removal, with dedicated cards, content preview, and confirmation before deletion.

The **ADB Over Network** screen was also redesigned. The previous static image was replaced by an animation representing connection states: idle, connecting, connected, and error. Additionally, the action of starting or stopping **ADB over Network** is now executed off the main UI thread, avoiding freezes while the root command is being processed.

With this, v1.1 makes the **Beerus** interface more organized, responsive, and beautiful.

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2026/06/captura-de-tela-2026-06-24-as-23-28-56.png)

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2026/06/captura-de-tela-2026-06-24-as-23-30-05.png)

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2026/06/captura-de-tela-2026-06-24-as-23-45-57.png)

---

### Honorable Mention: Magisk Detection by Our First External Contributor

This version also marks an important moment for the project: we received our first external contribution to the **Beerus Framework**. The contribution came from **dapsvi**, who fixed the **Magisk** detection logic in the initialization flow. Previously, **Beerus** relied on hardcoded paths such as `/system/bin/magisk`, `/sbin/magisk`, and Zygisk-related libraries to infer whether **Magisk** was present on the device. This approach could fail in environments where the binary was not at these paths or where the installation followed a different structure. The fix now validates **Magisk**'s presence by executing `magisk -v`, looking for the `:MAGISK:` signature in the command output, and combining this result with verification of the module directory at `/data/adb/modules/beerusMagiskModule`.

The PR changes this validation to use **Magisk**'s own binary as the source of truth. Instead of looking for specific files in the system, **Beerus** executes `magisk -v` and checks whether the output contains the `:MAGISK:` signature:

```kotlin
fun detectMagisk(callback: (Boolean) -> Unit) {
    val cmd = """
        output=$(magisk -v 2>/dev/null)
        case "$output" in
          *:MAGISK:*) echo true ;;
          *) echo false ;;
        esac
    """.trimIndent()

    runSuCommand(cmd) {
        callback(it.trim() == "true")
    }
}
```

Module detection was also adjusted to validate the correct directory at `/data/adb/modules/beerusMagiskModule` and combine this result with the actual presence of **Magisk**. With this, **Beerus** only considers the module active when the directory exists and the installed **Magisk** responds correctly:

```kotlin
fun detectRootModuleInstalled(callback: (Boolean) -> Unit) {
    val cmd = """
        if [ -d /data/adb/modules/beerusMagiskModule ]; then
            echo true
        else
            echo false
        fi
    """.trimIndent()

    runSuCommand(cmd) { dirResult ->
        val moduleDirExists = dirResult.trim() == "true"

        detectMagisk { magiskPresent ->
            callback(moduleDirExists && magiskPresent)
        }
    }
}
```

With this, detection becomes less dependent on specific system paths and more aligned with the actual behavior of the **Magisk** installed on the device. We extend our gratitude to **dapsvi** for this initiative as the project's first external contributor.

---

# Full Changelog

| PR  | Repository     | Type     | Description                                                                       |
| --- | -------------- | -------- | --------------------------------------------------------------------------------- |
| #33 | beerus-android | Fix      | Frida Auto Inject - Segfault fix, spawn mode injection, and real-time log console |
| #32 | beerus-android | Fix      | Inconsistent beerusRootModule name during detection                               |
| #31 | beerus-android | Feature  | New background, screen animations, and Frida screen separation                    |
| #30 | beerus-android | Fix      | Magisk detection fix by **dapsvi**                                                |
| #29 | beerus-android | Feature  | IPTables as an alternative to Proxy Profiles                                      |
| #28 | beerus-android | Refactor | General UX/UI refactor                                                            |
| #27 | beerus-android | Feature  | App Auto Update                                                                   |
| #26 | beerus-android | Feature  | gRPC transport for Memory Dump and Sandbox Exfiltration                           |
| #7  | beerus-server  | Feature  | gRPC service implementation for receiving dumps and exfiltrations                 |
| #25 | beerus-android | Fix      | Frida Auto Inject - Code Editor                                                   |
| #24 | beerus-android | Fix      | Root Modules UI freezing on startup                                               |
| #23 | beerus-android | Fix      | Memory Dump performance optimization                                              |
| #20 | beerus-android | Fix      | Trusted System Certificates + Root Manager icons                                  |
| #19 | beerus-android | Feature  | KernelSU support                                                                  |

---

# How to Get It

Beerus Framework v1.1 is available for download at the official repository: [**github.com/hakaioffsec/beerus-android**](https://github.com/hakaioffsec/beerus-android?ref=yokai.hakaisecurity.io)

**Requirements:**

- Android device rooted via **Magisk** or **KernelSU**
- Android 10 or higher recommended

Starting from the v1.1 update installed, future updates can be applied directly through the app with the new auto-update system.

---

# Contribution

Issues, pull requests, and bug reports are welcome on the repository. The project is open for community contributions.

In the coming months we'll have plenty of news! d(^o^)b