> ## 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 e Correções
- URL: https://yokai.hakaisecurity.io/beerus-framework-v1-1-kernelsu-grpc-transport-e-correcoes/
- Published: 2026-07-02T20:39:34.000Z
- Updated: 2026-07-02T20:39:34.000Z
- Author: daniel chactoura
- Tags: Research Blog, #wp, pt-br, #pair-beerus-v1.1

Alguns meses após o lançamento inicial do **Beerus Framework**, estamos publicando a versão **v1.1**, uma atualização minor que traz suporte a novos ambientes de root, uma mudança arquitetural relevante no transporte de dados, auto-atualização integrada e um conjunto de correções acumuladas desde o primeiro release.

Neste post, detalhamos cada mudança incluída nessa versão, o contexto técnico por trás de cada decisão e o que isso significa na prática para quem usa o **Beerus** no dia a dia de testes em Android.

---

**TL;DR:** O Beerus Framework v1.1 amplia a compatibilidade e melhora a experiência operacional do pentest Android, com suporte a [KernelSU](#h-kernelsu-support), correções no fluxo de [Trusted System Certificates](#h-fix-trusted-system-certificates), otimizações de performance no [Memory Dump](#h-fix-memory-dump-performance-optimization) e uma nova opção de transporte via gRPC para dumps e sandbox exfiltration. A atualização também resolve travamentos em [Root Modules](#h-fix-root-modules-freezing-on-startup), evolui o [Frida Auto Injector](#h-fix-frida-auto-injector-code-editor-console-and-fixes) com editor, console e injeção antecipada, adiciona [auto-update integrado](#h-integrated-auto-update), oferece [iptables como alternativa aos Proxy Profiles](#h-iptables-as-an-alternative-to-proxy-profiles) e traz uma [refatoração de UX/UI](#h-ux-ui-refactor). Por fim, destacamos a primeira contribuição externa ao projeto, que aprimorou a [detecção do Magisk](#h-honorable-mention-magisk-detection-by-our-first-external-contributor), além do [changelog completo](#h-full-changelog) e das instruções de [instalação](#h-how-to-get-it) e [contribuição](#h-contribution).

---

# Contexto

O **Beerus Framework** foi criado para consolidar as principais operações de pentest mobile em uma única interface instalada diretamente no dispositivo alvo. Ao invés de depender de múltiplas ferramentas externas, scripts avulsos e configurações manuais repetitivas, o **Beerus** expõe essas capacidades de forma centralizada, desde injeção de **Frida** até dump de memória e extração de sandbox.

A versão 1.0 estabeleceu essa base. A v1.1 corrige o bugs, expande compatibilidade, remodela interfaces e prepara o terreno para os próximos desenvolvimentos.

---

# Novidades

### KernelSU Support

O **Beerus** assumia o **Magisk** como **backend** principal de root. Isso funcionava bem para a maioria dos dispositivos de laboratório, mas limitava o framework a um modelo específico de **root manager**. Na prática, usuários com **KernelSU** conseguiam ter root no dispositivo, mas o **Beerus** ainda tratava o ambiente como se ele precisasse ser obrigatoriamente baseado em **Magisk**.

A v1.1 muda esse comportamento adicionando uma validação específica para **KernelSU** no fluxo de inicialização. Agora, o aplicativo tenta detectar primeiro o **Magisk**. Caso ele não esteja presente, o **Beerus** executa uma segunda validação utilizando o `ksud`, daemon responsável pelo gerenciamento do **KernelSU**. Se essa validação retornar uma versão válida, o framework considera que existe um ambiente de root compatível e habilita normalmente todos os recursos que dependem de privilégios elevados.

```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)
}
```

A principal diferença entre **Magisk** e **KernelSU** está na arquitetura. O **Magisk** atua durante o processo de boot, modificando o `initramfs` para criar um ambiente **systemless** capaz de fornecer acesso root, módulos e alterações no sistema sem modificar diretamente as partições originais. Já o **KernelSU** implementa o gerenciamento de privilégios diretamente no **kernel**, utilizando o `ksud` para controlar a concessão de permissões root aos aplicativos autorizados.

Por isso, o **Beerus** deixa de depender exclusivamente da detecção do **Magisk**. O fluxo de inicialização foi ajustado para tratar **Magisk** e **KernelSU** como dois **root managers** suportados. Quando o **Magisk** é encontrado, o comportamento permanece inalterado. Caso contrário, o aplicativo tenta detectar o **KernelSU** e, se ele estiver disponível, habilita o restante do fluxo normalmente, permitindo que os recursos dependentes de root sejam utilizados independentemente do gerenciador instalado.

```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)
                        }
                    }
                }
            }
        }
    }
}
```

Para o usuário, a principal diferença prática é que o **Beerus Framework** deixa de ser exclusivo para ambientes baseados em **Magisk**. Em dispositivos com **KernelSU**, basta conceder ao aplicativo a permissão de **Root Access by UID** através do próprio gerenciador. Após essa autorização, o **Beerus** passa a ter acesso aos privilégios necessários para executar normalmente suas funcionalidades.

Com essa mudança, recursos dependentes de root, como **Frida Core**, manipulação de propriedades do sistema, **Boot Options**, **Trusted System Certificates** e gerenciamento de módulos passam a funcionar também em ambientes baseados em **KernelSU**, ampliando a compatibilidade do framework sem alterar a experiência de uso para quem continua utilizando **Magisk**.

---

### Fix: Trusted System Certificates

Um dos recursos do **Beerus Framework** é a promoção de certificados instalados pelo usuário para a trust store utilizada pelo sistema, permitindo a interceptação TLS em aplicativos que não aceitam certificados presentes apenas na trust store de usuário, comportamento comum em aplicações com configurações restritivas de segurança de rede ou compiladas para API level 24+.

Na v1.0, esse fluxo seguia uma abordagem mais simples: os certificados presentes em `/data/misc/user/0/cacerts-added` eram copiados para `/system/etc/security/cacerts`. Embora essa técnica tenha funcionado por muitos anos, versões mais recentes do Android passaram a utilizar a trust store disponibilizada pelo módulo **Conscrypt** através do APEX `/apex/com.android.conscrypt/cacerts`, tornando insuficiente modificar apenas o caminho tradicional de certificados do sistema.

Na prática, o certificado promovido pelo **Beerus** podia estar presente em `/system/etc/security/cacerts`, mas o mecanismo responsável pela validação TLS continuava consultando a trust store exposta pelo **Conscrypt**, fazendo com que o certificado não fosse reconhecido como uma âncora de confiança válida.

A correção refez completamente o processo de promoção dos certificados. Agora, quando a opção **Trusted System Certificates** está ativa, o **Beerus** cria uma trust store temporária contendo os certificados originais do sistema e os certificados instalados pelo usuário. Após aplicar as permissões e o contexto **SELinux** adequados (`u:object_r:system_security_cacerts_file:s0`), o framework utiliza **bind mounts** para expor essa trust store consolidada nos caminhos efetivamente utilizados pelo dispositivo, incluindo `/apex/com.android.conscrypt/cacerts` e, quando aplicável, `/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
...
```

Com isso, o **Beerus** passa a disponibilizar uma visão unificada da trust store do sistema, preservando os certificados originais do Android e adicionando os certificados instalados pelo usuário nos locais efetivamente consultados durante o processo de validação TLS.

![](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

Além da mudança de transporte, o processo de **Memory Dump** também recebeu uma otimização direta no fluxo de coleta de strings da memória do processo alvo.

Na implementação anterior, o **Beerus** percorria todas as regiões legíveis presentes em `/proc/<pid>/maps`. Para cada região, o código calculava o intervalo de memória e executava a leitura em `/proc/<pid>/mem` usando `dd` com `bs=1`, ou seja, byte a byte. Em processos com muitas regiões mapeadas ou regiões muito grandes, essa abordagem podia tornar o dump extremamente lento, motivo pelo qual a interface exibia um aviso de que a operação poderia levar entre 20 e 30 minutos.

A correção mudou essa estratégia para reduzir o volume de memória analisado e tornar a leitura mais eficiente. Agora, antes de executar o dump, o **Beerus** filtra o `/proc/<pid>/maps` buscando apenas regiões com permissão de leitura e escrita relacionadas a áreas mais úteis para extração de strings, como `heap`, regiões anônimas e artefatos do runtime Android (`.dex`, `.odex` e `.oat`).

Além disso, regiões vazias ou inválidas são ignoradas, e regiões maiores que **20MB** deixam de ser processadas para evitar que uma única área de memória comprometa o tempo total da operação. A leitura também deixou de ser feita byte a byte: o `dd` passou a utilizar páginas de **4096 bytes**, calculando `skip` e `count` com base no tamanho da página, o que reduz significativamente o overhead de leitura em `/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")
                            }
                        }
                    }
                }
            }
        }
    }
```

O resultado é um **Memory Dump** mais rápido e previsível. Em vez de tentar extrair strings de todo e qualquer mapping legível do processo, o **Beerus** passa a focar em regiões com maior chance de conter dados úteis para análise, evitando leituras excessivamente grandes e reduzindo o custo operacional da coleta.

---

### gRPC Transport for Memory Dump and Sandbox Exfiltration

As duas operações mais pesadas do framework, **Memory Dump** e **Sandbox Exfiltration**. Até então, essas funcionalidades já conseguiam enviar os artefatos coletados para o **Beerus Server** através do fluxo **HTTP/HTTPS** existente, que continua disponível e segue como o modo padrão de comunicação.

A mudança da v1.1 não remove esse caminho anterior. Em vez disso, ela adiciona um novo transporte opcional baseado em **gRPC**, permitindo que o usuário escolha qual protocolo utilizar de acordo com o ambiente de teste. Na prática, quando o endereço configurado no aplicativo utiliza `http://` ou `https://`, o **Beerus** mantém o fluxo tradicional. Quando o endereço utiliza o prefixo `grpc://`, as operações passam a usar o novo cliente **gRPC**.

Essa escolha é importante porque **Memory Dump** e **Sandbox Exfiltration** normalmente produzem arquivos grandes em formato `.tar.gz`. Dumps de memória podem chegar facilmente a centenas de megabytes, enquanto uma exfiltração de sandbox pode empacotar uma grande quantidade de arquivos internos do aplicativo alvo. Nesses cenários, um transporte orientado a streaming tende a ser mais adequado do que tratar o envio como uma única requisição **HTTP** comum.

No lado do Android, a v1.1 adiciona um cliente **gRPC** dedicado para upload desses artefatos. Após o **Beerus** gerar o `.tar.gz`, o framework verifica o tipo de servidor configurado: se for `grpc://`, o arquivo é enviado pelo `BeerusGrpcUploader`; caso contrário, o envio **HTTP** existente continua sendo utilizado. Isso preserva compatibilidade com ambientes antigos e, ao mesmo tempo, oferece uma alternativa mais robusta para operações pesadas.

O protocolo **gRPC** foi definido em um arquivo `.proto` compartilhado entre cliente e servidor. Ele expõe um serviço **BeerusTransfer** com duas operações principais: `Check`, usada para validar se o servidor **gRPC** está disponível, e `Upload`, que recebe um stream de `UploadChunk`. O primeiro chunk carrega os metadados do artefato, como tipo da coleta, nome do arquivo, pacote relacionado e se o `.tar.gz` deve ser extraído. Os chunks seguintes carregam os dados binários do arquivo.

```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;
}
```

Esse modelo traz algumas vantagens práticas. O servidor consegue receber o arquivo de forma incremental, validar a ordem dos chunks através de um número de sequência, calcular o hash **SHA256** durante o recebimento e retornar ao cliente informações como sucesso da operação, caminho salvo, quantidade de bytes recebidos e hash final do artefato. Além disso, como o contrato entre cliente e servidor fica descrito em **Protocol Buffers**, a evolução futura do transporte passa a ser mais previsível e menos dependente de formatos ad-hoc.

No **Beerus Server**, o **HTTP** também continua existindo como modo padrão. A nova versão adiciona um modo **gRPC** separado, iniciado com `--grpc`, normalmente utilizando uma porta dedicada. Nesse modo, o servidor sobe o serviço **BeerusTransfer**, recebe os uploads em streaming, salva os artefatos no diretório de saída e, quando solicitado pelo cliente, realiza a extração segura dos arquivos `.tar.gz` recebidos.

Com isso, o **Beerus** passa a oferecer dois caminhos de comunicação: o fluxo **HTTP/HTTPS** tradicional, simples e compatível, e o novo fluxo **gRPC**, mais adequado para transferências maiores e operações que se beneficiam de streaming, contrato tipado e melhor controle sobre o ciclo de recebimento dos artefatos.

---

### Fix: Root Modules Freezing on Startup

Em algumas situações, a interface podia travar ou ficar presa durante a inicialização da lista de módulos, especialmente ao abrir o aplicativo e carregar os módulos instalados pelo **root manager**.

O problema estava na forma como o estado dos módulos era carregado e atualizado pela UI. Na implementação anterior, a enumeração dos módulos era iniciada apenas quando a tela de **Root Modules** era aberta. Como essa operação dependia da execução de comandos com privilégios de root, a composição da interface podia ficar bloqueada enquanto a lista era descoberta. Além disso, algumas consultas, como a verificação do estado de um módulo, utilizavam uma lógica síncrona baseada em `wait()`/`notify()`, contribuindo para congelamentos da interface.

A principal mudança foi mover a enumeração dos módulos para a inicialização do aplicativo. Agora, a **MainActivity** realiza a descoberta dos módulos durante o `onCreate()`, armazenando o resultado em um estado compartilhado (`RootModulesState`) que passa a ser observado pelas telas do Compose:

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

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

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

A própria função responsável pela enumeração também deixou de utilizar um fluxo síncrono, passando a retornar o resultado através de um callback assíncrono:

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

O mesmo padrão foi aplicado à consulta do estado de cada módulo. Em vez de bloquear a thread aguardando o término do comando root, a resposta passa a ser entregue diretamente pelo 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")
    }
}
```

Com essa mudança, a enumeração dos módulos passa a ocorrer apenas uma vez durante a inicialização do aplicativo, enquanto a interface apenas observa um estado já carregado. Além de eliminar operações bloqueantes baseadas em `wait()`/`notify()`, isso reduz consultas repetidas ao sistema de arquivos e torna a tela de **Root Modules** significativamente mais responsiva, tanto em ambientes **Magisk** quanto **KernelSU**.

---

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

O **Frida Auto Injector** permite escrever e injetar scripts **Frida** diretamente no dispositivo, sem depender de conexão com um host externo. Na v1.1, o editor embutido foi refeito para resolver problemas de usabilidade e confiabilidade ao editar scripts maiores ou com múltiplas linhas.

A implementação anterior usava um `BasicTextField` do Compose combinado com um `VisualTransformation` próprio para highlight de JavaScript (`JsSyntaxHighlighter`). Essa abordagem funcionava para casos simples, mas era limitada para códigos maiores. O campo de texto precisava lidar sozinho com scroll, cursor, teclado, seleção e renderização de syntax highlighting, o que tornava a experiência instável conforme o script crescia.

A correção substituiu esse editor manual por uma integração com o **Sora Editor**, renderizada dentro do Compose através de `AndroidView`. O commit adiciona as dependências `io.github.Rosemoe.sora-editor`, habilita Java 17/desugaring, inclui gramáticas e tema TextMate nos assets (`javascript.tmLanguage.json`, `languages.json` e `darcula.json`) e cria um inicializador dedicado para carregar o tema e a linguagem JavaScript.

```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
        }
    }
}
```

Com essa mudança, a tela passa a usar um editor de código nativo para edição de scripts, com numeração de linhas, auto-complete, suporte melhor a scroll, teclado e syntax highlighting via TextMate. Na hora de salvar, o **Beerus** também deixa de depender apenas do estado antigo mantido pelo Compose e passa a recuperar o conteúdo diretamente da instância do editor (`editorRef?.text?.toString()`), normalizando quebras de linha antes de persistir o arquivo.

```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)
}
```

Além do editor, a **v1.1 corrige um segfault crítico** no binário `fridaCore` (o wrapper C do frida-devkit). O crash ocorria no callback `on_message`, quando o Frida emitia uma mensagem que falhava no parse JSON, `json_parser_get_root()` retornava `NULL` e a chamada seguinte a `json_node_get_object(NULL)` acessava o offset `0x10` de um ponteiro nulo, **`SIGSEGV` imediato na thread `frida-main-loop`**.

A correção adiciona validação do retorno do parser antes de acessar o objeto. Quando a mensagem não é um **JSON** válido, o `fridaCore` apenas **imprime o conteúdo bruto e retorna**, evitando o **dereference de ponteiro nulo**:

```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);
}
```

O `Android.mk` também passou a incluir `-latomic` nos `LOCAL_LDLIBS`, flag exigida pelo próprio frida-devkit mas que estava ausente, o que poderia causar crashes adicionais em **operações atômicas de 64 bits em dispositivos ARM32**.

Antes, o **Beerus** iniciava o app alvo via `startActivity`, esperava 5 segundos com `sleep 5` e só então fazia attach pelo PID, o que significava que qualquer hook em `onCreate` ou no início do ciclo de vida da Activity simplesmente não funcionava, porque o código já tinha executado.

Agora o `fridaCore` recebe o **package name em vez do PID** e usa `frida_device_spawn_sync` para **iniciar o processo suspenso**, faz attach e carrega o script enquanto o app ainda está parado, e só então chama `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);
```

Depois que a sessão está anexada e o script foi criado, o **wrapper** **carrega o script antes de liberar a execução do processo alvo**:

```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);
```

Com isso, hooks em `onCreate` e **em qualquer ponto da inicialização passam a funcionar**, porque o script já está carregado **antes do processo sair do estado suspenso**.

No lado Kotlin, o `injectFridaCore` executa `am force-stop` **antes de cada injeção para garantir um spawn limpo**, inicia o `fridaCore` passando o **package name** e faz **streaming** de `stdout` e `stderr` em threads separadas. Isso **substitui o fluxo anterior baseado em `runSuCommand`**, que bloqueava até o fim do processo e impedia feedback em tempo real na 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()
}
```

