> ## 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.

# PT-BR - Beerus Framework: iOS - O canivete suíço para iOS
- URL: https://yokai.hakaisecurity.io/beerus-framework-ios-the-swiss-army-knife-for-ios-2/
- Published: 2026-09-18T13:49:44.000Z
- Updated: 2026-09-18T19:49:16.000Z
- Author: Daniel Franca Lima
- Tags: pt-br, Research Blog, #pair-beerus-ios

Em julho deste ano, lançamos a **v1.1 do Beerus Framework para Android**. Agora, em setembro, damos mais um passo na evolução do projeto com o lançamento do **Beerus Framework para iOS**, trazendo a visão e os recursos do framework também para o ecossistema da Apple. Esse lançamento também contou com a contribuição dos membros do time de pentest da Hakai, Daniel "Daniboy" Lima e Gabriel "Texugo" Rodrigues e a contribuição de Kelvin Montini, que ajudou no desenvolvimento e na evolução do projeto.

Com isso, neste post vamos abordar sua estrutura, seus recursos, o contexto técnico por trás de algumas decisões e como cada um pode ajudar os usuários que dependem do Beerus no dia a dia para testes em dispositivos iOS.

## Contexto

O Beerus Framework é uma ferramenta de segurança ofensiva para dispositivos móveis, criada para centralizar e simplificar o processo de pentesting diretamente no dispositivo. Agora disponível para iOS, o framework foi construído sobre o Frida e o Palera1n para oferecer, por meio de uma interface unificada, recursos como instrumentação dinâmica de aplicações, extração de dados do sandbox, memory dumping, configuração de proxy e extração de arquivos IPA. Com uma arquitetura modular e extensível, o Beerus busca agilizar tarefas recorrentes e facilitar a execução de diferentes técnicas durante testes de segurança em dispositivos iOS com Jailbreak.

## Introdução Técnica

O Beerus Framework funciona como um hub de recursos para analistas de segurança que trabalham com dispositivos iOS. O app oferece uma interface gráfica intuitiva com acesso a uma variedade de ferramentas, aproveitando os próprios recursos do dispositivo e privilégios de root (projetado para trabalhar em maior sincronia com o Palera1n).

As funções disponíveis são:

### Frida Server Setup

O recurso Frida Setup simplifica a configuração do Frida Server diretamente no dispositivo. O usuário pode selecionar uma versão e tocar em "Start Frida" para que o Beerus cuide automaticamente do download, da instalação e da inicialização do server. Por padrão, o framework oferece as 10 versões mais recentes, e também permite selecionar manualmente uma versão específica. Uma vez instalada, uma versão pode ser reutilizada sem um novo download, tornando mais rápido e conveniente preparar o ambiente de instrumentação dinâmica.

#### FridaChecker.swift (status check, Lines 13…35)

```swift
    /// Checks if frida-server is listening on the given port (synchronous, up to 1s timeout).
    static func isRunning(port: UInt16 = 27042) -> Bool {
        let sockfd = socket(AF_INET, SOCK_STREAM, 0)
        guard sockfd != -1 else { return false }
        defer { close(sockfd) }

        var timeout = timeval(tv_sec: 1, tv_usec: 0)
        setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, socklen_t(MemoryLayout<timeval>.size))

        var addr = sockaddr_in(
            sin_len: UInt8(MemoryLayout<sockaddr_in>.size),
            sin_family: UInt8(AF_INET),
            sin_port: port.bigEndian,
            sin_addr: in_addr(s_addr: inet_addr("127.0.0.1")),
            sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)
        )

        return withUnsafePointer(to: &addr) {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                connect(sockfd, $0, socklen_t(MemoryLayout<sockaddr_in>.size)) == 0
            }
        }
    }

```

#### SetupFridaViewController.swift (start/stop/install, Lines 134…149, 233…234, 252…253)

```swift
            // Stop Frida
            if fridaDaemonExists() {
                RootExec.shellAwait("\(BeerusStrings.launchctlBin) bootout system \(BeerusStrings.fridaDaemonPath)") { _ in
                    DispatchQueue.main.async {
                        self.buttonStart.isEnabled = true
                        self.checkFridaRunning()
                    }
                }
            } else {
                RootExec.shellAwait("killall frida-server") { _ in
                    DispatchQueue.main.async {
                        self.buttonStart.isEnabled = true
                        self.checkFridaRunning()
                    }
                }
            }

```

```swift
                // Copia para o tmp do sistema via daemon
                let copyResult = RootExec.shell("cp '\(fileURL.path)' '\(systemDebPath)'")

```

```swift
                // Instala usando o daemon
                if let response = RootExec.installFrida(from: systemDebPath) {

```

#### Status Check (FridaChecker.swift, Lines 14…35)

- `isRunning(port:)` abre um socket TCP e tenta fazer `connect` direto na porta do frida-server (`127.0.0.1:27042`) com timeout de 1s.
- Retorna `true` se `connect` retornar 0 (porta do frida-server aberta), `false` caso contrário. É o gate usado antes de rodar qualquer script.
- `checkAfterDelay(_:completion:)` (38…44) reverifica após um delay (startup/shutdown) e responde na main thread.
- `notifyStatusChanged()` (9…11) publica `statusDidChangeNotification` para que as telas possam se atualizar.

#### Toggle / Frida Installation (SetupFridaViewController.swift, Lines 116…230)

- `fridaDaemonExists()` (111…114) testa se o plist do serviço existe via `RootExec.shell("test -f …")` — precisa do daemon apenas para ler um caminho de sistema fora do sandbox.
- `ToggleFrida(_:)` (116) decide a ação com base no estado:  
  - **Running** → stop: `launchctl bootout system <plist>` (ou `killall frida-server` se não houver plist).
  - **Installed and stopped** (versão selecionada == versão instalada) → start: `launchctl bootstrap system <plist>`.
  - **New version** → download e instalação (fluxo abaixo).
