Techniques anti-debugging et protection logicielle
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.