MASTG-KNOW-0085: Detecção de Anti-Debugging
Explorar aplicações usando um debugger é uma técnica muito poderosa durante o processo de engenharia reversa. Você não só pode rastrear variáveis que contêm dados sensíveis e modificar o fluxo de controle da aplicação, mas também ler e modificar memória e registradores.
Existem várias técnicas anti-debugging aplicáveis ao iOS que podem ser categorizadas como preventivas ou reativas. Quando distribuídas adequadamente ao longo do app, essas técnicas atuam como uma medida de suporte para aumentar a resiliência geral.
- Técnicas preventivas atuam como uma primeira linha de defesa para impedir que o debugger se anexe à aplicação.
- Técnicas reativas permitem que a aplicação detecte a presença de um debugger e tenha a chance de divergir do comportamento normal.
Usando ptrace¶
Como visto em Depuração, o kernel XNU do iOS implementa uma chamada de sistema ptrace que carece da maioria das funcionalidades necessárias para depurar adequadamente um processo (por exemplo, permite anexar/executar passo a passo, mas não ler/escrever memória e registradores).
No entanto, a implementação do iOS da syscall ptrace contém um recurso não padrão e muito útil: impedir a depuração de processos. Esse recurso é implementado como a requisição PT_DENY_ATTACH, conforme descrito no Manual Oficial de Chamadas de Sistema BSD. Em poucas palavras, ele garante que nenhum outro debugger possa se anexar ao processo chamador; se um debugger tentar se anexar, o processo será encerrado. Usar PT_DENY_ATTACH é uma técnica anti-debugging bastante conhecida, então você pode encontrá-la com frequência durante pentests de iOS.
Antes de mergulhar nos detalhes, é importante saber que
ptracenão faz parte da API pública do iOS. APIs não públicas são proibidas, e a App Store pode rejeitar apps que as incluam. Por causa disso,ptracenão é chamado diretamente no código; ele é chamado quando um ponteiro de funçãoptraceé obtido viadlsym.
A seguir, um exemplo de implementação da lógica acima:
#import <dlfcn.h>
#import <sys/types.h>
#import <stdio.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
void anti_debug() {
ptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(RTLD_SELF, "ptrace");
ptrace_ptr(31, 0, 0, 0); // PTRACE_DENY_ATTACH = 31
}
Contornar: Para demonstrar como contornar essa técnica, usaremos um exemplo de um binário desmontado que implementa essa abordagem:

Vamos detalhar o que está acontecendo no binário. dlsym é chamado com ptrace como segundo argumento (registrador R1). O valor de retorno no registrador R0 é movido para o registrador R6 no offset 0x1908A. No offset 0x19098, o valor do ponteiro no registrador R6 é chamado usando a instrução BLX R6. Para desativar a chamada ptrace, precisamos substituir a instrução BLX R6 (0xB0 0x47 em Little Endian) pela instrução NOP (0x00 0xBF em Little Endian). Após a modificação, o código será semelhante ao seguinte:

Armconverter.com é uma ferramenta útil para conversão entre bytecode e mnemônicos de instrução.
Contornagens para outras técnicas anti-debugging baseadas em ptrace podem ser encontradas em "Defeating Anti-Debug Techniques: macOS ptrace variants" por Alexander O'Mara.
Usando sysctl¶
Outra abordagem para detectar um debugger anexado ao processo chamador envolve sysctl. De acordo com a documentação da Apple, ele permite que processos definam informações do sistema (se tiverem os privilégios apropriados) ou simplesmente recuperem informações do sistema (como se o processo está sendo depurado ou não). No entanto, note que apenas o fato de um app usar sysctl pode ser um indicador de controles anti-debugging, embora isso nem sempre seja o caso.
O Apple Documentation Archive inclui um exemplo que verifica a flag info.kp_proc.p_flag retornada pela chamada a sysctl com os parâmetros apropriados. De acordo com a Apple, você não deve usar este código a menos que seja para a versão de debug do seu programa.
Contornar: Uma maneira de contornar essa verificação é modificando o binário. Quando o código acima é compilado, a versão desmontada da segunda metade do código é semelhante à seguinte:

Após a instrução no offset 0xC13C, MOVNE R0, #1 é modificada e alterada para MOVNE R0, #0 (0x00 0x20 em bytecode), o código modificado é semelhante ao seguinte:

Você também pode contornar uma verificação sysctl usando o próprio debugger e definindo um breakpoint na chamada para sysctl. Essa abordagem é demonstrada em iOS Anti-Debugging Protections #2.
Usando getppid¶
Aplicações no iOS podem detectar se foram iniciadas por um debugger verificando seu PID pai. Normalmente, uma aplicação é iniciada pelo processo launchd, que é o primeiro processo em execução no modo de usuário e tem PID=1. No entanto, se um debugger iniciar uma aplicação, podemos observar que getppid retorna um PID diferente de 1. Essa técnica de detecção pode ser implementada em código nativo (via syscalls), usando Objective-C ou Swift como mostrado aqui:
func AmIBeingDebugged() -> Bool {
return getppid() != 1
}
Contornar: Assim como as outras técnicas, isso também tem uma contornagem trivial (por exemplo, modificando o binário ou usando hooks do Frida).