- **Installation** (a partir de 148): detecta a arquitetura com `dpkg --print-architecture`, monta a URL da release no GitHub, baixa o `.deb` com `Requests.downloadFile` (feito pelo app), copia para o tmp do sistema (`RootExec.shell("cp …")`) e instala via daemon com `RootExec.installFrida(from:)`; erros disparam `Alert.show`.
- Cada branch chama `checkFridaRunning()` ao final para refletir o novo estado.

#### State and Version Check (SetupFridaViewController.swift, Lines 378…412)

- `checkFridaRunning()` monta o texto de status/versão/botão.
- Se `startDownloading`, mostra "Status: Downloading"; caso contrário, roda `RootExec.shell("ps aux | grep frida-server")` (via daemon) para ver se está ativo e ajusta `isRunning`/o label do botão (Start/Stop).
- Consulta a versão com `RootExec.shell("<fridaServerPath> --version")` e a valida com a regex `^\d+\.\d+\.\d+`, preenchendo `versionRunning`/`selectedVersion`.

#### Daemon Bridge — client authentication (BeerusDaemon.c, Lines 429…457)

- Toda chamada `RootExec.*` acima abre o socket UNIX `/var/run/beerus.sock` e envia uma mensagem de texto para o daemon `beerusd`, que roda como **root** (iniciado pelo launchd via `com.beerus.daemon.plist`). O app em sandbox não pode iniciar serviços do launchd, instalar pacotes `.deb` ou inspecionar processos do sistema — então ele delega ao daemon.
- Antes de atender qualquer comando, `verify_client(fd)` valida o cliente: obtém o PID do processo a partir do próprio socket (`getsockopt … LOCAL_PEERPID`), confirma que o caminho do executável termina no binário do Beerus e compara o **CDHash** do cliente com o autorizado (`memcmp(client_hash, g_allowed_cdhash, …)`). Se não bater, responde `error: unauthorized`. Assim, apenas o binário legítimo do Beerus pode emitir comandos como root.

#### Root Execution and Installation (BeerusDaemon.c, Lines 1755…1779, 801…885 e 709…740)

- O que é enviado nos passos acima:  
  - `RootExec.shell("cp …" / "dpkg --print-architecture" / "ps aux | …")` vira a mensagem `SHELL <cmd>`. O handler `SHELL` (1755) resolve a shell (`/bin/sh`, ou `/var/jb/bin/sh` em rootless) e chama `runCommand` (801), que faz `posix_spawn` de `sh -c <cmd>` com **stdout e stderr redirecionados para um pipe** e retorna a saída bruta seguida do trailer `\n\0EXIT:<code>\0`.
  - `RootExec.installFrida(from:)` vira `INSTALL_FRIDA <path>`. O handler (1797) chama `install_frida` (709): para um `.deb`, `install_from_deb` (602) extrai o pacote com `dpkg-deb --extract` e `replace_frida_binary` troca o binário do frida-server no caminho de sistema, respondendo `ok: frida-server installed …` ou um erro.
- No lado Swift, `RootExec.shell` (RootExec.swift 48…118) faz o parse do trailer `\0EXIT:` (102) para separar `output` de `exitCode`.

0:00 

/0:29 

1× 

### Script Editor + Frida Auto Inject

O recurso Frida Auto Inject permite rodar e injetar scripts Frida diretamente do dispositivo usando o Frida Swift, que cuida da integração do Frida com apps escritos em Swift. Por meio dele, o Beerus se comunica com o Frida Server em execução no dispositivo, permitindo controlar sessões de instrumentação e rodar scripts sem depender de um computador externo.  
Todo o processo pode ser realizado inteiramente dentro do Beerus Framework: pelo Script Editor, o usuário inicia o Frida Server e pode então criar ou importar um script para o armazenamento do dispositivo. Depois de adicionar o script, basta editá-lo se necessário, selecionar a aplicação alvo e tocar em "Run". O Beerus então usa o Frida Swift para iniciar a aplicação e instrumentá-la com o script selecionado.

#### ScriptConsoleViewController.swift (Lines 306…341)

```swift
    private func executeOnTarget(pid: UInt) {
        Task {
            do {
                await log("Attaching to PID \(pid)…", systemColor)
                await MainActor.run { setStatus(.connecting) }

                let session = try await FridaManager.shared.beginScript(
                    source: scriptModel.source, pid: pid)
                self.scriptSession = session

                eventTask = Task { [weak self] in
                    guard let self else { return }
                    for await rawJSON in session.rawMessages {
                        await self.handleRawMessage(rawJSON)
                    }
                    await log("Event stream ended.", systemColor)
                    if !self.didReceiveOutput {
                        await log("(script produced no output)", systemColor)
                    }
                    await MainActor.run {
                        self.isRunning = false
                        self.setStatus(.stopped)
                    }
                }

                await MainActor.run {
                    isRunning = true
                    setStatus(.running)
                }
                await log("Script loaded and running.", systemColor)
            } catch {
                await log("Failed: \(error.localizedDescription)", errorColor)
                await MainActor.run { setStatus(.error) }
            }
        }
    }

```

#### FridaManager.swift (Lines 82…108)

```swift
    func beginScript(source: String, pid: UInt) async throws -> ScriptSession {
        device = nil
        let dev = try await getDevice()
        let session = try await attachWithRetry(device: dev, pid: pid)
        let script = try await session.createScript(source)

        let (stream, continuation) = AsyncStream<String>.makeStream()

        let eventTask = Task {
            for await event in script.events {
                guard case .message(let m, _) = event else { continue }
                if let data = try? JSONSerialization.data(withJSONObject: m),
                   let json = String(data: data, encoding: .utf8) {
                    continuation.yield(json)
                }
            }
            continuation.finish()
        }

        try await script.load()

        return ScriptSession(rawMessages: stream) {
            eventTask.cancel()
            try? await script.unload()
            try? await session.detach()
        }
    }

```

