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


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.

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

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.patchCodenesta localização e, em vez da instruçãob, substituí-la-emos pornopusandoArm64Writer
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


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.

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.