O fluxo de parada também foi ajustado. O `stopFridaCore` primeiro envia `SIGINT`, permitindo o shutdown graceful do **GLib main loop**, unload do script e detach da sessão. Só depois ele recorre ao `SIGKILL` como fallback. Isso evita o problema anterior em que matar o processo abruptamente deixava um agente **Frida** órfão dentro do app alvo, e uma segunda execução podia causar instabilidade ou reboot do dispositivo.

```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")
    }
}
```

Por fim, o editor agora inclui um **console de logs integrado**. Um ícone de terminal no canto superior direito da tela indica o estado do console (**branco inativo, vermelho quando ativo**). Ao clicar em "Run", o console abre automaticamente e exibe em tempo real o **stdout e stderr do `fridaCore`**, mensagens de status em verde, erros em vermelho, e output do script em cinza.

```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)
            )
        }
    }
}
```

Na prática, o fix torna o **Frida Auto Injector** mais confiável para executar, escrever, editar e salvar scripts **Frida** diretamente no dispositivo, especialmente scripts mais longos e multiline, além de melhorar bastante a experiência operacional e visual do editor.

---

### Integrated Auto Update

A v1.1 adiciona um fluxo de **Auto Update** diretamente dentro do **Beerus**. Ao iniciar o aplicativo, o framework consulta a release mais recente do repositório oficial no GitHub e compara a versão publicada com a versão instalada localmente.