#### Pre-check and App Selection (ScriptConsoleViewController.swift, Lines 254…279)

- `startExecution()` verifica se o frida-server está no ar com `FridaChecker.isRunning()`; se não estiver, imprime um erro e aborta. A injeção em si conversa com o frida-server por TCP `localhost:27042` (o frida-server foi previamente instalado e iniciado como root pelo `beerusd` — veja Frida Server Setup).
- Imprime "Starting script: " e "Waiting for app selection…".
- Instancia `AppPickerViewController` e o apresenta; `onSelect` atualiza o label "PID:  — " e chama `executeOnTarget(pid:)`; `onCancel` define `.stopped` e imprime "Cancelled — no app selected.".

#### Attaching to the Process — (ScriptConsoleViewController.swift, Lines 281…287)

- `executeOnTarget(pid:)` imprime "Attaching to PID …" e define o status `.connecting`.
- Chama `FridaManager.shared.beginScript(source:pid:)`.

#### Attach — (FridaManager.swift, Lines 82…86 e 112…128)

- `beginScript` reseta o device e obtém a conexão com `getDevice()`, que faz `deviceManager.addRemoteDevice(address: "localhost")` — o frida-server local.
- Faz o attach com `attachWithRetry(device:pid:)`: até 3 tentativas, timeout de 30s cada (via `withTimeout` em torno de `device.attach(to: pid)`), com espera de 1s entre falhas; lança `FridaError.attachFailed` se esgotar.

#### Script Creation and Loading (FridaManager.swift, Lines 86…107)

- Cria o script com `session.createScript(source)` a partir do código-fonte do editor.
- Cria um `AsyncStream<String>` (um par stream/continuation) para carregar as mensagens.
- Inicia a eventTask: itera sobre `script.events`, filtra `.message`, serializa cada payload para JSON e chama `continuation.yield(json)`; quando termina, `continuation.finish()`.
- Carrega o script no processo alvo com `script.load()`.
- Retorna uma `ScriptSession` com o stream e uma closure de parada (cancela a eventTask, `script.unload()` e `session.detach()`).

#### Event Consumption and the "Running" State (ScriptConsoleViewController.swift, Lines 287…312)

- Armazena a sessão em `self.scriptSession`.
- Inicia a eventTask: consome `session.rawMessages` e, para cada JSON bruto, chama `handleRawMessage(rawJSON)`.
- Quando o stream termina: imprime "Event stream ended."; se nada foi produzido, "(script produced no output)"; define `isRunning = false` e status `.stopped`.
- Após iniciar o consumo: define `isRunning = true`, status `.running` e imprime "Script loaded and running.".
- Em uma exceção de attach/load: imprime "Failed: " e status `.error`.

#### Handling Script Messages (ScriptConsoleViewController.swift, Lines 318…361)

- `handleRawMessage` decodifica o JSON em um dicionário e lê o campo `type`.
- "send": extrai texto/cor via `parseSendPayload` (distingue log/error/result pela cor) e imprime no console.
- "error": imprime "ERROR: " e, se presente, a stack.
- "log": imprime o payload usando uma cor baseada no nível (error → vermelho).
- Toda a saída vai para a UI via `log(...)` → `appendOutput` no MainActor. Define `didReceiveOutput = true`.

#### Session Teardown (ScriptConsoleViewController.swift, Lines 363…379 / FridaManager.swift 102…106)

- `stopExecution()` (via `stopTapped` ou ao sair da tela): se não estiver rodando, retorna; caso contrário, define `isRunning = false`.
- Cancela e limpa a eventTask.
- Chama `session.finish()`, que dispara a closure de parada da `ScriptSession`: cancela a eventTask interna, roda `script.unload()` e `session.detach()`.
- `stopTapped` também define o status `.stopped` e imprime "Stopped by user.".

0:00 

/0:12 

1× 

### IPA Extractor

O recurso IPA Extractor permite extrair o IPA de uma aplicação iOS usando scripts Frida para obter o executável descriptografado depois que ele é carregado na memória, removendo a proteção aplicada pelo FairPlay DRM e produzindo um pacote que pode ser usado para análise estática e trabalho de engenharia reversa.

#### InstalledAppsViewController.swift (dump → IPA orchestration, Lines 111…113, 130…140, 167…171)

```swift
                guard FridaChecker.isRunning() else {
                    throw DumpError.fridaNotRunning
                }

```

```swift
                let bundleInfo = try await FridaManager.shared.dumpExecutable(
                    bundleId: app.bundleIdentifier,
                    pid: app.pid,
                    outputPath: dumpPath
                ) { msg in
                    loadingVC.addLog(msg)
                }

                guard FileManager.default.fileExists(atPath: dumpPath) else {
                    throw DumpError.executableNotDumped
                }

```

```swift
                let ipaURL = try IPABuilder.build(app: appWithPaths, dumpedExecutable: dumpPath) { msg in
                    loadingVC.addLog(msg)
                } progressCallback: { done, total in
                    loadingVC.updateProgress(done, total: total)
                }

```

#### Entry and Listing (IPAExtractorViewController.swift 52…56 / InstalledAppsViewController.swift 44…67)

- `dumpButtonTapped()` empilha `InstalledAppsViewController` na navigation stack.
- `loadApps()` chama `FridaManager.shared.getInstalledApps()` (via frida-server, `enumerateApplications`) para listar os apps; tocar em uma linha revalida a lista e chama `showDumpConfirmation(for:)`.

#### Dump → IPA Flow (InstalledAppsViewController.swift, Lines 101…190)

- `performDump(app:loadingVC:)` roda em uma `Task` e conduz as fases através do `LoadingViewController` (`.checking → .attaching → .dumping → .building → .complete`).
- **checking**: valida `FridaChecker.isRunning()`; se não estiver, lança `DumpError.fridaNotRunning`.
- **dumping**: define `dumpPath = /tmp/beerus_dump_<uuid>` e chama `FridaManager.shared.dumpExecutable(...)`, que faz o attach através do frida-server, injeta `FridaScripts.dumpScript` e extrai o binário descriptografado; encaminha cada `msg` para `loadingVC.addLog`.
- Verifica se o binário foi gravado (`FileManager.fileExists`) e monta um `AppModel` com os caminhos reais do bundle retornados em `BundleInfo`.
- **building**: `IPABuilder.build(app:dumpedExecutable:)` empacota o `.ipa` **localmente** (com callbacks de log e progresso `updateProgress(done,total)`).
- Remove o dump temporário, marca `.complete`, chama `showSuccess(ipaPath:)` e, após \~2s, dispensa a tela e abre a share sheet (`showShareSheet(for:)`). Qualquer erro cai em `loadingVC.showError(error)`.

#### Visual Feedback (LoadingViewController.swift, Lines 228…341)

- `updatePhase(_:)` anima a mudança de fase; `updateProgress(_:total:)` atualiza a barra; `addLog(_:)` acrescenta linhas ao console.
- `showSuccess(ipaPath:)` e `showError(_:)` encerram a tela em um estado de sucesso/erro.

0:00 

/0:21 

1× 

### Memory Dump

O recurso Memory Dump faz localmente o dump da memória de um processo em execução e salva tudo em um arquivo compactado. Esse pacote pode ser baixado para análise posterior.

#### ProcessListViewController.swift (memory-dump orchestration, Lines 101…120)

```swift
                let result = try await FridaManager.shared.dumpMemory(
                    pid: app.pid,
                    outputDir: outputDir
                ) { msg in
                    loadingVC.addLog(msg)
                }

                try Task.checkCancellation()

                loadingVC.updatePhase(.packaging)

                // Zip only the single dump.bin + index.json (fast — just 2 files)
                let zipPath = NSTemporaryDirectory() + "\(safeName)_memdump.zip"
                try? FileManager.default.removeItem(atPath: zipPath)

                let zipURL = URL(fileURLWithPath: zipPath)
                try ZipArchive.create(
                    at: zipURL,
                    from: URL(fileURLWithPath: outputDir)
                )

```

#### Entry and Listing (MemoryDumpViewController.swift 54…56 / ProcessListViewController.swift 43…63)

- `dumpButtonTapped()` empilha `ProcessListViewController`.
- `loadRunningApps()` chama `FridaManager.shared.getInstalledApps()` e mantém apenas os que estão em execução; na seleção, `startDump(for:)` cria `MemoryDumpLoadingViewController` e chama `performMemoryDump`.

#### Memory Dump Flow (ProcessListViewController.swift, Lines 81…155)

- `performMemoryDump(app:loadingVC:)` roda em um `dumpTask` cancelável e conduz as fases (`.connecting → .attaching → .dumping → .packaging → .complete`).
- **connecting**: valida `FridaChecker.isRunning()` (caso contrário `DumpError.fridaNotRunning`).
- **dumping**: monta `outputDir` no tmp (`beerus_memdump_<name>_<timestamp>`), checa cancelamento e chama `FridaManager.shared.dumpMemory(pid:outputDir:)`, que faz o attach através do frida-server e roda `FridaScripts.memoryDumpScript`; internamente usa `executeScriptWithProgress`, cujo idle timeout de 120s **reinicia a cada** mensagem de log/progresso (um dump longo não é confundido com um travamento).
- **packaging**: comprime `dump.bin` \+ `index.json` em um `.zip` com `ZipArchive.create(at:from:)` (compressão local via `Compression`/`zlib`, **não** o daemon) e remove o diretório bruto.
- **complete**: `showSuccess(message:)` com a contagem/tamanho/tempo das regiões e, após \~2s, dispensa + share (`showShareSheet(for: zipURL)`). `CancellationError` fecha sem erro; outros erros vão para `showError(error)`.

0:00 

/1:28 

1× 

### LLDB Server

O recurso LLDB Server permite selecionar uma aplicação iOS e iniciar o debugserver anexado ao seu processo na porta "1234", habilitando uma conexão remota via LLDB para realizar análise dinâmica, como definir breakpoints, inspecionar memória e registradores e acompanhar a execução da aplicação.

#### LLDBServerViewController.swift (debugserver attach, Lines 282…310)

```swift
            _ = RootExec.shell("killall -9 debugserver 2>/dev/null")
            try? await Task.sleep(nanoseconds: 300_000_000)

            // Clear old log
            _ = RootExec.shell("> \(self.logFile)")

            await self.setProgress(0.4, "attaching to \(process.name) (\(process.pid))...")

            // Launch debugserver attached to the target PID, log output to file
            let launch = "( \(path) 0.0.0.0:\(self.port) --attach=\(process.pid) "
                       + "> \(self.logFile) 2>&1 & )"
            _ = RootExec.shell(launch)

            await self.setProgress(0.7, "verifying...")

            // Poll for the server process
            var serverPID = ""
            for i in 0..<6 {
                try? await Task.sleep(nanoseconds: 500_000_000)
                let check = RootExec.shell(
                    "ps -eo pid,comm 2>/dev/null | grep debugserver | grep -v grep | head -1 | sed 's/^[[:space:]]*//' | cut -d' ' -f1"
                )
                let p = check.output.trimmingCharacters(in: .whitespacesAndNewlines)
                if !p.isEmpty {
                    serverPID = p
                    break
                }
                await self.setProgress(0.7 + Float(i + 1) * 0.04, "verifying...")
            }

```

#### State Detection (LLDBServerViewController.swift, Lines 141…187)

- `refreshState()` verifica se o daemon está no ar (`RootExec.isRunning`, ou seja, `PING`→`PONG`); se não, marca `.notInstalled`.
- Via daemon, roda um script que resolve o caminho do `debugserver` (`which`) e procura o PID em execução (`ps … grep debugserver`), aplicando `.online`/`.offline`/`.notInstalled`.
- `actionTapped()` (189) despacha: install / start (process picker) / stop dependendo do estado atual.

#### Installation (LLDBServerViewController.swift, Lines 198…235)

- `install()` exige que o daemon esteja no ar, checa `apt-get`/`apt-cache show debugserver` e instala com `apt-get install -y debugserver` via `RootExec.shell` — instalar pacotes só é possível como root, daí o daemon.

#### Attaching to the Process (LLDBServerViewController.swift, Lines 237…311)

- `showProcessPicker()` lista processos (`RootExec.shell("ps -eo pid,comm")`) e, na seleção, chama `attachToProcess(_:)`.
- `attachToProcess(_:)` roda em uma `Task.detached`: mata o `debugserver` anterior (`killall -9`), limpa o log e **lança o debugserver** anexado ao alvo: `<path> 0.0.0.0:<port> --attach=<pid> > log 2>&1 &`.
- Cada `RootExec.shell` aqui é indispensável porque o app não pode matar outros processos, escrever no `logFile` do sistema ou anexar o `debugserver` a outro PID — tudo requer root.
- Faz polling (até \~6×500ms) com `ps … grep debugserver` para obter o `serverPID`; em caso de sucesso aplica `.online` e inicia `startLogStreaming()`; caso contrário `.offline`.

#### Stop and Log (LLDBServerViewController.swift, Lines 313…374)

- `stopServer()` roda `killall -9 debugserver` (via daemon) e retorna a `.offline`.
- `startLogStreaming()` faz polling do log via daemon (`tail -c +<offset> <logFile>`, lendo apenas o que é novo) enquanto o processo estiver vivo, atualizando o console; quando detecta a saída, acrescenta "\[debugserver exited\]".

#### Daemon Bridge (BeerusDaemon.c, Lines 1755…1779 e 801…885)

- Todo `RootExec.shell(...)` nesta tela envia `SHELL <cmd>`. O handler `SHELL` (1755) roda `sh -c <cmd>` via `runCommand`/`posix_spawn` (801) como **root**, com stdout+stderr no pipe, e retorna a saída + o trailer `\n\0EXIT:<code>\0`.
- O que é passado é a linha de comando; o que volta é `output` \+ `exitCode` — usado, por exemplo, para extrair o `serverPID` do `ps` ou o novo trecho de log do `tail`.

0:00 

/3:13 

1× 

### Proxy Profiles

O recurso Proxy Profiles permite criar diferentes perfis de proxy e habilitá-los ou desabilitá-los a qualquer momento, sem precisar configurar ou remover o proxy manualmente ao trocar de rede ou usar os apps normalmente. Para criar um perfil, basta informar um nome e o endereço IP com uma porta. Depois disso, você pode habilitar/desabilitar, editar ou excluir o perfil sempre que precisar.

#### ProxyProfilesViewController.swift (apply/toggle proxy, Lines 133…149)

```swift
    private func handleSwitchChange(_ sender: UISwitch) {
        let index = sender.tag
        guard profiles.indices.contains(index) else { return }

        let profile = profiles[index]
        let turningOn = sender.isOn
        let result = RootExec.setProxy(turningOn ? profile.proxy : "OFF")

        guard result.exitCode == 0 else {
            sender.setOn(!turningOn, animated: true)
            self.showAlert(title: "Erro", message: "Não foi possível aplicar o proxy.")
            return
        }

        storage.setProfileEnabled(named: profile.name, enabled: turningOn)
        reloadProfiles()
    }

```

#### Daemon Bridge — SET\_PROXY (RootExec.swift 201…283 / BeerusDaemon.c 1571…1712)

- `setProxy(_:)` (RootExec.swift 201) abre o socket e envia `SET_PROXY <value>`, onde `<value>` é `host:port` (ex.: `127.0.0.1:8083`) ou `OFF`.
- No daemon, o handler `SET_PROXY` (1571) faz o que o sandbox não pode: lê e reescreve `/var/preferences/SystemConfiguration/preferences.plist` (a configuração de rede do sistema) via `CFPropertyList`. Localiza o *set*/serviço ativo e o dicionário `Proxies`; para `host:port` valida a porta e escreve `HTTPEnable=1`, `HTTPProxy`, `HTTPPort` e os equivalentes `HTTPS*` (1699); para `OFF` remove essas chaves (1662).
- **Passado:** a string do proxy. **Esperado de volta:** `ok: proxy set to <ip>:<port>` / `ok: proxy disabled`, ou um erro (formato/porta inválidos, falha ao ler as preferences).

#### Proxy Actions (ProxyProfilesViewController.swift, Lines 7…160)

- `proxyOff(_:)` (7): `RootExec.setProxy("OFF")` \+ `storage.disableAllProfiles()`.
- `proxyTest(_:)` (13): aplica um proxy de teste fixo `127.0.0.1:8083`.
- `addProfileTapped(_:)` (17): coleta nome/proxy e persiste via `storage.addProfile(name:proxy:)`.
- `handleSwitchChange(_:)` (133): on → `setProxy(profile.proxy)`; off → `setProxy("OFF")`; em ambos atualiza o estado no storage e recarrega.
- `handleDeleteProfile(_:)` (150): se ativo, desliga o proxy antes de `storage.deleteProfile(named:)`.

