Segurança

Frida Parte 6: Utilizando Writers | 8kSec

O post aborda o uso avançado da ferramenta Frida, focando na utilização de Writers para diferentes arquiteturas de CPU. O exemplo prático demonstra como usar Arm64Writer em iOS para contornar a proteção anti-debug de um aplicativo que verifica se está sendo depurado e encerra caso positivo.

Compartilhar
Frida Part 6: Utilizing Writers | 8kSec

Introdução

Bem-vindo a mais um post do blog em nossa série sobre Uso Avançado do Frida. Já abordamos muitas funcionalidades, mas ainda não mencionamos uma que é realmente importante: o writer. O Frida suporta vários writers diferentes para arquiteturas de CPU distintas, como X86Writer para x86 e Arm64Writer para AArch64.

Os writers são utilizados dentro do método Memory.patchCode, pois o assembly que escrevemos pode acabar em uma localização temporária e posteriormente será remapeado para a endereço de destino.

Neste tutorial, vamos abordar o Arm64Writer no iOS. Para criar um writer, simplesmente especificamos o endereço onde o assembly será escrito (nosso por Memory.patchCode) junto com o valor do PC (registrador Program Counter) para onde realmente queremos que seja escrito.

Um exemplo de como utilizamos Memory.patchCode com Arm64Writer é dado abaixo:

// Onde queremos fazer o patch ou escrever assembly var location = ptr("0xdeadbeef"); // Memory.patchCode(addressWhereToReallyWrite, sizeOfBuffer, apply) Memory.patchCode(location, 4, code => {     // Cria um novo writer na localização fornecida por Memory.patchCode     // e queremos que seja escrito nessa localização (segundo argumento)     const writer = new Arm64Writer(code, { pc: location });     // Escreve a instrução NOP lá     writer.putNop(); }

Os writers fornecem muitas funções que podemos usar, como putNop() para escrever a instrução NOP ou putRet() para escrever a instrução RET.

Agora que vimos o que é um writer e como usá-lo com Memory.patchCode, vamos passar para um exemplo prático onde bypassaremos uma aplicação simples que sai quando detecta o debugger (proteção anti-debug).

Análise

Vamos usar a aplicação simples que nos permite verificar se o debugger está anexado verificando o ID do Processo Pai (PPID). Se o processo foi iniciado pelo debugger, o PPID será algum número diferente de 1. Caso o debugger esteja anexado, a aplicação sai com código de status 1.

import SwiftUI
struct ContentView: View {
    @State private var attached = false
    var body: some View {
        VStack {
                Button("Verificar status do depurador") {
                    attached = isDebuggerAttached()
                }
                .alert("Depurador detectado", isPresented: $attached) {
                    Button("OK") {
                        print("Depurador detectado! Saindo...")
                        exit(1)
                    }
                }
        }
        .padding()
    }
    func isDebuggerAttached() -> Bool {
        if (debugged() == 1) {
            return true
        }
        return false
    }
}

Tela principal do aplicativo iOS mostrando o botão Verificar status do depurador

Caixa de diálogo de alerta do aplicativo exibindo a mensagem Depurador detectado que aciona a saída

Aqui está a implementação da função que detecta depuradores.

int debugged(void)
{
    int mib[4];
    struct kinfo_proc info;
    size_t info_size = sizeof(info);
    info.kp_proc.p_flag = 0;
    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC;
    mib[2] = KERN_PROC_PID;
    mib[3] = getpid();
    sysctl(mib, 4, &info, &info_size, NULL, 0);
    if ((info.kp_proc.p_flag & P_TRACED) != 0)
        return 1;
    else if(getppid() != 1)
        return 1;
    else
        return 0;
}

A primeira coisa que podemos fazer aqui é verificar as XREFs (Cross-References) para o debug. Uma vez que carregamos o binário dentro do Hopper, podemos procurar por ele e examinar suas XREFs.

Desmontador Hopper mostrando única XREF para a função debugged no binário

Podemos ver que temos apenas uma XREF e clicaremos em Ir para examiná-la.

Desmontagem do Hopper na XREF mostrando instrução branch a ser substituída por NOP para contornar

Podemos ver que temos a instrução b após o str, é essa que queremos substituir pela instrução NOP para definir w8 como 0x0.

Nós poderíamos abordar isso substituindo a implementação de _debugged, mas, para demonstrar os escritores, usaremos-os.

