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.
Pour ce faire, 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’exé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’exé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 #déclaration 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 chiffrement du code, que nous vous exposerons une fois la partie réseaux reconstruite.