Essa lógica foi centralizada em um novo `UpdateManager`, responsável por obter a versão atual do APK via `PackageManager`, consultar o endpoint de releases do GitHub, extrair o `tag_name` da última release e localizar, entre os assets publicados, o arquivo `.apk` disponível para download. A comparação de versões remove prefixos como `v`/`V` e compara os componentes numéricos da versão para decidir se existe uma atualização mais nova.

Quando uma nova versão está disponível, o app pode exibir um diálogo automático na inicialização perguntando se o usuário deseja atualizar naquele momento. Além disso, a navegação ganhou uma nova tela **Update**, onde é possível visualizar a versão atual, a versão mais recente, o status da checagem, mensagens de erro, progresso de download e disparar manualmente a verificação ou instalação da atualização.

O processo de atualização baixa o APK para o cache interno do aplicativo como `update.apk`, acompanha o progresso do download e, quando a release fornece um digest **SHA256** no asset, valida a integridade do arquivo antes de iniciar a instalação. Para entregar o APK ao instalador do Android, o commit adiciona a permissão **REQUEST\_INSTALL\_PACKAGES**, configura um **FileProvider** e declara o arquivo `file_paths.xml`, permitindo compartilhar o APK baixado via URI segura.

Na prática, isso reduz a fricção de manter o **Beerus** atualizado em ambientes de laboratório, o usuário não precisa abrir o navegador, procurar a release manualmente ou baixar o APK por fora. O próprio app verifica se existe uma versão nova, baixa o asset correto e encaminha a instalação pelo fluxo padrão do Android.

