J'ai enfin décidé de m'attaquer au fameux Trial crackme !
Lorque Toniard m'avait fait une première proposition il y a quelques mois (je ne sais pas s'il s'en souvient),
je lui avais répondu que je débutais, et j'ai juste survolé le crackme de Fnaax pour voir de quoi il ressortait.
J'ai rapidement constaté que c'était hors de ma portée et je n'ai pas insisté...
D'autant que le challenge était réputé pour être plutôt ardu (selon un message du forum);
alors naturellement, j'ai abandonné.
Ce n'est que récemment (le 5 Juin 2003) qu'un autre membre de la team, RocketSpawn, m'a relancé et m'a encouragé à tenter
ma chance. Il m'a finalement convaincu et m'a donné quelques
conseils pour aborder le programme, notamment en ce qui concerne la technique d'unpacking.
Je me suis donc lancé à l'aventure pour finalement aboutir à ce tutoriel, qui je l'espère,
vous plaira et s'avérera suffisamment complet.
Voilà pour cette introduction, je vous propose dès maintenant de passer à l'étude du crackme, si vous le voulez bien !
On va commencer par lancer le programme "à vide" pour voir à quoi l'on a affaire.
Trois champs de texte sont affichés : "Name", "Key" et "Code", en bas nous avons un label qui fait office de
status bar.
Si l'on clique sur "Go!", un vilain nag apparaît, si on le supprime, des message box
s'affichent en boucle infinie... On est obligé de faire un Ctrl + Alt + Suppr et de mettre fin au processus
pour quitter !
Le programme paraît donc d'emblée assez vicieux car au moins deux clés sont calculées ("Key" et "Code"),
et d'autre part il y a ce nag qui nous limite dans notre champ d'action, puisque l'on a le droit de
ne faire qu'un seul essai à chaque fois que l'on lance l'éxécutable.
Bon, nous allons regarder ce que cela donne avec un debugger.
Pour ma part, j'ai utilisé OllyDbg et WinDbg, mais
les manipulations sont aussi valables sous SoftIce ou tout autre outil du genre.
On charge donc l'application, on va à l'entry point et l'on trouve quelque chose qui ressemble à ceci.
0041009C JMP crkmetri.00410147 004100A1 DD crkmetri.00411CAC 004100A5 DD <&KERNEL32.LoadLibraryA> 004100A9 DD <&KERNEL32.GetProcAddress> 004100AD DD 00000000 004100B1 DD 000028AC 004100B5 DD crkmetri.0041015E 004100B9 ASCII "NeoLite Executab" 004100C9 ASCII "le File Compress" 004100D9 ASCII "or Copyright (c" 004100E9 ASCII ") 1998,1999 NeoW" 004100F9 ASCII "orx Inc Portion" 00410109 ASCII "s Copyright (c) " 00410119 ASCII "1997-1999 Lee Ha" 00410129 ASCII "siuk All Rights" 00410139 ASCII " Reserved.",0 00410146 DB 00 00410147 MOV EAX,DWORD PTR SS:[ESP+4] 0041014B AND EAX,DWORD PTR DS:[4100AD] 00410151 CALL crkmetri.00410643 00410156 INC BYTE PTR DS:[410146] 0041015C JMP EAX 0041015E CMP BYTE PTR DS:[410146],0 00410165 JNZ SHORT crkmetri.0041017A 00410167 NOP 00410168 NOP 00410169 NOP 0041016A NOP 0041016B PUSH EAX 0041016C SUB EAX,EAX 0041016E CALL crkmetri.00410643 00410173 POP EAX 00410174 INC BYTE PTR DS:[410146] 0041017A RETN
Bref, on constate que le PE a été packé (avec Neolite, mais peu importe).
Plusieurs choix s'offrent alors à nous quand à la technique à utiliser.
00410146 DB 00 00410147 MOV EAX,DWORD PTR SS:[ESP+4] 0041014B AND EAX,DWORD PTR DS:[4100AD] 00410151 CALL crkmetri.00410643 <-- procédure de décompression en mémoire 00410156 INC BYTE PTR DS:[410146] 0041015C JMP EAX <-- saut vers le "vrai" entry point 0041015E CMP BYTE PTR DS:[410146],0 00410165 JNZ SHORT crkmetri.0041017A 00410167 NOP
On suit donc le saut (de toute façon on n'a pas trop le choix) pour se retrouver au début du programme :
00401000 PUSH crkmetri.004022DD <-- pose l'adresse d'un buffer 00401005 CALL crkmetri.004016E4 <-- GetLocaleTime : met la date courante dans ce buffer 0040100A CALL crkmetri.00401620 <-- procédure anti-debugger (!) 0040100F PUSH crkmetri.0040101E 00401014 CALL crkmetri.004016DE 00401019 JMP crkmetri.00401591 <-- saut vers des routines d'anti-debugging (!) 0040101E NOP 0040101F PUSH 0 00401021 CALL crkmetri.004016EA 00401026 JMP crkmetri.00401657 0040102B PUSH 0 0040102D CALL crkmetri.004016D2 00401032 MOV DWORD PTR DS:[402268],EAX 00401037 PUSH 0 00401039 PUSH crkmetri.004012F0 0040103E PUSH 0 00401040 PUSH 3E8 00401045 PUSH DWORD PTR DS:[402268] 0040104B CALL crkmetri.004016AE <-- DialogBoxParamA : affiche la boîte de dialogue 00401050 PUSH 0 00401052 CALL crkmetri.004016EA
C'est assez bizarre, n'est-ce pas ? Je vais vous faire un petit résumé.
00401436 PUSH 32 00401438 PUSH crkmetri.0040226C 0040143D PUSH 6E 0040143F PUSH DWORD PTR SS:[EBP+8] 00401442 CALL crkmetri.004016C0 <-- GetDlgItemTextA : récupère le nom 00401447 TEST EAX,EAX <-- teste si un nom a été rentré 00401449 JE SHORT crkmetri.004014B1 <-- si ce n'est pas le cas, affiche un message dans la status bar 0040144B CALL crkmetri.00401620 <-- protection anti-debugger 00401450 PUSH 0A 00401452 PUSH crkmetri.0040229F 00401457 PUSH 6F 00401459 PUSH DWORD PTR SS:[EBP+8] 0040145C CALL crkmetri.004016C0 <-- GetDlgItemTextA : récupère la clé 00401461 TEST EAX,EAX <-- ... 00401463 JE SHORT crkmetri.004014B1 <-- ... 00401465 PUSH 32 00401467 PUSH crkmetri.004022AA 0040146C PUSH 70 0040146E PUSH DWORD PTR SS:[EBP+8] 00401471 CALL crkmetri.004016C0 <-- GetDlgItemTextA : récupère le code 00401476 TEST EAX,EAX <-- ... 00401478 JE SHORT crkmetri.004014B1 <-- ... 0040147A CMP EAX,1F <-- compare la longueur du code à 1F (31 caractères en décimal) 0040147D JL crkmetri.004015DB <-- si elle est inférieure, affiche le nag 00401483 JMP crkmetri.00401057 <-- saut vers la routine de vérification
Le crackme vérifie donc que tous les champs sont remplis, et teste la longueur du code,
lequel doit faire obligatoirement 31 caractères.
Méfiez-vous de la protection anti-debugger, pour ce faire nopez le call ou passez directement du
premier breakpoint au deuxième sans tracer. Lorsque vous reverrez cette protection, n'hésitez pas
à noper l'appel sur le champ ;-).
Redémarrez le programme, entrez cette fois-ci un code possédant la bonne longueur et vous arriverez
normalement jusqu'au saut qui mène à la routine de vérification.
Cette routine est relativement longue et se divise en plusieurs sous-parties, concernant le keyfile et le code.
On se rend vite compte que la "clé" que l'on rentre est en fait le nom d'un keyfile dans lequel le programme va récupérer des informations.
Au tout début de la routine, le crackme va tester si le nom du keyfile est valide.
00401057 MOV EAX,crkmetri.0040229F <-- eax = le nom du keyfile 0040105C ADD EAX,5 0040105F CMP BYTE PTR DS:[EAX],2E <-- teste si le 6ème caractère est un '.' (2E) 00401062 JNZ crkmetri.004015DB <-- si ce n'est pas le cas, affiche le nag 00401068 INC EAX 00401069 CMP BYTE PTR DS:[EAX],6B <-- teste si le 7ème caractère est un 'k' (6B) 0040106C JNZ crkmetri.004015DB <-- ... 00401072 INC EAX 00401073 CMP BYTE PTR DS:[EAX],65 <-- teste si le 8ème caractère est un 'e' (65) 00401076 JNZ crkmetri.004015DB <-- ... 0040107C INC EAX 0040107D CMP BYTE PTR DS:[EAX],79 <-- teste si le 9ème caractère est un 'y' (79) 00401080 JNZ crkmetri.004015DB <-- ... 00401086 INC EAX 00401087 MOV EAX,crkmetri.0040229F 0040108C MOVZX EDX,BYTE PTR DS:[EAX] <-- edx = 1er caractère 0040108F XOR DL,BYTE PTR DS:[EAX+1] <-- edx = edx xor 2ème caractère 00401092 XOR DL,BYTE PTR DS:[EAX+2] <-- edx = edx xor 3ème caractère 00401095 XOR DL,BYTE PTR DS:[EAX+3] <-- ... 00401098 XOR DL,BYTE PTR DS:[EAX+4] <-- ... 0040109B CMP DL,62 <-- teste si edx est égal à 62 (en héxadécimal) 0040109E JNZ crkmetri.004015DB <-- si ce n'est pas le cas, affiche le nag 004010A4 CMP BYTE PTR DS:[EAX+1],72 <-- teste si le deuxième caractère est un 'r' (72) 004010A8 JNZ crkmetri.004015DB <-- ...
Là aussi, un petit récapitulatif s'impose pour mettre les choses au clair.
1er caractère xor 2ème caractère ... xor 5ème caractère = 62
'5' xor '4' xor '3' ... xor 'b' = 62
,
l'égalité est bien vérifiée.
Ça y est, on a notre nom ! Si on rentre srqpb.key, on ne saute pas vers le nag !
Notez que l'on aurait pu faire un "key generator" permettant de trouver tous les noms
possibles par brute force. Je vous laisse essayer, c'est assez rapide à mettre en oeuvre.
Maintenant qu'il a ce nom de fichier, le programme va logiquement essayer de l'ouvrir pour y chercher une clé.
Si l'on regarde la suite du code, on remarque qu'il y a deux appels de fonction.
004010AE PUSH 0 004010B0 PUSH crkmetri.0040229F <-- pose le nom du fichier 004010B5 CALL crkmetri.004016CC <-- ouvre le fichier 004010BA CMP EAX,-1 <-- teste si le handle est valide (présence du fichier) 004010BD JE crkmetri.004015DB <-- si ce n'est pas le cas, affiche le nag 004010C3 PUSH 0A <-- pose la longueur de la clé (A, 10 en décimal) 004010C5 PUSH crkmetri.0040229F <-- pose le nom du fichier 004010CA PUSH EAX <-- pose le handle 004010CB CALL crkmetri.004016D8 <-- lit la clé et la stocke à la place du nom de fichier
Je pense que c'est assez clair. La clé est récupérée et elle est placée dans le buffer qui
contenait auparavant le nom du fichier.
C'est à ce moment-là que les choses deviennent intéressantes.
Le crackme va en effet générer une clé et la comparer avec celle contenue dans le fichier. Voyons cela plus en détail.
004010D0 XOR EAX,EAX 004010D2 XOR EBX,EBX 004010D4 XOR ECX,ECX 004010D6 XOR EDX,EDX 004010D8 MOV EAX,crkmetri.004022DD <-- eax = la date courante (cf. plus haut) 004010DD MOV EBX,crkmetri.0040226C <-- ebx = le nom 004010E2 MOV DX,WORD PTR DS:[EAX] <-- edx = l'année 004010E5 SUB DL,BYTE PTR DS:[EBX] <-- edx = edx - 1er caractère du nom 004010E7 ADD ECX,EDX <-- ecx = ecx + edx 004010E9 MOV DX,WORD PTR DS:[EAX+2] <-- edx = le mois 004010ED SUB DL,BYTE PTR DS:[EBX+1] <-- edx = edx - le deuxième byte du nom 004010F0 ADD ECX,EDX <-- ... 004010F2 MOV DX,WORD PTR DS:[EAX+4] <-- edx = le jour de la semaine 004010F6 SUB DL,BYTE PTR DS:[EBX+3] <-- ... 004010F9 ADD ECX,EDX <-- ... 004010FB MOV DX,WORD PTR DS:[EAX+6] <-- edx = le jour 004010FF SUB DL,BYTE PTR DS:[EBX+5] <-- ... 00401102 ADD ECX,EDX <-- ... 00401104 MOV DX,WORD PTR DS:[EAX+8] <-- edx = l'heure 00401108 SUB DL,BYTE PTR DS:[EBX+7] <-- ... 0040110B ADD ECX,EDX <-- ... 0040110D IMUL ECX,ECX,100000 <-- décale la somme de 5 bytes vers la gauche 00401113 PUSH ECX <-- pose la somme 00401114 PUSH crkmetri.00402000 00401119 PUSH crkmetri.004022F3 <-- pose le buffer 0040111E CALL crkmetri.00401696 <-- wsprintfA : convertit le nombre en string 00401123 ADD ESP,0C 00401126 CALL crkmetri.00401620 <-- protection anti-debugger 0040112B PUSH crkmetri.004022F3 <-- pose la clé créée 00401130 PUSH crkmetri.0040229F <-- pose la clé du fichier 00401135 CALL crkmetri.004016C6 <-- lstrcmpA : compare les deux clés 0040113A TEST EAX,EAX <-- teste si elles sont égales 0040113C JNZ crkmetri.004015DB <-- si ce n'est pas le cas, affiche le nag
On rend compte que la clé est créée à partir du nom et de la date, date qui a été récupérée
au début du programme (souvenez-vous, juste après l'entry point).
Mais la date varie ;-), ce qui fait que l'on a une nouvelle clé à chaque heure (puisque les opérations se font au
maximum avec l'heure) !
Pour que ça fonctionne, on pourrait évidemment regarder la clé générée à partir du debugger,
la recopier dans le keyfile puis relancer le crackme, mais c'est une manipulation qui fait perdre beaucoup de temps.
C'est pourquoi je pense que créer un keygen n'est pas de trop.
J'ai mis ci-dessous un petit keygen en JavaScript pour vous montrer à quoi cela ressemblerait
(vous pouvez jeter un oeil à la source de cette page pout voir l'algo).
Il ne reste plus qu'à copier la clé dans le keyfile, et lancer le crackme avant expiration !
On va maintenant s'attaquer à la deuxième grande partie, à savoir la vérification du code.
Comme celui-ci fait 31 caractères, ce sera assez long. En fait, la vérification est divisée en
plusieurs petites routines que nous allons détailler ci-après.
Ces 4 premiers caractères font l'objet de quelques 70 lignes de code à eux seuls ! Mais ne nous laissons
pas impressionner par le nombre car c'est bien connu, avec l'ASM on obtient vite des listings phénoménaux pour finalement
pas grand chose...
Voyons voir ce que cela donne.
00401142 XOR EAX,EAX 00401144 XOR EBX,EBX 00401146 XOR ECX,ECX 00401148 XOR EDX,EDX 0040114A MOV EDX,crkmetri.004022AA <-- edx = le code 0040114F MOV BYTE PTR DS:[EDX+4],0 <-- isole les 4 premiers caractères 00401153 MOVZX EAX,BYTE PTR DS:[EDX] <-- boucle : convertit chaque caractère en entier 00401156 CMP AL,0 00401158 JE SHORT crkmetri.00401164 0040115A SUB AL,30 0040115C ROL ECX,8 0040115F MOV CL,AL 00401161 INC EDX 00401162 JMP SHORT crkmetri.00401153 00401164 ROL ECX,8 00401167 MOVZX EBX,CL 0040116A IMUL EBX,EBX,10 0040116D MOV AL,BL 0040116F ROL ECX,8 00401172 ADD AL,CL 00401174 ROL EAX,8 00401177 ROL ECX,8 0040117A MOVZX EBX,CL 0040117D IMUL EBX,EBX,10 00401180 MOV AL,BL 00401182 ROL ECX,8 00401185 ADD AL,CL <-- eax = les 4 premiers chiffres du code
Vous vous demandez peut-être à quoi servent ces quelques instructions.
En fait elles effectuent juste un formatage des données.
Si les 4 premiers caractères sont
'1',
'2',
'3' et
'4', on obtient quelque chose comme cela en hexadécimal :
31323334
,
ce qui correspond au code des caractères. Eh bien ce code est transformé pour arriver à ceci :
00001234
.
Facile, non ?
Nous pouvons maintenant nous intéresser à la suite.
00401187 XOR EBX,EBX 00401189 XOR ECX,ECX 0040118B XOR EDX,EDX 0040118D MOV DL,AL <-- prend les 3ème et 4ème chiffres du code 0040118F MOV AL,0 00401191 CMP DL,10 00401194 JL SHORT crkmetri.0040119D 00401196 SUB DL,10 <-- boucle : garde uniquement le chiffre des unités 00401199 ADD AL,0A <-- augmente al d'autant de fois A que l'on a d'itérations nécessaires 0040119B JMP SHORT crkmetri.00401191 0040119D ADD AL,DL <-- ajoute le chiffre des unités à al 0040119F ADD BL,AL <-- ebx = al (on conserve ce résultat pour plus tard) 004011A1 MOV AL,0 004011A3 ROL EAX,8 004011A6 ROL EAX,8 004011A9 ROL EAX,8 004011AC MOV DL,AL <-- prend les 1er et 2ème chiffres du code 004011AE MOV AL,0 004011B0 CMP DL,10 004011B3 JL SHORT crkmetri.004011BC 004011B5 SUB DL,10 004011B8 ADD AL,0A 004011BA JMP SHORT crkmetri.004011B0 004011BC ADD AL,DL <-- procède de la même manière que précédemment 004011BE IMUL EAX,EAX,64 <-- mutiplie ce résultat par 64 004011C1 ADD BX,AX <-- ajoute le résultat à ebx 004011C4 XOR EAX,EAX 004011C6 IMUL EBX,EBX,5 <-- multiplie le tout par 5 004011C9 PUSH EBX <-- pose le résultat 004011CA PUSH crkmetri.00402000 004011CF PUSH crkmetri.004022EE <-- pose un buffer 004011D4 CALL crkmetri.00401696 <-- wsprintfA : formate ce nombre en chaîne de caractères 004011D9 ADD ESP,0C 004011DC CALL crkmetri.00401620 <-- protection anti-debugger 004011E1 XOR EAX,EAX 004011E3 XOR EBX,EBX 004011E5 XOR ECX,ECX 004011E7 XOR EDX,EDX 004011E9 MOV EAX,crkmetri.004022AA <-- eax = le code 004011EE MOV EDX,crkmetri.004022EE <-- edx = le nombre créé jute avant 004011F3 MOV CL,BYTE PTR DS:[EAX+2] <-- ecx = le 3eme caractère du code 004011F6 CMP CL,BYTE PTR DS:[EDX+1] <-- teste si le 3eme caractère est égal au 2ème caractère du nombre créé 004011F9 JNZ crkmetri.004015DB <-- si ce n'est pas le cas, affiche le nag
Même si le nombre de lignes peut paraître impressionnant au premier abord, nous allons comme d'habitude essayer
d'en tirer l'essentiel.
Visiblement, un nombre est créé à partir des 4 premiers chiffres du code, puis un test est effectué à partir
de ce nombre, une fois que celui-ci a été formaté en caractères.
Intéressons-nous tout d'abord à la manière dont le nombre est généré.
Si l'on suit attentivement les instructions, on arrive à cette opération-là :
nombre = ((A * 1er chiffre + 2ème chiffre) * 64 + A * 3ème chiffre + 4ème chiffre) * 5
.
Ceci peut être déroutant au premier abord, mais essayons donc de simplifier... Passons tout déjà
en décimal pour que ce soit plus clair :
nombre = ((10 * 1er chiffre + 2ème chiffre) * 100 + 10 * 3ème chiffre + 4ème chiffre) * 5
.
Si l'on applique ensuite la distributivité :
nombre = (1000 * 1er chiffre + 100 * 2ème chiffre + 10 * 3ème chiffre + 4ème chiffre) * 5
.
Au final, on obtient tout simplement :
nombre = 4 premiers chiffres * 5
Eh oui, il s'agit d'une bête multiplication par 5 ! Tout ce travail pour en arriver là !
On en déduit que l'auteur (Fnaax) possède un certain sens de l'humour...
Il ne nous reste plus qu'un détail à régler : le test conditionnel à la fin, qui exige que le 3ème caractère du
code soit égal au deuxième caractère du nombre, donc de sa multiplication par 5 si vous avez bien suivi !
Aucun problème, il y a des tonnes de possibilités : prenez 1000 par exemple, multipliez-le par 5, on arrive à 5000, les
conditions sont bien remplies !
Pour ma part j'ai essayé 2003 et ça marchait !
On peut faire un keygen brute force, puisque je ne vous en ai pas montré pour l'instant.
Ne vous inquiétez pas si quelques secondes s'écoulent avant l'affichage des chiffres, c'est normal ;-)
Donc pour récapituler nous avons actuellement un code possédant ce format :
2003xxxxxxxxxxxxxxxxxxxxxxxxxxx.
C'est déjà pas mal mais on est encore loin du compte, puisque 27 caractères restent encore inconnus !
On se retrouve ensuite dans une petite routine qui va effectuer des opérations sur 5 autres caractères.
004011FF MOV EAX,crkmetri.004022AF 00401204 MOV DL,BYTE PTR DS:[EAX] <-- dl = 6ème caractère 00401206 SUB DL,BYTE PTR DS:[EAX+4] <-- dl = dl - 10ème caractère 00401209 ADD DL,BYTE PTR DS:[EAX+1] <-- dl = dl + 7ème caractère 0040120C CMP DL,BYTE PTR DS:[EAX+3] <-- compare dl au 9ème caractère 0040120F JNZ crkmetri.004015DB <-- si ils sont différents, affiche le nag 00401215 MOV DL,BYTE PTR DS:[EAX] <-- dl = 6ème caractère 00401217 SUB DL,2 <-- dl = dl - 2 0040121A MOV CL,BYTE PTR DS:[EAX+2] <-- cl = 8eme caractère 0040121D ADD CL,2 <-- cl = cl + 2 00401220 CMP CL,DL <-- compare dl et cl 00401222 JNZ crkmetri.004015DB <-- si ils sont différents, affiche le nag 00401228 CMP CL,BYTE PTR DS:[EAX+4] <-- compare cl et le 10ème caractère 0040122B JNZ crkmetri.004015DB <-- si ils sont différents, affiche le nag
On peut en tirer plusieurs informations.
Au niveau du premier test, on trouve :
9ème caractère = 6ème caractère - 10ème caractère + 7ème caractère
.
De plus, les égalités :
8ème caractère = 6ème caractère - 4
et :
10ème caractère = 8ème caractère + 2
doivent être vraies.
Compliqué tout ça, n'est-ce pas ?
On va effectuer quelques remplacements :
9ème caractère = 6ème caractère - ((6ème caractère - 4) + 2) + 7ème caractère
.
On peut à présent trouver des valeurs, si nous fixons le 6ème caractère avec la valeur
'6'
et le 7ème avec la valeur
'7'
(pour faire simple), on arrive à cela :
9ème caractère = 6 - ((6 - 4) + 2) + 7 = 6 - 4 + 7 = 9
(en plus le chiffre correspond à sa position ;-)),
8ème caractère = 6 - 4 = 2
,
et
10ème caractère = 2 + 2 = 4
.
Nous pouvons donc affiner notre shéma de code comme cela :
2003x67294xxxxxxxxxxxxxxxxxxxxx.
Là on commence à avoir l'habitude, hein ;-). Regardons donc le code.
00401231 ADD EAX,7 00401234 MOV BYTE PTR DS:[EAX-1],0 00401238 MOV BYTE PTR DS:[EAX+7],0 <-- isole la chaîne 0040123C MOVZX EDX,BYTE PTR DS:[EAX] <-- boucle : effectue des opérations sur chacun des caractères (edx) 0040123F TEST EDX,EDX 00401241 JE SHORT crkmetri.00401259 00401243 ADD EDX,3 <-- edx = edx + 3 00401246 XOR EDX,3 <-- edx = edx xor 3 00401249 SUB EDX,3 <-- edx = edx - 3 0040124C MOV BYTE PTR DS:[EAX],DL <-- on remet le caractère modifié dans son buffer 0040124E INC EAX 0040124F JMP SHORT crkmetri.0040123C 00401251 CMP BYTE PTR SS:[ECX],DH <-- inutile 00401254 XOR EAX,363734 <-- ... 00401259 SUB EAX,7 0040125C PUSH EAX 0040125D PUSH EAX <-- pose le nombre généré 0040125E PUSH crkmetri.00401251 <-- pose une constante ("6815476") 00401263 CALL crkmetri.004016C6 <-- lstrcmpA : compare le nombre à la constante ! 00401268 TEST EAX,EAX 0040126A JNZ crkmetri.004015DB <-- si ils sont différents, affiche le nag
Cette partie est très simple, puisque l'on prend chaque caractère de la chaîne, sur lesquels on effectue un petit cryptage
en xor, et on compare ensuite le tout avec une constante.
Pour retrouver la bonne combinaison, il nous suffit d'effectuer l'opération inverse à partir de la constante.
Voici l'équivalent JavaScript de cette opération :
(((6 + 3) ^ 3) - 3).toString() + (((8 + 3) ^ 3) - 3).toString() + (((1 + 3) ^ 3) - 3).toString() + (((5 + 3) ^ 3) - 3).toString() + (((4 + 3) ^ 3) - 3).toString() + (((7 + 3) ^ 3) - 3).toString() + (((6 + 3) ^ 3) - 3).toString()
.
On obtient au final la chaîne 7548167, ce qui nous permet de continuer notre code :
2003x67294xx7548167xxxxxxxxxxxx.
Allez courage, on est bientôt à la fin !
00401270 CALL crkmetri.00401620 <-- protection anti-debugger 00401275 POP EAX 00401276 XOR ECX,ECX 00401278 ADD EAX,9 <-- eax = la suite du code 0040127B MOVZX EBX,BYTE PTR DS:[EAX] <-- boucle : fait la somme des 10 derniers caractères 0040127E ADD CX,BX 00401281 INC EAX 00401282 CMP BYTE PTR DS:[EAX],0 00401285 JNZ SHORT crkmetri.0040127B 00401287 CMP ECX,207 <-- teste si cette somme est égale à 207 0040128D JNZ crkmetri.004015DB <-- si ce n'est pas le cas, affiche le nag
Très bien, il suffit que la somme des 10 derniers bytes soit égale à 207 en héxadécimal !
Après quelques essais, j'ai remarqué que la chaîne 4444444443 convenait parfaitement !
On va compléter de suite notre code :
2003x67294xx7548167xx4444444443
.
Notre code est-il terminé ? Non, car il reste encore des blancs (représentés par la lettre 'x' dans notre shéma).
Comme nous ne savons pas quoi en faire, on va regarder la suite (et fin) du listing, bien entendu !
00401293 PUSH 32 00401295 PUSH crkmetri.004022AA <-- pose le buffer de code 0040129A PUSH 70 0040129C PUSH DWORD PTR SS:[EBP+8] 0040129F CALL crkmetri.004016C0 <-- GetDlgItemTextA : récupère le code 004012A4 MOV EAX,crkmetri.004022AA 004012A9 XOR ECX,ECX 004012AB MOVZX EBX,BYTE PTR DS:[EAX] <-- boucle : fait la somme de tous les caractères 004012AE ADD CX,BX 004012B1 INC EAX 004012B2 CMP BYTE PTR DS:[EAX],0 004012B5 JNZ SHORT crkmetri.004012AB 004012B7 CMP ECX,65B <-- teste si ette somme est égale à 65B 004012BD JNZ crkmetri.004015DB <-- si ce n'est pas le cas, affiche le nag 004012C3 JMP SHORT crkmetri.004012DC <-- sinon saute vers good boy !!
Ceci ressemble fortement à l'étape précédente, mais cette fois-ci c'est la somme de
tous
les caractères du code qui doit être égale à un nombre, en l'occurrence 65B.
Il faut en fait ajuster les blancs pour que le compte soit bon.
Après un petit moment de réflexion (merci JavaScript et la calculatrice Windows !), je suis arrivé à cela :
2003567294557548167594444444443
.
C'est le code final et il fonctionne, vous pouvez le tester, le message Key and Code are OK :) s'affiche !
On y est arrivés !!
Bien sûr, il y a plein d'autres codes possibles mais maintenant que vous connaissez le principe, cela
ne devrait pas trop poser de problèmes pour en trouver !
On pourrait aussi faire un keygen (ou plutôt un codegen ;-)), mais là je vous laisse vous débrouiller !
Vous avez toutes les clés en main !
Le crackme de Fnaax est effectivement long, mais comme vous l'avez vu, c'est possible !
Quand je pense que le 8 Juin j'étais sur le forum de la team en croyant que l'on ne pouvait pas trouver de
code valide tellement il y avait d'ambigüités !
Il faut avoir la volonté de réussir et c'est ceci le plus important.
Voilà, c'était mon premier tuto sur le cracking et j'espère que vous avez apprécié !
Je remercie particulièrement RocketSpawn pour son accueil, ses encouragements, et ses conseils.
Sans lui, je ne serais peut-être pas en train d'écrire ces mots !
Merci aussi à Eeddy31 qui m'a tout de suite répondu sur le forum quand j'avais posé ma question au sujet
de Trial crackme.
Et enfin, je passe le bonjour à tous les membres de la Iciteam !
Bonne journée,
Canterwood - 11 Juin 2003 (modifié le 23 Décembre 2003 pour publication)