#### Persistence (ProxyProfilesStorage.swift, Lines 32…98)

- `ProxyProfile` é `Codable` (name, proxy, isEnabled), salvo em um arquivo JSON dentro do sandbox (`fileURL`).
- `fetchProfiles()`/`saveProfiles(_:)` leem e escrevem a lista; `addProfile`, `deleteProfile`, `setProfileEnabled` e `disableAllProfiles` a mutam e a reescrevem.

0:00 

/0:21 

1× 

### Terminal

O recurso Terminal fornece um terminal integrado ao Beerus Framework, permitindo executar comandos diretamente no dispositivo com privilégios de root através do Daemon do framework.

#### TerminalViewController.swift (running root commands, Lines 236…266)

```swift
        // Check daemon
        guard RootExec.isRunning else {
            appendOutput("error: beerus daemon is not running\n",
                         color: UIColor(red: 1, green: 0.3, blue: 0.3, alpha: 1))
            return
        }

        isExecuting = true
        sendButton.isEnabled = false
        promptLabel.text = "..."

        Task.detached { [weak self] in
            let result = RootExec.shell(cmd)
            await MainActor.run {
                guard let self else { return }
                self.isExecuting = false
                self.sendButton.isEnabled = true
                self.promptLabel.text = "root#"

                if !result.output.isEmpty {
                    self.appendOutput(result.output + "\n", color: .white)
                }

                if result.exitCode != 0 {
                    self.appendOutput(
                        "exit: \(result.exitCode)\n",
                        color: UIColor(red: 1, green: 0.3, blue: 0.3, alpha: 1)
                    )
                }
            }
        }

```

#### Command Execution (TerminalViewController.swift, Lines 205…267)

- `executeCurrentInput()` (também chamado por `sendTapped`/Return) lê e limpa o input, o armazena no histórico e ecoa `root# <cmd>`.
- Trata built-ins locais: `help`, `clear`, `history` (sem ir ao daemon).
- O daemon entra **apenas depois** de descartar os built-ins e confirmar, via `RootExec.isRunning` (`PING`), que o `beerusd` está no ar; caso contrário, imprime um erro.
- Roda o comando em uma `Task.detached` via `RootExec.shell(cmd)`, que envia `SHELL <cmd>`; o daemon faz `posix_spawn` de `sh -c <cmd>` como root com stdout/stderr capturados e retorna a saída + código. É indispensável porque todo o propósito da tela é rodar comandos **como root**, além do alcance do processo do app.
- `printWelcome()` (166) e `printHelp()` (269) apenas escrevem texto no console; `appendOutput` (309) acrescenta com timestamp/cor.

0:00 

/0:11 

1× 

### Plist Reader

O recurso Plist Reader permite que os usuários listem e leiam arquivos .plist associados à aplicação selecionada diretamente pela interface do Beerus Framework, eliminando a necessidade de acessar manualmente o dispositivo por um terminal. Essa funcionalidade simplifica a análise de arquivos de configuração da aplicação e de outras informações armazenadas nesse formato.

#### AppManager.swift (discovering apps and their .plist files, Lines 51…57, 80…88)

```swift
            let name = appFolder.components(separatedBy: ".app").first ?? ""
            let appPath = "\(uuidPath)/\(appFolder)"
            let plistPath = "\(appPath)/Info.plist"

            guard let plist = NSDictionary(contentsOfFile: plistPath) as? [String: Any] else { continue }

            guard let bundleId = plist["CFBundleIdentifier"] as? String else { continue }

```

```swift
            var plistFiles: [String] = []

            if let fileEnum = fileManager.enumerator(atPath: appPath) {
                for case let File as String in fileEnum {
                    if File.hasSuffix(".plist") {
                        plistFiles.append(appPath+"/"+File)
                    }
                }
            }

```

#### FileBottomSheet.swift (reading a .plist, Lines 29…38)

```swift
    func readPlist() -> [(key: String, value: String)] {
        var plistsValues: [String: String] = [:]
        if let dict = NSDictionary(contentsOfFile: fileText) as? [String: Any] {
           for (key, value) in dict {
               plistsValues["\(key)"] = "\(value)"
           }
        }

        return plistsValues.sorted { $0.key < $1.key }
    }

```

#### App Discovery (AppManager.swift, Lines 34…98)

- `getApps()` varre `/private/var/containers/Bundle/Application/`, entra em cada UUID, encontra a pasta `.app` e lê o `Info.plist` (`NSDictionary(contentsOfFile:)`) para obter `CFBundleIdentifier`, nome e ícone.
- Coleta a lista de todos os arquivos `.plist` do bundle (`plistFiles`) e resolve o `dataContainerURL` via `LSApplicationWorkspace`, montando um `AppInfo` por bundle id.
- `PlistReaderViewController.viewDidLoad` (56…60) chama `appmanager.getApps()` e popula a tabela.

#### Opening and Reading a Plist (PlistReaderViewController 119…166 / FileBottomSheet.swift 30…39)

- Selecionar um app (`didSelectRowAt`/`openPlistsMenu`) abre `BottomSheetViewController(plists:)` com a lista de `.plist` daquele app.
- Ao escolher um arquivo, `FileBottomSheet.readPlist()` carrega o `.plist` com `NSDictionary(contentsOfFile:)`, achata cada entrada em `key → value` e retorna os pares ordenados por chave (`[(key, value)]`) para exibição na tabela.

0:00 

/0:22 

