Techniques anti-debugging et protection logicielle
IV°) La protection par code checksum (vérification de la somme)
Explication de la technique
En ouvrant un éxécutable avec un éditeur hexadécimal tel hexedit, on peut avoir le bytecode de
notre programme. Autrement dit, le code complet en hexadécimal ou en décimal (ce qui donne les caractères que l'on
peut voir en ouvrant un éxécutable avec un éditeur texte, qui est la correspondance ascii du bytecode). Le code complet
n'étant qu'une succession de nombres (les opcodes), il nous est tout à fait possible d'en faire la somme. Le
principe de la protection par code checksum est de vérifier que la somme de ces opcodes n'a pas été modifiée, autrement
dit, que le programme tourne avec son code original. Pour ce, nous allons, à l'aide d'un
programme simple en assembleur, tout d'abord vous montrer comment il est possible de contourner facilement des instructions
de comparaison (cmp, cmpl) puis comment insérer un code de vérification de la somme des opcodes.
Exemple
Nous allons étudier le programme suivant, toujours codé en assembleur AT&T pour Unix. Il n'est autre
qu'un classique Hello, World, légèrement modifié pour illustrer notre point et également codé en version longue (d'une part pour
compliquer un peu le code et décourager les crackers débutants, c'est une bonne habitude à prendre, et d'autre part car les instructions
telles xor ou inc sont bien plus rapides que mov, mais ce sont des détails ;-) ) :
.data #declaration du segment des variables statiques initialisées
bonjour: .string "Hello, World !\n"
non_affiche: .string "Ce message ne peut pas être affiché\n"
.text #declaration du segment code
.global _start
_start:
xorl %eax,%eax #Affichage de Hello, World !
movb $4, %al
xorl %ebx,%ebx
inc %ebx
movl $bonjour,%ecx
xorl %edx,%edx
mov $15,%edx
int $0x80
xorl %eax,%eax #On mets eax à 0, puis on compare 1 et al, ce qui est donc toujours faux
cmp $1,%al
jne exit #Ce Jump if Not Equal sera donc en théorie toujours réalisé
naffiche: #Affiche Ce message ne peut pas être affiché
xorl %eax,%eax
movb $4, %al
xorl %ebx,%ebx
inc %ebx
movl $non_affiche,%ecx
xorl %edx,%edx
mov $36,%edx
int $0x80
exit: #sortie
xorl %eax,%eax
xorl %ebx,%ebx
inc %eax
int $0x80
Pour ceux qui ne connaissent pas très bien l'assembleur, l'essentiel de ce code est expliqué dans la partie concernant
le faux désassemblage. A noter que, comme indiqué, nous avons amplifié le code, par
exemple, on aurait pu coder le label de sortie de façon plus simple :
exit: #sortie
mov $1,%eax
mov $0,%ebx
int $0x80
Voici les sorties de ce programme en éxécution normale :
$ gcc test.s -c -o test.o && ld test.o && ./a.out
Hello, World !
$
Comme prévu, le programme ne pouvant pas passer par le label naffiche (question de logique informatique), on a
seulement un Hello, World ! classique qui s'affiche. Bien entendu, avec quelques connaissances légères en cracking,
on sait facilement détourner le flux de ce programme. A l'aide d'un éditeur hexadécimal, on va donc modifier
3C 01 (cmp $1,%al)
75 15 (jne +15 <exit>)
en
3C 00 (cmp $0,%al)
75 15 (jne +15 <exit>)
ou
3C 01 (cmp $1,%al)
74 15 (je +15 <exit>)
Je pense que vous avez compris le but de la manoeuvre : soit on compare %al à 0, ce qui est toujours vrai et on
n'utilise pas l'instruction Jump if Not Equal to exit, ou on compare %al à 1, ce qui est toujours faux et on
n'utilise pas l'instruction Jump if Equal to exit). Vérifions le résultat de cette modification :
$ hexedit a.out
$./a.out
Hello, World !
Ce message ne peut pas être affiché
$
Notre manipulation a donc parfaitement fonctionné et le cours du programme a été modifié, la présence d'un message
qui n'aurait jamais pu être affiché en temps normal le prouve. Maintenant, nous allons protéger l'éxécutable par
code checksum. En réalité, on le voit bien dans les deux modifications précédentes que la somme des opcodes sera décrémentée
de 1 après l'édition de l'éxécutable. Voici le nouveau code protégé :
.data #declaration du segment des variables statiques initialisées
bonjour: .string "Hello, World !\n"
non_affiche: .string "Ce message ne peut pas être affiché\n"
tentative_crack: .string "Tentative de crack !\nAbandon...\n"
.text #declaration du segment code
.global _start
_start:
jmp checksum #On commence par effectuer Checksum
suite: #on revient ici après le checksum s'il est positif
xorl %eax,%eax
movb $4, %al
xorl %ebx,%ebx
inc %ebx
movl $bonjour,%ecx
xorl %edx,%edx
mov $15,%edx
int $0x80
xorl %eax,%eax
cmp $1,%al
jne exit
naffiche:
xorl %eax,%eax
movb $4, %al
xorl %ebx,%ebx
inc %ebx
movl $non_affiche,%ecx
xorl %edx,%edx
mov $36,%edx
int $0x80
exit:
xorl %eax,%eax
xorl %ebx,%ebx
inc %eax
int $0x80
checksum: #fonction checksum
xorl %ebx,%ebx
mov $checksum,%ecx
sub $_start,%ecx
mov $_start,%esi
boucle: #boucle d'addition des opcodes
lodsb
add %eax,%ebx
loop boucle
cmpl $5917,%ebx  #on a au préalable compté les opcodes et trouvé 5917
jne crack  #Si le résultat de la boucle n'est pas 5917, on passe à <crack>
jmp suite  #sinon on revient au début du programme
crack: #On avertit de la tentative de crack et on quitte
xorl %eax,%eax
movb $4, %al
xorl %ebx,%ebx
inc %ebx
movl $tentative_crack,%ecx
xorl %edx,%edx
mov $32,%edx
int $0x80
jmp exit
Il ne nous reste plus qu'à vérifier que ce code checksum marche réellement :
$ gcc checksum.s -c -o checksum.o && ld checksum.o -o checksum && ./checksum
Hello, World !
$ hexedit checksum
$ ./checksum
Tentative de crack !
Abandon...
$
Tout s'est bien déroulé, la protection n'entrave pas le fonctionnement du programme et empêche toute modification
de la somme des opcodes. Cette technique est réellement puissante, surtout quand elle est combinée à d'autres techniques
comme le faux désassemblage, ce qui rend très dur pour l'éventuel reverser ou cracker de modifier le programme à sa guise
(puisque rien que le positionnement d'un breakpoint terminera l'éxécution du programme). Il doit alors, soit combler ses
modifications par des instructions sans importance mais qui feront la même somme au final, ce qui n'est pas facile,
soit réussir à déjouer le faux assemblage puis à détourner la fonction checksum, ce qui est aussi ardu. Par conséquent,
cette protection est de loin la plus efficace que nous vous ayons exposé ici. A vrai dire, la seule protection qui
est plus efficace est la protection par cryptage du code, que nous vous exposerons une fois la partie réseaux reconstruite.