O plano para fazer isso é o seguinte:

  • Adicione 0x49c8 à base do módulo principal (precisamos fazer isso devido ao ASLR e a localização mapeada não será a mesma que na visualização desmontada)
  • Escreva Memory.patchCode nesta localização e, em vez da instrução b, substituí-la-emos por nop usando Arm64Writer
var eight8ksec = Process.getModuleByName("eight8ksec");
var baseAddress = eight8ksec.base;
var addr = ptr("0x49c8").add(baseAddress);
Memory.patchCode(addr, 4, code => {
    const writer = new Arm64Writer(code, { pc: addr });
    writer.putNop();
});

Depois de iniciar o aplicativo, anexaremos a ele usando debugserver.

/usr/local/bin/debugserver 127.0.0.1:6666 -a 1909

Agora precisamos criar um proxy na porta 6666 com iproxy e então podemos conectar a ele usando lldb e começar a depuração.

proxy 6666 6666

Uma vez que fizemos isso, iniciamos o lldb e entramos no comando para conectar ao servidor remoto debugserver.

process connect connect://IPADDRESS:PORT que neste caso é process connect connect://127.0.0.1:6666

terminal lldb conectando-se ao servidor de debug remoto via comando process connect

saída do lldb mostrando a execução pulada para retornar false após o patch NOP bypassa anti-debug

Pode-se ver que fizemos uma pausa em return false e o processo não está “debugado” então nosso processo não sairá e conseguimos bypassar a técnica anti-debug com sucesso.

Exemplo de código

Agora rapidamente passaremos por uma das utilizações dos writers para criar shellcode. O script completo pode ser encontrado (aqui). Vamos cobrir algumas linhas do script, a explicação completa estenderia muito este post. A shellcode é um shell TCP reverso.

var impl = Memory.alloc(Process.pageSize);
Memory.patchCode(impl, Process.pageSize, function (code) {
  var arm64Writer = new Arm64Writer(code, { pc: impl });
  // SUB             SP, SP, #0x50
  arm64Writer.putSubRegRegImm('sp', 'sp', 0x50);
  // STP             X29, X30, [SP, #0x40]
  arm64Writer.putStpRegRegRegOffset('x29', 'x30', 'sp', 0x40, 'pre-adjust');
  // ADD             X29, SP, #0x40
  arm64Writer.putAddRegRegImm('x29', 'sp', 0x40);
  // STR             X0, [SP, #0x18]
  arm64Writer.putStrRegRegOffset('x0', 'sp', 0x18); 
  // MOV             W0, #2
  arm64Writer.putInstruction(0x52800040);
  // MOV             W1, #1
  arm64Writer.putInstruction(0x52800021);
  // MOV             W2, WZR
  arm64Writer.putInstruction(0x2A1F03E2);
  arm64Writer.putCallAddressWithArguments(Module.findExportByName('libc.so', 'socket'), ['w0', 'w1', 'w2']);
// ...

Primeiro, vemos que o script começa alocando Process.pageSize bytes e este é o lugar onde a shellcode será escrita. Seguindo isso vem a chamada de Memory.patchCode na memória previamente alocada.

Começa com o prologue padrão de função AArch64, escrevendo sub sp, sp, #0x50. Isso é feito chamando a função putSubRegRegImm, que coloca a instrução sub usando o segundo argumento como register e o terceiro argumento como imediato, sendo armazenado no primeiro argumento em nosso caso sp.

A seguir, chama-se a função putStpRegRegRegOffset, que coloca a instrução stp para armazenar um par de registradores.

O próximo é a chamada à função putAddRegRegImm, que coloca a instrução add que armazena o resultado da adição entre sp e 0x40 no registrador x29.

A próxima é a chamada à função putStrRegRegOffset, que armazena o valor na localização de sp + 0x18 dentro do registrador x0.

As próximas três chamadas de função colocam a instrução em hexadecimal. A próxima imagem confirma que temos 0x52800040 como a instrução mov w0, #2.

Codificação da instrução ARM64 MOV confirmando 0x52800040 como mov w0, #2

E a última é a chamada à função putCallAddressWithArguments, que coloca a instrução call com base no endereço da função deModule.findExportByName seguida pelos tipos de argumentos aceitos pela função.

Isto marca o fim do post sobre writers, eles são uma ótima característica do Frida e não muito foi escrito sobre eles.

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa