Techniques anti-debugging et protection logicielle


<< La méthode ptrace Faux points d'arrêt >>


II°) Le faux désassemblage (false disassembly)

Explication de la technique
   Encore une fois, le principe de cette technique est plutôt simple (il suffisait d'y penser, comme qui dirait..). Le but est de place des chaines de caractères dans le programme en assembleur ayant la valeur d'opcodes ainsi, le debuggeur trouvant ces opcodes va les prendre en compte. En plaçant des caractères bien étudiés, on peut totalement brouiller un code désassemblé ou juste les parties que l'on ne veut pas montrer. L'illustration de ce procédé devrait rendre les choses limpides.

Illustration
   Intéressons-nous au code largement commenté qui suit. Ce code est en langage assembleur, synthaxe AT&T pour Unix. Pour ceux qui ne connaissent pas l'assembleur, il est aisé de le comprendre après avoir lu la partie mémoire de ce site : tout d'abord, les segments data et bss sont remplis si besoin est, puis le segment text. Ensuite, vous n'avez qu'une suite d'appels systèmes qui se déroulent de la même façon, à savoir, on entre dans le registre %eax le numéro de l'appel système, puis dans les registres suivants les variables nécessaires à l'appel (placés dans la pile), puis 0x80 qui correspond à l'appel au kernel qui va entraîner l'éxécution du syscall. Vous avez accès à l'ensemble des appels systèmes depuis /usr/include/asm-i386/unistd.h et à leurs paramètres dans les pages du manuel correspondantes. Intéressons nous maintenant à ce programme d'authentification :
    .data #declaration des variables statiques initialisées

      auth_req: .string "Authentification requise\nMot de passe :\t"

      ok: .string "Authentification OK\n"

      mauvais: .string "Echec de l'authentification\nAbandon...\n"

    .text #declaration du code

      .global _start
       _start:

         mov $4, %eax   #Afficher le message auth_req
         mov $1,%ebx   #1 est le flux STDOUT (votre écran)
         mov $auth_req,%ecx   #On mets le message auth_req dans la pile
         mov $40,%edx   #40 caractères à afficher
         int $0x80   #On effectue le syscall 4

         mov $3,%eax   #Lire la réponse au clavier
         mov $0,%ebx   #0 est le flux STDIN (clavier)
         movl %esp,%ecx   #%ecx pointe vers le haut de la pile (donc vers ce qui sera entré qui sera en haut de la pile après le syscall)
         mov $10,%edx   #On lit 10 caractères max
         int $0x80   #on effectue le syscall 3

         cmpl $0x37333331,(%ecx)   #On compare ce qui a été entré avec 0x37333331 qui est 7331 en héxadécimal, interprété 1337 en mémoire (little endian)
         jne echec   #Si ce n'est pas égal, on passe au label echec
         je debut   #sinon au label debut

       exit:
         mov $1, %eax   #Quitter
         mov $0, %ebx   #Code de sortie (ici 0, pas d'erreur)
         int $0x80   #Appel au syscall 1

       debut:
         mov $4, %eax   #Afficher le message ok
         mov $1,%ebx
         movl $ok,%ecx
         mov $20,%edx
         int $0x80
         jmp exit   #On passe au label exit

       echec:
         mov $4, %eax   #Afficher le message mauvais
         mov $1,%ebx
         movl $mauvais,%ecx
         mov $39,%edx
         int $0x80
         jmp exit
Cet exemple d'une authentification archaïque est simple : il affiche un prompt à l'écran demandant le mot de passe, prend 10 caractères et vérifie s'il s'agit du bon mot de passe ou non (ici, 1337). Ensuite, il affiche à l'écran le résultat de l'authentification. Même s'il ne faut jamais vérifier les mots de passe de cette façon, notre but ici est plus de montrer comment arriver à cacher les points sensibles d'un programme au désassemblage. Tout d'abord, essayons le programme et essayons de le désassembler à l'aide de gdb :
    $ gcc auth.s -c -o auth.o && ld auth.o -o auth && ./auth
    Authentification requise
    Mot de passe : test-pass
    Echec de l'authentification
    Abandon...
    $ ./auth
    Authentification requise
    Mot de passe : 1337
    Authentification OK
    $ gdb -q auth
    (no debugging symbols found)
    Using host libthread_db library "/lib/libthread_db.so.1".
    (gdb) disas _start
    Dump of assembler code for function _start:
    0x08048074 <_start+0>: mov $0x4,%eax
    0x08048079 <_start+5>: mov $0x1,%ebx
    0x0804807e <_start+10>: mov $0x80490e0,%ecx
    0x08048083 <_start+15>: mov $0x28,%edx
    0x08048088 <_start+20>: int $0x80
    0x0804808a <_start+22>: mov $0x3,%eax
    0x0804808f <_start+27>: mov $0x0,%ebx
    0x08048094 <_start+32>: mov %esp,%ecx
    0x08048096 <_start+34>: mov $0xa,%edx
    0x0804809b <_start+39>: int $0x80
    0x0804809d <_start+41>: cmpl $0x37333331,(%ecx)
    0x080480a3 <_start+47>: jne 0x80480c6 <echec>
    0x080480a5 <_start+49>: je 0x80480ae <debut>
    End of assembler dump.
    (gdb)
On a donc compilé et linké le programme et il semble marcher. Ensuite, on a désassemblé le label _start avec gdb. Evidemment, le pass apparaît en clair, puisque nous l'avions laissé en clair dans le programme :
    0x0804809d <_start+41>: cmpl $0x37333331,(%ecx)
Maintenant, nous allons ajouter dans le code de l'assembleur l'instruction .ascii "\xeb\x01\xe8" juste avant l'instruction cmpl et observer comment le désassembleur de gdb va l'interpréter :
    $ gcc auth.s -c -o auth.o && ld auth.o -o auth && ./auth
    Authentification requise
    Mot de passe : 1337
    Authentification OK
    $ ./auth
    Authentification requise
    Mot de passe : retest
    Echec de l'authentification
    Abandon...
    $ gdb -q auth
    (no debugging symbols found)
    Using host libthread_db library "/lib/libthread_db.so.1".
    (gdb) disas _start
    Dump of assembler code for function _start:
    0x08048074 lt;_start+0gt;: mov $0x4,%eax
    0x08048079 lt;_start+5gt;: mov $0x1,%ebx
    0x0804807e lt;_start+10gt;: mov $0x80490e4,%ecx
    0x08048083 lt;_start+15gt;: mov $0x28,%edx
    0x08048088 lt;_start+20gt;: int $0x80
    0x0804808a lt;_start+22gt;: mov $0x3,%eax
    0x0804808f lt;_start+27gt;: mov $0x0,%ebx
    0x08048094 lt;_start+32gt;: mov %esp,%ecx
    0x08048096 lt;_start+34gt;: mov $0xa,%edx
    0x0804809b lt;_start+39gt;: int $0x80
    0x0804809d lt;_start+41gt;: jmp 0x80480a0 <_start+44>
    0x0804809f lt;_start+43gt;: call 0x3b35ba25
    0x080480a4 lt;_start+48gt;: xor (%edi),%esi
    0x080480a6 lt;_start+50gt;: jne 0x80480c9 <echec>
    0x080480a8 lt;_start+52gt;: je 0x80480b1 <debut>
    End of assembler dump.
    (gdb)
Effectivement, malgré le bon fonctionnement du programme, à partir de _start + 41, rien ne va plus dans ce désassemblage !
En fait, le résultat est facilement explicable : le désassembleur a interprété la chaîne que l'on a déclaré comme des opcodes. Or, \xEB est l'équivalent en hexadecimal de l'instruction jmp, le \x01 qui le suit indique donc qu'il faut faire un jmp d'un octet et \xE8 est le début d'un call (qui appelle une fonction). Par conséquent, on a désaligné le code désassemblé qui va afficher un jmp +1 puis un call avec les 4 prochaines bytes qu'il va trouver et continuer avec des instructions sans sens jusqu'à retomber sur ses pattes (c'est à dire jusqu'à retrouver l'alignement des réelles instructions, ici, trois lignes plus tard avec le jne).
Cette technique est fréquemment utilisée pour cacher les sauts aux fonctions checksum ou les vérifications des appels ptrace. Elle peut aussi être utilisée pour brouiller d'autres parties du code et le rendre illisible, ce qui complique vraiment la tâche de l'éventuel cracker.


<< La méthode ptrace Faux points d'arrêt >>



5 Commentaires
Afficher tous


FrizN 09/11/11 11:14
J'ajouterais en effet que le faux désassemblage permet de ralentir l'analyse et non pas de protéger l'exécutable. Mais bien fait, ça peut être particulièrement pénible.

FrizN 09/11/11 11:12
Je me suis mal expliqué je pense. Le .ascii a des conséquences. 0xeb0x0a effectue un jmp short 1 que l'on voit bien à _start+41. A l'exécution le 0xe8 est donc ignoré. Seulement, le désassembleur continue sur son alignement d'instructions et commence à parser un call avec le 0xe8. C'est donc une faiblesse du désassemblage, mais ignorer l'alignement et partir à jmp 1 aurait les mêmes conséquences si par le jmp 1 était du code mort.

SansNom 09/11/11 10:14
Bonjour. Très intéressant. Mais comment se fait-il qu'à l'exécution la bonne suite d'instructions soit reconnue (et le .ascii sans conséquences) mais que le désassembleur soit piégé ? Ne s'agit-il pas plutôt d'une faiblesse du désassembleur qu'une vraie protection ?

FrizN 18/07/11 06:57
Oups c'est exact.. Par contre dans les sources (lien dans le menu) vous trouvez bien l'exemple avec le .ascii, qui in fine se rajoute tel quel dans la syntaxe AT&T, avant le cmpl.




Commentaires désactivés.

Apprendre la base du hacking - Liens sécurité informatique/hacking - Contact

Copyright © Bases-Hacking 2007-2014. All rights reserved.