---

### IPTables as an Alternative to Proxy Profiles

Tradicionalmente, o framework configurava um proxy global através das configurações do Android, alterando o parâmetro `http_proxy` do sistema para redirecionar o tráfego para ferramentas como Burp Suite ou mitmproxy. Essa abordagem continua disponível e funciona bem para aplicações que respeitam as configurações de proxy do sistema.

Na prática, entretanto, nem todos os aplicativos seguem esse comportamento. Alguns clientes implementam suas próprias stacks de rede, ignoram completamente as configurações globais de proxy ou realizam conexões que não passam pelos componentes responsáveis por consultar o `http_proxy` configurado no Android. Um exemplo recorrente são aplicações desenvolvidas em **Flutter**, que utilizam a stack de rede da engine baseada em **BoringSSL**. Dependendo da implementação e da configuração utilizada pelo aplicativo, as conexões podem não respeitar automaticamente as definições de proxy global do Android, dificultando a interceptação apenas através da configuração de `http_proxy`. Nesses cenários, o **Proxy Profiles** tradicional deixa de ser suficiente para garantir a interceptação do tráfego.

Para contornar essa limitação, a v1.1 adiciona uma alternativa baseada em **iptables**. Quando habilitada, o **Beerus** passa a criar regras de redirecionamento na tabela `nat`, interceptando conexões de saída e encaminhando o tráfego para o proxy configurado. Diferentemente da abordagem baseada apenas em configurações globais do Android, o redirecionamento ocorre em um nível mais próximo da pilha de rede do sistema operacional.