1× 

### APP Store

O recurso App Store integra o Beerus Framework à loja de aplicativos, permitindo selecionar e instalar versões mais antigas de aplicações diretamente da App Store, simplificando o processo de downgrade para testar e analisar diferentes versões.

#### AppStoreService.swift (login / download, Lines 168…175, 190…195, 524…565)

```swift
    func login(email: String, password: String, authCode: String = "") async throws -> AppStoreAccount {
        let guid = getGUID()
        var endpoint = try await fetchBag()

        // ponytail: ipatool requires trailing slash for /native/ auth endpoints
        if endpoint.contains("/native/") && !endpoint.hasSuffix("/") {
            endpoint += "/"
        }

```

```swift
            let payload = PlistPayload.buildLoginPayload(
                email: email, password: password,
                authCode: authCode, guid: guid, attempt: attempt
            )
            // ponytail: login uses form-urlencoded body, not plist
            let body = PlistPayload.encodeFormData(payload)

```

```swift
    func download(app: AppStoreApp, externalVersionID: String? = nil,
                  progress: ((Int64, Int64) -> Void)? = nil) async throws -> DownloadResult {
        let account = try accountInfo()
        let guid = getGUID()

        let item = try await fetchDownloadItem(
            account: account, appID: app.id, guid: guid,
            externalVersionID: externalVersionID
        )

        let version: String = {
            if let v = item.metadata["bundleShortVersionString"] { return "\(v)" }
            return "unknown"
        }()

        // ponytail: sanitize filename - remove chars that break shell commands
        let safeBundleID = app.bundleID.replacingOccurrences(of: "'", with: "")
        let safeVersion = version
            .replacingOccurrences(of: "'", with: "")
            .replacingOccurrences(of: " ", with: "_")
            .replacingOccurrences(of: "/", with: "-")
        let fileName = "\(safeBundleID)_\(app.id)_\(safeVersion).ipa"
        let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let destination = docs.appendingPathComponent(fileName).path

        // Unique per invocation so two concurrent downloads of the same bundleID/version don't race
        // on the same working file.
        let tmpPath = destination + ".\(UUID().uuidString.prefix(8)).tmp"
        try await downloadFile(from: item.url, to: tmpPath, progress: progress)

        try IPAProcessor.applyPatches(
            metadata: item.metadata,
            account: account,
            sinfs: item.sinfs,
            sourcePath: tmpPath,
            destinationPath: destination
        )

        try? FileManager.default.removeItem(atPath: tmpPath)

        return DownloadResult(destinationPath: destination, sinfs: item.sinfs)
    }

```

#### Authentication (AppStoreService.swift, Lines 117…345)

- `fetchBag()` (117) resolve o endpoint de autenticação da Apple (com suporte a `/native/`).
- `login(email:password:authCode:)` (168) obtém o GUID do dispositivo (`getGUID`), monta o payload com `PlistPayload.buildLoginPayload` (form-urlencoded) e tenta novamente até 4 vezes.
- Trata redirects, o código `-5000` (credenciais inválidas), 2FA (quando `authCode` está ausente) e extrai `token` \+ `dsid`, retornando/armazenando um `AppStoreAccount`.
- `accountInfo()` (346) lê a conta armazenada; `revoke()` (355) a exclui (quando o token expira).

#### Search (AppStoreService.swift, Lines 361…450)

- `search(term:limit:)` resolve o código do país (a partir do storeFront) e consulta `https://<iTunesDomain>/search`, mapeando o JSON para `[AppStoreApp]`.
- `lookup(bundleID:)` (413) é a variante por bundle-id.

#### Purchase and Download (AppStoreService.swift 451…560 / DownloadViewController.swift 132…180)

- `purchase(app:)` (451) realiza a "compra" gratuita (STDQ) necessária para apps ainda não associados à conta.
- `download(app:externalVersionID:progress:)` (524): busca o item (`fetchDownloadItem`), baixa o `.ipa` bruto para `<dest>.tmp` com `downloadFile` (callback de progresso) e aplica os patches com `IPAProcessor.applyPatches`, produzindo o `.ipa` final.
- `DownloadViewController.startDownload()` conduz os passos na UI: `purchase` (tolera "already purchased") → `download` (barra/label em MB/%) → passos de patch → armazena `downloadedPath` e habilita Share/Files.
- `handleTokenExpired()` (222) chama `revoke()` e pede um novo login; `retryTapped()` reinicia o fluxo.

#### Final Packaging (IPAProcessor.swift, Lines 5…70)

- `applyPatches(metadata:account:sinfs:sourcePath:destinationPath:)` reescreve o zip do `.ipa` **localmente**: injeta o `iTunesMetadata.plist` e, para cada caminho listado nos `SinfPaths` do `Manifest.plist`, escreve o `.sinf` correspondente (validando que as contagens batem), produzindo o `.ipa` assinado para a conta.

0:00 

/0:30 

1× 

### JB Bypass

O recurso Jailbreak Bypass permite contornar os mecanismos de detecção de Jailbreak implementados por certas aplicações, permitindo que elas rodem em dispositivos com Jailbreak quando as verificações usadas pelo app são compatíveis com o método de bypass.

#### SpawnGateService.swift (spawn injection — "Frida Auto Inject", Lines 21…44, 90…114)

```swift
    func start() async throws {
        guard !isRunning else { return }

        log("Starting spawn gating...")

        do {
            device = try await deviceManager.addRemoteDevice(address: "localhost")
            guard let device else { throw SpawnGateError.deviceNotAvailable }

            try await device.enableSpawnGating()
            isRunning = true
            onStatusChange?(true)
            log("Spawn gating enabled")

            gatingTask = Task { [weak self] in
                await self?.pollPendingSpawns()
            }
        } catch {
            isRunning = false
            onStatusChange?(false)
            log("Failed to enable spawn gating: \(error.localizedDescription)")
            throw error
        }
    }

```

