Técnicas AntiCracking – Parte IV – Técnicas Ativas de Antidebug

As técnicas mencionadas abaixo são utilizadas para dificultar a análise dinâmica do código, aquela quando usamos um debugger.

No post “A Fórmula do Crack” discutimos alguma coisa sobre debuggers. Como é o processo de debuggin em user-mode e kernel-mode, como cada um deles trabalha quando um usuário anexa um processo à eles, quando fixa um breakpoints em uma instrução etc.

O que não foi discutido é a forma como o debugger atua: quando o usuário escolhe pontos de parada para conferir o andamento da execução do programa é comum o debugger substituir a(s) instrução(ões) por uma instrução própria, que só ele entenda. Esta instrução é conhecida como “interrupção de breakpoint” (breakpoint interrupt) e não é executada pelo programa, mas notifica ao debugger que uma interrupção foi encontrada naquele ponto. Quando a execução chega nesta instrução, o debugger congela a execução do programa e podemos inspecionar seu estado.

Sabendo que estas instruções geram comportamentos específicos – como levantamento de exceções – utilizamo-as como chaves para detecção dos debuggers.

1. hadUserModeDebugger__

hadUserModeDebbuger, também conhecido como UserDebuggerInformation ou isDebuggerPresentAPI é uma API (Aplication Program Interface) usada na detecção de debuggers que trabalham em user-mode, tais como o OllyDbg.

Quando implementada em um aplicativo acessa o “Process Environment Block” (PEB) para determinar se um debugger foi encontrado. A implementação do código é a seguinte:

/*
* da mesma forma que fizemos como exemplo no tópico “Detectando Máquinas Virtuais”, 
* antes de termos uma função para inicializar o programa, sempre colocamos a função
* para teste das condicoes (onde testamos licença, presença de vm, de debugger, etc).
*
* no java, seria equivalente ao bloco static{ } da primeira classe a ser carregada.
*/
void prestarter(){
	__asm(
		mov %eax, %fs: [00000018]
		mov %eax, [%eax+0x30]
		cmp byte ptr [%eax+0x02], 0
		je _RunProgram
	);
}
 
void RunProgram(){
	// start you program here
}

 

2. The Trick

Esta técnica consiste em utilizar uma “flag” (Trap Flag) para detectar se uma exceção específica é gerada ou não. É uma abordagem semelhante à alguns algoritmos de detecção de máquinas virtuais, como já comentei antes (“The Jerry Code”, http://www.trapkit.de/research/vmm/jerry/index.html , que utiliza instruções que não são da arquitetura física para gera exceções específicas para detecção).

Um valor booleano é criado para determinar se o sistema caiu na armadilha ou não. A armadilha é a seguinte: se a exceção não é gerada, então é porque um debugger retirou a exceção para que o programa possa ser analisado. Ou seja, a única possibilidade da “flag” não mudar de estado é que “algo” impeça a exceção. E este “algo” é o próprio debugger.

A vantagem desta abordagem é que ela pode detectar de forma simples qualquer debugger que esteja monitorando o programa que desejamos proteger, seja em modo usuário ou kernel.

Também é uma forma relativamente segura, pois não depende de uma API externa do sistema operacional e mantém todo o controle dentro da própria aplicação.

Uma implementação da Armadilha é a seguinte:

 
void prestarter(){
	bool trapFlag = true;	
 
	try { 
		__asm__("pushfd; " 
				"or dword ptr[%esp], 0x100; " 
				"popfd;" 
				"nop;"); 
	} catch (Exception e) { 
		trapFlag = false;	// uma exceção foi gerada, então não existe debugger
	}
 
	if(trapFlag){
		printf(“Debugger esta presente.”);
		die();			// programa morre aqui, não executa se tiver debugger
	}else{
		RunProgram();	// as opções, funcionalidades, etc, são abertas aqui
	}
}
 
void RunProgram(){
	// start you program here
}
 
void main(){
	prestarter();
}

 

Conclusão

Estas implementações tendem a ser bem eficientes, mas deve-se ficar claro que é restrita à família NT. Embora as AADTs sejam as que oferecem menos perda de performance (quase nula, como observa-se nos códigos acima), são as que oferecem mais riscos, pois qualquer incompatibilidade com o ambiente de execução acarreta em um erro fatal, impedindo execução do programa.

Outra desvantagem das AADTs é que elas são dependentes de arquitetura. O que torna o sistema não-portável.