A nova implementação foi integrada diretamente à interface de **Proxy Profiles**, permitindo alternar entre o modo tradicional e o modo baseado em **iptables** de acordo com o cenário de teste. Isso oferece maior flexibilidade para lidar com aplicações que ignoram proxies globais, ambientes modificados por fabricantes ou comportamentos específicos de determinadas bibliotecas de rede.

Primeiro, os perfis agora possuem um modo explícito, permitindo que cada configuração seja salva como **HTTP** ou **IPTABLES**:

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

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

Quando o modo é **HTTP**, o **Beerus** aplica o proxy global do Android. Quando o modo é **IPTABLES**, ele desativa o proxy global e cria regras de redirecionamento para as portas **80** e **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"
        ) { }
    }
}
```

Por fim, a interface passou a expor essa escolha diretamente no card do perfil. Ao alternar entre **HTTP Proxy** e **iptables**, o app salva o novo modo, reaplica o perfil caso ele já esteja selecionado e sincroniza o valor com o módulo root:

```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()
    }
)
```

Outro detalhe importante é que essa escolha deixa de ser apenas uma configuração da interface. O modo selecionado também é persistido no módulo root através da propriedade `proxyMode` e reaplicado automaticamente durante a inicialização do dispositivo. Assim, ao reiniciar o Android, o **Beerus** restaura tanto o modo tradicional baseado em `http_proxy` quanto as regras de **iptables**, preservando o comportamento configurado pelo usuário sem necessidade de reconfiguração manual.

```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
```

Na prática, o **Beerus** passa a disponibilizar duas estratégias complementares de interceptação: o método tradicional baseado em configurações de proxy do Android e um mecanismo alternativo utilizando **iptables**, aumentando a compatibilidade com aplicações que normalmente escapariam da instrumentação de tráfego durante um teste mobile.

---

### UX/UI Refactor

A v1.1 também recebeu uma refatoração visual na interface do **Beerus Framework**, focada em melhorar a navegação, separar fluxos que estavam misturados e dar feedback mais claro durante operações interativas.

A primeira mudança adiciona uma nova identidade visual ao aplicativo, com novos backgrounds, imagens animadas e ajustes na navegação para suportar transições entre telas. Para isso, o projeto atualiza o uso de imagens no Compose com **Glide** e **Landscapist**, além de adaptar telas para o esse novo comportamento visual.

Outro ajuste importante foi a separação entre **Frida Setup** e **Frida Scripts**. Antes, a configuração do servidor **Frida** e o gerenciamento de scripts ficavam na mesma tela. Agora, o setup do servidor fica em uma tela dedicada, enquanto a nova tela **Frida Scripts** concentra criação, upload, busca, edição e remoção de scripts, com cards próprios, preview do conteúdo e confirmação antes de deletar.

A tela **ADB Over Network** também foi redesenhada. A imagem estática anterior foi substituída por uma animação que representa os estados de conexão: idle, conectando, conectado e erro. Além disso, a ação de iniciar ou parar o **ADB over Network** passou a ser executada fora do fluxo principal da UI, evitando travamentos enquanto o comando root é processado.

Com isso, a v1.1 deixa a interface do **Beerus** mais organizada, responsiva e bela.

![](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

Esta versão também marca um momento importante para o projeto: recebemos nossa primeira contribuição externa no **Beerus Framework**. A contribuição veio de **dapsvi**, que corrigiu a lógica de detecção do **Magisk** no fluxo de inicialização. Antes, o **Beerus** dependia de caminhos hardcoded, como `/system/bin/magisk`, `/sbin/magisk` e bibliotecas relacionadas ao Zygisk, para inferir se o **Magisk** estava presente no dispositivo. Essa abordagem podia falhar em ambientes onde o binário não estava nesses caminhos ou onde a instalação seguia uma estrutura diferente. A correção passou a validar a presença do **Magisk** através da execução de `magisk -v`, procurando a assinatura `:MAGISK:` na saída do comando, e combinando esse resultado com a verificação do diretório do módulo em `/data/adb/modules/beerusMagiskModule`.

O PR altera essa validação para usar o próprio binário do **Magisk** como fonte de verdade. Em vez de procurar arquivos específicos no sistema, o **Beerus** executa `magisk -v` e verifica se a saída contém a assinatura `:MAGISK:`:

```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")
    }
}
```

A detecção do módulo também foi ajustada para validar o diretório correto em `/data/adb/modules/beerusMagiskModule` e combinar esse resultado com a presença real do **Magisk**. Com isso, o **Beerus** só considera o módulo ativo quando o diretório existe e o **Magisk** instalado responde corretamente:

```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)
        }
    }
}
```

Com isso, a detecção fica menos dependente de caminhos específicos do sistema e mais alinhada ao comportamento real do **Magisk** instalado no dispositivo. Fica aqui nosso agradecimento ao **dapsvi** por essa iniciativa como primeiro colaborador externo do projeto.

---

# Full Changelog

| PR  | Repositório    | Tipo     | Descrição                                                                                        |
| --- | -------------- | -------- | ------------------------------------------------------------------------------------------------ |
| #33 | beerus-android | Fix      | Frida Auto Inject - Correção de segfault, injeção via spawn mode e console de logs em tempo real |
| #32 | beerus-android | Fix      | Nome do beerusRootModule inconsistente durante a detecção                                        |
| #31 | beerus-android | Feature  | Novo background, animações de tela e separação das telas do Frida                                |
| #30 | beerus-android | Fix      | Correção na detecção do Magisk por **dapsvi**                                                    |
| #29 | beerus-android | Feature  | IPTables como alternativa aos Proxy Profiles                                                     |
| #28 | beerus-android | Refactor | Refatoração geral da UX/UI                                                                       |
| #27 | beerus-android | Feature  | App Auto Update                                                                                  |
| #26 | beerus-android | Feature  | gRPC transport para Memory Dump e Sandbox Exfiltration                                           |
| #7  | beerus-server  | Feature  | Implementação dos serviços gRPC para recebimento de dumps e exfiltrações                         |
| #25 | beerus-android | Fix      | Frida Auto Inject - Code Editor                                                                  |
| #24 | beerus-android | Fix      | UI do Root Modules travando na inicialização                                                     |
| #23 | beerus-android | Fix      | Otimização de performance no Memory Dump                                                         |
| #20 | beerus-android | Fix      | Trusted System Certificates + ícones de Root Manager                                             |
| #19 | beerus-android | Feature  | Suporte a KernelSU                                                                               |

---

# How to Get It

O Beerus Framework v1.1 está disponível para download no repositório oficial: [**github.com/hakaioffsec/beerus-android**](https://github.com/hakaioffsec/beerus-android?ref=yokai.hakaisecurity.io)

**Requisitos:**

- Dispositivo Android com root via **Magisk** ou **KernelSU**
- Android 10 ou superior recomendado

A partir da atualização v1.1 instalada, poderá ser atualizado diretamente pelo app com o novo sistema de auto-update.

---

# Contribution

Issues, pull requests e reports de bugs são bem-vindos no repositório. O projeto está aberto para contribuições da comunidade.

Nos próximos meses teremos bastante novidades! d(^o^)b