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
- $ 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)
- 0x0804809d <_start+41>: cmpl $0x37333331,(%ecx)
- $ 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)
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.
0 commentaires:
Enregistrer un commentaire