```swift
    private func injectBypassAndResume(device: Device, spawn: SpawnDetails) async {
        let pid = spawn.pid
        let identifier = spawn.identifier ?? "pid:\(pid)"

        log("Spawn: \(identifier)")

        do {
            // Timeout: 5 seconds max for entire injection
            try await withTimeout(5) { [self] in
                let session = try await device.attach(to: pid)
                let script = try await session.createScript(JBBypassScript.source)
                try await script.load()
                self.log("Injected: \(identifier)")
            }
        } catch {
            log("Injection failed for \(identifier): \(error.localizedDescription)")
        }

        // ALWAYS resume - fail-open pattern
        do {
            try await device.resume(pid)
        } catch {
            log("Resume failed for \(identifier): \(error.localizedDescription)")
        }
    }

```

#### Spawn Gating + Auto Inject (SpawnGateService.swift, Lines 21…125)

- `start()` conecta ao **frida-server** (`localhost`), habilita `enableSpawnGating()` (novos apps sobem pausados) e dispara `gatingTask` com o loop de polling.
- `pollPendingSpawns()` chama `enumeratePendingSpawn()` continuamente e, para cada spawn pendente, chama `injectBypassAndResume`.
- `injectBypassAndResume(device:spawn:)`: com um timeout de 5s, faz `device.attach(to: pid)`, cria o script com `JBBypassScript.source` e `script.load()`; **sempre** chama `device.resume(pid)` no final (fail-open — não trava o app se a injeção falhar).
- `stop()` (46) cancela o loop e chama `disableSpawnGating()`.
- `JBBypassScript.source` é o JavaScript do Frida que intercepta as verificações de Jailbreak (ex.: `Interceptor.attach` em `canOpenURL`, checagens de arquivo/caminho).

#### Shadow Installation (ShadowService.swift, Lines 44…170)

- `isInstalled` verifica a presença de `Shadow.dylib` nos caminhos do MobileSubstrate (rootless e rootful) — uma leitura local.
- `fetchReleases(completion:)` lista as releases `.deb` do Shadow no GitHub; `selectRelease(_:)` escolhe a versão. O app baixa o `.deb` para `/var/tmp/shadow.deb`.
- `install(...)`/`uninstall(...)` **dependem do daemon**: chamam `RootExec.exec("<dpkg> -i '/var/tmp/shadow.deb' 2>&1")` (e `dpkg -r <packageId>` para remover) — instalar/remover pacotes requer root.
- `enableBypass(for:)`/`disableBypass(for:)` → `modifyAppConfig`: montam o plist de config do Shadow em memória, o escrevem em um arquivo temporário no sandbox e usam o daemon para copiá-lo para o local do sistema com as permissões corretas: `RootExec.exec("cp '<tmp>' '/var/mobile/Library/Preferences/me.jjolano.shadow.plist' && chmod 644 … && chown mobile:mobile …")`. O app não pode escrever nesse caminho por conta própria.

#### Daemon Bridge — EXEC (RootExec.swift 11 / BeerusDaemon.c 1781…1787 e 520…585)

- `RootExec.exec(_:)` envia `EXEC <cmd>`. No daemon, o handler `EXEC` (1781) chama `run_shell_capture` (520), que roda o comando como **root** e captura a saída em um buffer.
- **Passado:** a linha de comando (`dpkg -i`, `cp`, `killall -9 SpringBoard` de `respring()`). **Esperado de volta:** a saída/status; o Shadow trata como sucesso quando o retorno é non-nil.

#### Module UI (JailbreakBypassViewController.swift, Lines 293…402)

- `installTapped()` → `performInstall()` chama `service.install`, mostra o progresso e atualiza a lista; `confirmUninstall()`/`performUninstall()` fazem o inverso.
- `loadInstalledApps()` varre os bundles instalados para montar a lista de apps.
- `toggleApp(at:)` habilita/desabilita o bypass para um app específico via `service.enableBypass`/`disableBypass` em background e atualiza a célula.
- `respringTapped()`/`showRespringPrompt()` fazem o respring após instalar/remover.

0:00 

/0:14 

1× 

## Conclusão

O **Beerus Framework** está disponível para download e build diretamente do [repositório oficial da **Hakai Offensive Security**](https://github.com/hakaioffsec/beerus-ios?ref=yokai.hakaisecurity.io), reunindo em uma única ferramenta uma variedade de recursos voltados para análise de segurança e pentesting de aplicações móveis.  
O projeto segue evoluindo, com novos recursos, automações e integrações sendo desenvolvidos para tornar cada vez mais simples o trabalho de pesquisadores e profissionais de segurança. Além disso, o Beerus é um projeto aberto à comunidade: contribuições, issues, pull requests e sugestões de novos recursos são sempre bem-vindas. Ainda há muito mais por vir. Até breve com mais novidades sobre o Beerus Framework! 0w0

## Referências

- [Frida](https://github.com/frida/frida?ref=yokai.hakaisecurity.io) – Dynamic instrumentation toolkit for developers, reverse engineers, and security researchers.
- [Frida Swift](https://github.com/frida/frida-swift?ref=yokai.hakaisecurity.io) – Lets you control Frida directly from an app written in Swift.
- [Palera1n](https://palera.in/?ref=yokai.hakaisecurity.io) \- Jailbreak for iPhone, iPad, Macbooks, and AppleTV's for versions 15 and higher
- [Beerus Framework - iOS ](https://github.com/hakaioffsec/beerus-ios?ref=yokai.hakaisecurity.io)\- Repositório para baixar o framework Beerus para iOS