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)