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

December 12th, 2008 by SAWP | Filed under Programming Internals

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:

  1.  
  2. /*
  3. * da mesma forma que fizemos como exemplo no tópico “Detectando Máquinas Virtuais”,
  4. * antes de termos uma função para inicializar o programa, sempre colocamos a função
  5. * para teste das condicoes (onde testamos licença, presença de vm, de debugger, etc).
  6. *
  7. * no java, seria equivalente ao bloco static{ } da primeira classe a ser carregada.
  8. */
  9. void prestarter(){
  10.         __asm(
  11.                 mov %eax, %fs: [00000018]
  12.                 mov %eax, [%eax+0×30]
  13.                 cmp byte ptr [%eax+0×02], 0
  14.                 je _RunProgram
  15.         );
  16. }
  17.  
  18. void RunProgram(){
  19.         // start you program here
  20. }
  21.  

 

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:

  1.  
  2.  
  3. void prestarter(){
  4.         bool trapFlag = true;   
  5.  
  6.         try {
  7.                 __asm__("pushfd; "
  8.                                 "or dword ptr[%esp], 0×100; "
  9.                                 "popfd;"
  10.                                 "nop;");
  11.         } catch (Exception e) {
  12.                 trapFlag = false;       // uma exceção foi gerada, então não existe debugger
  13.         }
  14.  
  15.         if(trapFlag){
  16.                 printf(“Debugger esta presente.”);
  17.                 die();                  // programa morre aqui, não executa se tiver debugger
  18.         }else{
  19.                 RunProgram();   // as opções, funcionalidades, etc, são abertas aqui
  20.         }
  21. }
  22.  
  23. void RunProgram(){
  24.         // start you program here
  25. }
  26.  
  27. void main(){
  28.         prestarter();
  29. }
  30.  

 

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.

 

Tags: tag_icon [ Tags: | Programming Internals ]

Você pode ler as eventuais respostas desta entrada através do RSS 2.0 feed.
As respostas estão encerradas, mas você pode trackback a partir do seu próprio site.