0x01. DEFINITIONS
Je met permet de définir en bref ce terme pour éviter toutes confusions. Le reversing est l'art de décortiquer un programme. A l'origine il est question de reverser un programme lorsqu'il a un défaut de conception et que l'on a pas accès aux sources. Il permettait donc pour les experts de rendre inoffensif un virus ou alors d'empêcher à un programme trop curieux de surveiller son ordinateur à son insu.
Tout programme fait un appel à une fonction interne ou externe, c'est cette fonction qui permet le bon fonctionnement du programme. Si on arrive à conna�tre le rôle de ces fonctions on peut alors par exemple inverser les tests, les esquiver ou les supprimer afin que le programme fasse ce qu'on lui demande plutôt que de remplir sa fonction d'origine. Définissons également quelques notions qui sont omniprésentes.
0x02. STRING DATA
Les données de chaines de caractères, appelées 'String Data' représentent les ressources du programme, c'est à dire le nom des menus, des boites de dialogues et des titres. Par exemple le bloc notes doit avoir comme 'String Data' la ressource "Fichier" car elle fait référence à un menu.
0x03. FONCTIONS EXPORTEES
Une fonction est un bout de programme qui correspond en assembleur souvent à un saut. Toutes les fonctions exportées ne correspondent pas nécessairement à un saut, et c'est même rarement le cas, car leur utilité est de transmettre une fonction du programme vers un autre, ou vers une DLL. La fonction d'achat d'un programme payant, enverra des informations de versions et autres relatives à un autre programme (register.exe, par exemple) Il y a aura donc une fonction exportée correspondante.
0x04. FONCTIONS IMPORTEES
Les fonctions importées correspondent en grande partie aux fonctions des DLLs. Il est très courant de voir un programme appeler des fonctions de Windows, ne serait-ce que pour l'affichage. Ce qu'on cherche dans les fonctions importées c'est souvent les appels à la base de registre, oà au boite de Dialogue (comme MessageBox, ...) . Voire à la fonction ExitProcess de Kernel32.dll. C'est à partir de ça que parfois il faut se baser pour 'cracker' un programme.
0x05. WINDASM
Inutile de présenter cet incontournable outil de désassemblage. Pour accéder directement à Windasm, cliquez simplement sur le titre. Accouplé à OllyDbg et d'une bonne logique, ils forment de très bons outils complémentaires. Dans l'exemple de cet article nous allons simplement montrer la puissance du deadlisting, c'est à dire une approche de reversing par désassemblage plutot que par débogage. Nous allons donc commencer par lancer Windasm et ouvrir le programme à cracker communément appelé 'crackme'. Une fois le fichier ouvert :
Refs -> String Data References
Une boite de dialogue appara�tra, il faudra alors chercher les valeurs sensibles...

Après avoir trouvé ces informations, Windasm nous envoie au code source assembleur faisant référence à cette chaine de caractère.
Nous avons dans ce cas plein d'informations intéressantes, notamment l'endroit oà va la fonction de comparaison lorsque le test est validé ici :
:004011128 cmp byte ptr [ebp-04] , 61 :00401112C je 0040114D :00401112E cmp byte ptr [ebp-04] , 7A :004011132 je 0040114D :004011134 cmp byte ptr [ebp-04] , 65 :004011138 je 0040114D :00401113A cmp byte ptr [ebp-04] , 72 :00401113E je 0040114D ... Possible StringData Ref from Data Obj -> "gagne :)" :00401114D push 0040C10A
Ici on voit très nettement que si le test est validé alors il va à l'adresse correspondante à celle de la chaine de caractère "gagne :)"

Pour cracker ce programme il y a donc plusieurs méthodes :
[1] dire au programme de faire le test inverse changer les je en jne
[2] comme on voit que le programme continue en dessous de son test pour dire que le mot de passe n'est pas bon, il suffit de lui dire de ne rien faire, ni tester ni dire que le mot de passe n'est pas bon
[3] deviner le mot de passe
[4] ou bien de lui dire de faire un test toujours vrai pour qu'il saute forcément à la bonne adresse
Voici comment faire pour chacun de ces cas.
0x06. REVERSING
Pour ça il s'agit de transformer un JE en JNE, de manière plus générale de transformer un test commençant par J en un test commençant par JN Pour inverser le test il faut, souvent mais pas tout le temps, de mettre le bit de poids faible du premier octet en partant de la droite à 1. Voici une liste de saut conditionnels et inconditionnels.
; Sauts conditionnels et opérande ; ------------------------------- JA Jump if Above =JNBE ; op2 > op1 JAE Jump if Above or Equal =JNB ; op2 >= op1 JB Jump if Below =JNAE ; op2 < op1 JBE Jump if Below or Equal =JNA ; op2 <= op1 JCXZ Jump if CX = 0 ; CX = 0 JG Jump if Greaater =JNLE ; op2 > op1 JGE Jump if Greater or Equal =JNL ; op2 >= op1 JL Jump if Less =JNGE ; op2 < op1 JLE Jump if Less or Equal ; op2 <= op1 JNA Jump if Not Above =JBE ; op2 <= op1 JNAE jump if Not Abobe or Equal ; op2 < op1 JNE Jump if Not Equal =JNZ ; op2 != op1 JNG Jump if Not Greater =JLE ; op2 <= op1 JNGE Jump if Not Greater or Equal ; op2 < op1 JNL Jump if Not Less =JGE ; op2 > op1 JNLE Jump if Not Less or Equal ; op2 >= op1 ; Saut inconditionnel ; ------------------- JMP Jump saut inconditionnel ; Sauts conditionnels et test sur les données ; ------------------------------------------- JNO Jump if Not Overflow ; saut s'il n'y pas de dépassement JNP Jump if Not Parity ; saut si pas de bit de parité JNS Jump if Not Sign ; saut si pas de bit signé JNZ Jump if Not Zero ; saut si différent de zéro JO Jump if Overflow ; saut si dépassement JP Jump if Parity =JPE ; saut si de bit de parité paire JPE Jump if Parity Even =JP ; saut si de bit de parité paire JPO Jump if Parity Odd =JNP ; saut si de bit de parité impaire JS Jump if Sign ; saut si bit signé JZ Jump if Zero =JE ; saut si égal à zéro ; Chargements par adresses ; ------------------------ LAHF Load AH with Flags ; bits arithmétique du "flag-reg" LDS Load pointer to DS ; adresse de op2 -> DS:op1 LEA Load Effective Address ; adresse de op2 -> op1 LES Load pointer to ES ; adresse de op2 -> ES:op1 LOCK LOCK the bus for 1 cycle ; réservation du bus pour 1 cycle LODB Load Byte ; Cône-mémoire -> AL LODW Load Word ; Cône-mémoire -> AX ; Boucle, on suppose que CX a une valeur avant de débuter la boucle ; ----------------------------------------------------------------- LOOP LOOP while CX != 0 ; branchement si CX = 0 LOOPZ LOOP while Zero =LOOPE ; branchement si CX = 0 et ZF = 1 LOOPNZ LOOP while Not Zero =LOOPNE ; branchement si CX = 0 et ZF = 0 LOOPE LOOP while Equal =LOOPZ ; branchement si CX = 0 et ZF = 1 LOOPNE LOOP while Not Equal =LOOPNZ ; branchement si CX = 0 et ZF = 0
Source: Net Hackers du juillet-aoàt 2006 / FaSm
Ainsi nous allons changer nos JE en JNE ...
L'adresse (offset) de l'octet se situe tout en bas de l'écran : 0x72C -ou- 72Ch , le '0x' ou le 'h' signifie 'hexadécimal'
Il faut bien sûr faire une copie du programme avant de l'éditer. J'aime bien l'idée de laisser le programme d'origine et de faire deux copies :
"asm de crackme.exe" et "hexedit de crackme.exe"
Maintenant il n'y que la valeur à changer, dans notre cas les 4 valeurs:
Comme ci-dessous.
Le but lors de l'écrasement de tests est tout simplement de lui de ne RIEN FAIRE. Pour ne rien faire, il existe en assembleur, une instruction codée sur 1 octet appelée NOP. NOP signigie "No OPeration". Pour reverser, cracker une application le NOP n'est pas toujours la solution bien qu'il soit très souvent utilisé. Voici la zone de test ci-dessous que nous allons écraser avec notre instructon.
Si le programme ne fait aucun tests et ne fait rien au lieu d'afficher "perdu!" alors il se contentera d'exécuter à partir de "gagne :)"
Dans l'éditeur héxadécimal il suffit de ne mettre que des NOP depuis l'adresse du programme 00401128 jusqu'à 00401114D :
; Depuis 0x728, offset dans le programme :004011128 cmp byte ptr [ebp-04] , 61 ; Jusqu'à 0x74D :00401114D push 0040C10A
on aura donc tout les valeurs en rouge à mettre à 0x90, 90 en héxadécimal :

-> devient ->

Dans ce cas là il n'est pas trop dur à lire...

On peut le voire dans l'éditeur héxadécimal...

le mot de passe est donc azer
L'idée est de réaliser un test sur le buffer lui-même plutot que sur les caractères. Effectivement le test sera toujours vrai.
Le bout de programme ci-dessous "signifie" en langage interpretable :
En considérant que ptr [ebp-04] est une variable :
comparer la variable à 0x61
si le test est vrai alors aller en 0040114D
deviendra
comparer la variable à la variable
si le test est vrai alors aller en 0040114D
le test ira donc toujours en 0040114D > "gagne :)"
D'après le code que l'on peut voir :

807DFC61 cmp byte ptr [ebp-04] , 61 ; 807D = cmp byte ; FC = ptr [ebp-04] ; 61 = 'a' 807DFCFC cmp byte ptr [ebp-04] , ptr [ebp-04]
0x07. CODING
Nous allos donc maintenant coder le programme ne C qui permettra de patcher un crackme générique.
Voici un bout de code en C qui va permettre de tester si on est sur le bon octet et si c'est le cas
le remplacer la valeur :
1- D'abord on ouvre le fichier en mode "mise à jour" c'est à dire "r+b"
flux_fichier=fopen(nom_de_fichier,"r+b");
2- Ensuite on se positionne sur l'offset trouvé avec WinDasm, avec fseek et le paramètre SEEK_SET
3- Il faut tester si on est sur le bon caractère :
if( test != 0x74 ) // octet à tester, par exemple 0x74, si ce n'est pas le bon, { // on avertit et quitte le programme printf("/nCe n'est pas la bonne version du programme./n"); exit(1); }
4- On revient au début du fichier pour se repositionner car avec fgetc, fseek on avance d'un caractère, il faut revenir au début :
rewind(flux_fichier);
5- Ensuite on se positionne sur l'offset trouvé avec WinDasm cette fois ci pour mettre la bonne valeur :
6- Tout ceci est à faire autant d'octets il y a à patcher, donc dans une boucle.
0x08. CONCLUSION
Voici les programmes utilisés au cours de ce tutoriel
Lien : Le programme qui patch le 'crackme'
Lien : Source complète du crackme
=> Écrit par : Nicolas, le 25 juillet 2015
reverse