Bon, reprenez votre suffle parce que la ça va être plus
dur. Il s'agit d'un crack en pur Win32Asm codé par THE_q de
la PhroZen Crew. Alors comme d'hab on le lance, on entre un nom bidon
(Sirius) et un code bidon (666) ; petit problème : quand on valide
il se passent que dalle donc on se dit merde, des complications... Et
bien nom, on désassemble avec Win32Dasm, on va dans les String
Data References et on voit "Cool !! U found a correct Serial ! =)".
On regarde quand est appelé ce texte , d'où on vient etc.
On met des breaks avant, histoire de voir se qui se passe et puis au boût
d'un moment on en tire le boût de programme suivant :
:004012C4 6A65 push 00000065
:004012C6 FF7508 push [ebp+08]
* Reference To: USER32.GetDlgItem, Ord:0000h
|
:004012C9 E852010000 Call 00401420
:004012CE 68A9214000 push 004021A9
:004012D3 6A28 push 00000028
:004012D5 6A0D push 0000000D
:004012D7 50 push eax
* Reference To: USER32.SendMessageA, Ord:0000h //
API interessante
|
:004012D8 E873010000 Call 00401450
:004012DD A2D1214000 mov byte ptr [004021D1], al
:004012E2 3C00 cmp al, 00 // tient ça ressemble
:004012E4 7467 je 0040134D
:004012E6 90 nop
:004012E7 90 nop
:004012E8 90 nop
:004012E9 90 nop
:004012EA 3C08 cmp al, 08 // à quelque chose
qu'on a vu tout a l'heure
:004012EC 7309 jnb 004012F7
:004012EE 90 nop
:004012EF 90 nop
:004012F0 90 nop
:004012F1 90 nop
:004012F2 E85F000000 call 00401356
* Referenced by a (U)nconditional or (C)onditional Jump
at Address:
|:004012EC(C)
|
:004012F7 6A00 push 00000000
:004012F9 6A00 push 00000000
* Possible Reference to Dialog: DialogID_0001, CONTROL_ID:0066,
""
|
:004012FB 6A66 push 00000066
:004012FD FF7508 push [ebp+08]
* Reference To: USER32.GetDlgItemInt, Ord:0000h //
Interessant
|
:00401300 E821010000 Call 00401426
:00401305 A3D2214000 mov dword ptr [004021D2], eax
:0040130A 3C00 cmp al, 00
:0040130C 743F je 0040134D
:0040130E 90 nop
:0040130F 90 nop
:00401310 90 nop
:00401311 90 nop
:00401312 E85E000000 call 00401375
:00401317 E8A5000000 call 004013C1
:0040131C 85C0 test eax, eax // On teste eax
:0040131E 742D je 0040134D // si c'est egal à
0 on saute
:00401320 90 nop
:00401321 90 nop
:00401322 90 nop
:00401323 90 nop
:00401324 6A00 push 00000000
* Possible StringData Ref from Data Obj ->"Correct
!" // sinon c'est que le pass est bon
|
:00401326 689F214000 push 0040219F
* Possible StringData Ref from Data Obj ->" C00L
!! U found a correct "
->"Serial ! =)"
|
:0040132B 683A214000 push 0040213A
:00401330 FF7508 push [ebp+08]
* Reference To: USER32.MessageBoxA, Ord:0000h
|
:00401333 E806010000 Call 0040143E
On débugge le prog, la première API (GetDlgItem) ne nous interesse pas à priori. Pour la seconde (SendMessage) plusieurs paramètres sont passés dont une adresse en mémoire : 004021A9. On met un break avant et après le call puis on va voir en mémoire ce qui a été mis en 4021A9. On clique sur data hex puis on trouve :
:00402178 79 20 65 2D 6D 61 69 6C y e-mail
:00402180 20 74 6F 20 3A 20 50 68 to : Ph
:00402188 72 6F 7A 65 6E 5F 71 40 rozen_q@
:00402190 63 79 62 65 72 64 75 64 cyberdud
:00402198 65 2E 63 6F 6D 20 00 43 e.com .C
:004021A0 6F 72 72 65 63 74 20 21 orrect !
:004021A8 00 53 69 72 69 75 73 00 .Sirius.
:004021B0 8F 00 00 00 00 00 00 00 ........
:004021B8 00 00 00 00 00 00 00 00 ........
:004021C0 00 00 00 00 00 00 00 00 ........
:004021C8 00 00 00 00 00 00 00 00 ........
:004021D0 00 06 9A 02 00 00 50 68 ......Ph
:004021D8 72 6F 5A 65 6E 51 00 00 roZenQ..
:004021E0 00 00 00 00 00 00 00 00 ........
:004021E8 00 00 00 00 00 00 00 00 ........
:004021F0 00 00 00 00 00 00 00 00 ........
:004021F8 00 00 00 00 00 00 00 00 ........
On voit bien notre login (Sirius) à l'adresse prévu mais on remarque aussi la chaîne "PhroZenQ" qui est sans doute ici pour encrypter notre login ou notre pass plus tard. On comprend aussi que la longeur du nom qu'on entre est renvoyé dans eax (6 dans notre cas). Ensuite le prog vérifie si on a entré quelque chose. Si ya rien on se casse. Ensuite on regarde si le login a au moins 8 caractères, si c'est la cas le login est validé sinon on appelle le call 00401356.
A partir de maintenant je designerais cette procédure comme le PRE-CALL. Que fait-il ? Et bien il modifie notre login si ce dernier a une longueur comprise entre 1 et 7 inclus. Voici le call :
* Referenced by a CALL at Address:
|:004012F2
|
:00401356 33DB xor ebx, ebx // met ebx à
zero
:00401358 8A1DD1214000 mov bl, byte ptr [004021D1]//la
longueur de notre login
:0040135E B108 mov cl, 08 // cl=8;
:00401360 2ACB sub cl, bl // cl = 8 - longeur(login)
:00401362 BFA9214000 mov edi, 004021A9 // l'adresse
de notre login
:00401367 6603FB add di, bx // edi = edi + bl
:0040136A B030 mov al, 30 // al est initialisée
à 30 en hexa soit '0'
:0040136C AA stosb // je vais vous expliquer ça
:0040136D FEC0 inc al // on incrémente al
:0040136F E2FB loop 0040136C // boucle vers 0040136C
:00401371 C60700 mov byte ptr [edi], 00 // met 0
dans edi
:00401374 C3 ret
Bon, petit cours de ratrapage pour les nuls :
Avec les progrès des processeurs, les registres ont gagnés
en taille. Maintenant les programmes sont codés en 32 bits soit
4 octets. 4 octets c'est la taille qui sert pour désigner une adresse
en mémoire (par exemple 004021A9). Les registres comme ebx prennent
eux aussi 4 octets mais à une époque ils ne prenaient que
2 octets, on avait alors droit au registre bx qui est l'ancètre
de ebx. En fait on a juste augmenté sa taille mais les ancien noms
on subsistés et serve toujours (ils sont d'ailleurs très
pratique). Peit exemple avec ebx
EBX
(32 bits)
|
||
---|---|---|
BX
(16 bits)
|
||
BH
(8)
|
BL
(8)
|
Bref, quand on parle de EBX, on parle aussi de BX (sa moitié) puis de BH (H comme high) et BL (L comme low). Ainsi si on a EBX = 0040A9B8 et que l'on fait un mov bx, 4D56 alors on aura EBX = 00404D56. C'est pareil pour di (16 bits) qui est passé à edi (32 bits). En gros les registres de 32 bits commencent par un 'e'.
Maintenant analysons le PRE-CALL. Au début on met EBX à
0.
On a donc EBX = 00000000 (vu qu'il fait 32 bits).
Ensuite on met dans bl (registre de 8 bits soit un octet) la longueur
sur un octet de notre login.
Comme bl est une partie de EBX, on a EBX = 000000XX où XX est la
longueur de notre chaine en hexa.
Puis cl initialisé à 8. Ensuite on retranche la longueur
de notre login à cl.
mov edi, 004021A9 signifie que edi prend l'adresse de notre login (à
ne pas confondre avec la valeur). On dit que edi "pointe" sur
le premier caractère de la chaine. di est ensuite augmenté
de bx (la longueur) : di pointe donc maintenant sur le caractère
de fin de chaine (après le dernier caractère).
L'instruction stosb je vais vous l'expliquer en dernier.
Inc al est l'équivalent de al = al + 1;
Loop Adresse est une boucle qui retourne à Adresse tant
que ecx n'est pas égal à 0. A chaque fois que l'on rencontre
Loop, ecx est décrémenté.
mov byte ptr[edi], 00 signifie que l'on met dans l'octet pointé
par edi la valeur 0.
L'instruction stosb est une instruction très utilisé pour
les chaines de caractères. A chaque fois que l'on crois stosb,
le contenu de al est mis dans l'octet pointé par edi, puis edi
est incrémenté.
Dans notre cas, edi pointe d'abord sur la fin de la chaine et al vaut
30 soit le caractère '0'.
On passe par stosb, '0' se retrouve donc à la fin de notre chaine
(on a maintenant 'Sirius0').
al est ensuite incrémenté (al devient le caractère
'1') puis on fait un loop. On en déduit que ecx est décrémenté.
Or ecx est la valeur 8 - longueur(login). C'est à dire que l'opération
va se répeter jusqu'à ce que notre login ai atteint 8 caractères
et ce en ajoutant à chaque fois '0', '1', '2'... Avec Sirius on
obtient Sirius01. Avec SB on aurrait eu SB012345.
Bon c'est fini pour le PRE-CALL. Si on regarde l'algo que je vous ai mis au début, maintenant on passe par l'API GetDlgItemInt qui comme son nom l'indique lit un entier. Après quelques essai on se rend compte que cette fonction ne renvoie une valeur qui si on lui donne un chiffre. Comme d'habitude la valeur de retour est dans eax. Les infos des API nous donnent :
API UINT Arg00 = GetDlgItemInt(Arg01,Arg02,Arg03,Arg04)
API Address=00401426, API Return Address=00401305
Arg01 = (HWND) 00000d30 (Window"Register")
Arg02 = (int) 00000066
Arg03 = (BOOL *) 00000000
Arg04 = (BOOL) 00000000
RESULT for API GetDlgItemInt
Arg00 = (UINT) 0000029a
Arg01 = (HWND) 00000d30 (Window"Register")
Arg02 = (int) 00000066
Arg03 = (BOOL *) 00000000
Arg04 = (BOOL) 00000000
Où 29a est bien évidemment la traduction hexa de notre 666. On fait un petit tour dans la mémoire pour voir où en sont les données :
:00402178 79 20 65 2D 6D 61 69 6C y e-mail
:00402180 20 74 6F 20 3A 20 50 68 to : Ph
:00402188 72 6F 7A 65 6E 5F 71 40 rozen_q@
:00402190 63 79 62 65 72 64 75 64 cyberdud
:00402198 65 2E 63 6F 6D 20 00 43 e.com .C
:004021A0 6F 72 72 65 63 74 20 21 orrect !
:004021A8 00 53 69 72 69 75 73 30 .Sirius0
:004021B0 31 00 00 00 00 00 00 00 1.......
:004021B8 00 00 00 00 00 00 00 00 ........
:004021C0 00 00 00 00 00 00 00 00 ........
:004021C8 00 00 00 00 00 00 00 00 ........
:004021D0 00 06 9A 02 00 00 50 68 ......Ph
:004021D8 72 6F 5A 65 6E 51 00 00 roZenQ..
:004021E0 00 00 00 00 00 00 00 00 ........
:004021E8 00 00 00 00 00 00 00 00 ........
:004021F0 00 00 00 00 00 00 00 00 ........
:004021F8 00 00 00 00 00 00 00 00 ........
Vous pouvez remarquer que notre chiffre est mis à l'envers dans
la mémoire, c'est comme ça pour tous les chiffres. Donc
si ce que l'on a entré lui convient il vas vers les deux call juste
avant le test final sinon il se barre. Encore une fois on voit que ce
serait très facile de patcher le test final mais c'est moins intéressant
;-). J'ai appelé les deux calls respectivement call1 et
call2.
Etudions le premier call :
* Referenced by a CALL at Address:
|:00401312
| // on initialise
:00401375 33C9 xor ecx, ecx // on met ecx à
0
:00401377 BEA9214000 mov esi, 004021A9 // esi pointe
vers notre chaine (Sirius01)
:0040137C 8BFE mov edi, esi // edi aussi
* Referenced by a (U)nconditional or (C)onditional Jump
at Address:
|:00401391(C)
|
:0040137E AC lodsb
:0040137F F7D1 not ecx
:00401381 8A99B1214000 mov bl, byte ptr [ecx+004021B1]
:00401387 F7D1 not ecx
:00401389 02C3 add al, bl
:0040138B AA stosb
:0040138C FEC1 inc cl
:0040138E 80F904 cmp cl, 04
:00401391 75EB jne 0040137E
:00401393 B904000000 mov ecx, 00000004
:00401398 BEA9214000 mov esi, 004021A9
:0040139D 8BFE mov edi, esi
:0040139F 83C704 add edi, 00000004
:004013A2 F2 repnz
:004013A3 A4 movsb
:004013A4 BEA9214000 mov esi, 004021A9
:004013A9 8BFE mov edi, esi
:004013AB B908000000 mov ecx, 00000008
:004013B0 AC lodsb
:004013B1 F7D1 not ecx
:004013B3 8A99DF214000 mov bl, byte ptr [ecx+004021DF]
:004013B9 F7D1 not ecx
:004013BB 32C3 xor al, bl
:004013BD AA stosb
:004013BE E2F0 loop 004013B0
:004013C0 C3 ret
Ce que j'ai mis en bleu, ce sont les deux boucles principales de notre
call1. Voyons la première boucle :
lodsb est à peut près ce que fait stosb mais en sens inverse.
Cette instruction remplie al avec l'octet pointé par esi.
not ecx permet de complémenter ecx. On peut considérer cela
comme un calcul de l'inverse. A quoi ça va servir ? et bien en
fait pour faire des soustractions. En fait soustraire 3, c'est comme ajouter
-3. De même soustraire 00000001 serait comme ajouter fffffffe. Donc
que fait notre boucle ? Dès le début, elle met dans al le
premier caractère de notre chaine et esi est incrémenté
(comme stosb faisait avec edi).
Puis ecx est complémenté. ecx a été précedemment
fixé à zéro donc là il prend la valeur ffffffff.
004021B1 est le pointeur vers le dernier caractère de notre chaine
('1').
L'instruction mov bl, byte ptr [ecx+004021B1] signifie que bl prend la
valeur sur un octet de la valeur pointé par (dernier caractère
- 0). Donc bl prend le caractère '1'. Le premier caractère
(al) et le dernier (bl) sont ensuite additionné
Juste après ecx est complémenté de nouveau pour reprendre
sa valeur d'avant.
Avec stosb, notre premier caractère est remplacé par le
premier + le dernier = la valeur hexa de 'S' + '1'.
cl est ensuite incrémenté. On recommence cette boucle tant
qu'il n'est pas égal à 4 (la moitié de notre chaine).
ecx vaut donc maintenant 1. On en déduit que al vas prendre la valeur login[1] en considérant que login[0] correspond au premier caractère et login[n] au dernier. En fait notre boucle calcule à chaque fois login[ecx] + login[n-ecx] jusqu'à ce que ecx vaut 4. On va donc calculer la somme du premier et du 8ième, du second et du septième, du troisième et du sixième puis de quatrième avec le 5ième. A chaque fois, par l'intermédiaire de stosb, la somme est mise à la place de notre chaine d'origine. En C, cela nous donne :
for (i=0;i!=4;i++){ login[i]=login[i]+login[8-i]; }
Et on obtient :
:004021A0 6F 72 72 65 63 74 20 21 orrect !
:004021A8 00 84 99 E5 DE 75 73 30 .....us0
:004021B0 31 00 00 00 00 00 00 00 1.......
:004021B8 00 00 00 00 00 00 00 00 ........
:004021C0 00 00 00 00 00 00 00 00 ........
:004021C8 00 00 00 00 00 00 00 00 ........
:004021D0 00 06 9A 02 00 00 50 68 ......Ph
Donc voilà notre code à la fin de la première boucle
: DEE59984 (je vous rappelle que les chiffres sont stockés à
l'envers).
Les quelques isnstructions entre les deux boucles provoquent les calculs
suivants :
esi pointe sur les 4 premiers octets du login (DEE59984) et edi sur les
4 derniers ('us01'). Puis le premier octet de edi prend la valeur du premier
octet de esi, ensuite on recommence jusqy'à ce que ecx soit nul
(instructions repnz et movsb). Et comme ecx était initialisé
à 4 on obtient :
:004021A0 6F 72 72 65 63 74 20 21 orrect !
:004021A8 00 84 99
E5 DE 84 99
E5 ........
:004021B0 DE 00 00 00 00 00 00 00 ........
:004021B8 00 00 00 00 00 00 00 00 ........
:004021C0 00 00 00 00 00 00 00 00 ........
:004021C8 00 00 00 00 00 00 00 00 ........
:004021D0 00 06 9A 02 00 00 50 68 ......Ph
La seconde boucle consiste à faire un xor de tout ça avec
la chaine 'PhroZenQ'. Le résultat restant toujours à la
même place. On a :
:004021A0 6F 72 72 65 63 74 20 21 orrect !
:004021A8 00 D4 F1 97 B1 DE FC 8B ........
:004021B0 8F 00 00 00 00 00 00 00 ........
:004021B8 00 00 00 00 00 00 00 00 ........
:004021C0 00 00 00 00 00 00 00 00 ........
:004021C8 00 00 00 00 00 00 00 00 ........
:004021D0 00 06 9A 02 00 00 50 68 ......Ph
Bon jusque là, ça allait... Mais là on tombe sur
un truc de paranoïaque obsessionnel gravement atteint au LSD. J'ai
passé des mois sur cette putain de routine sans réussir
à voir comment l'inverser. Finalement je me suis décidé
pour regarder l'explication avec le crakme mais g pas vraiment compris
comment ça marche. Apparemment c'est tellement bien foutu que l'on
peut pas faire un simple script qui transforme notre login en serial,
il faut utiliser une méthode brute force pour une partie de la
routine... Enfin bon voici donc le call2 :
* Referenced by a CALL at Address:
|:00401317
|
:004013C1 A1D2214000 mov eax, dword ptr [004021D2] ;
le code que l'on a entre (666)
:004013C6 8A0DA9214000 mov cl, byte ptr [004021A9] ;
premier octet du code genere (D4)
:004013CC D3C0 rol eax, cl ; :-( ca commence mal
:004013CE 8B0DAE214000 mov ecx, dword ptr [004021AE] ;3
derniers octets du code (00 8F 8B FC)
:004013D4 33C1 xor eax, ecx
:004013D6 8B1DAA214000 mov ebx, dword ptr [004021AA] ;
milieu du code (DE B1 97 F1)
:004013DC 8AC8 mov cl, al
:004013DE D3CB ror ebx, cl
:004013E0 33C3 xor eax, ebx ; ZE Big Test, le xor
renvoie 0 en cas de succès
:004013E2 B800000000 mov eax, 00000000 ; eax est
mis à 0
:004013E7 7506 jne 004013EF ; si les valeurs ne
sont pas les memes on quitte avec eax=0
:004013E9 90 nop
:004013EA 90 nop
:004013EB 90 nop
:004013EC 90 nop
:004013ED FEC0 inc al ; sinon c la fête, on
quitte avec eax = 1 --> "COOL !!..."
* Referenced by a (U)nconditional or (C)onditional Jump
at Address:
|:004013E7(C)
|
:004013EF C3 ret
Explication : On commence par récupérer le code que l'on
a entré dans eax.
Ensuite on "rol" ce code avec le premier octet du code généré.
Le rol c'est un roulement à gauche (left). C'est à dire
que l'on va décaler les bits vers la gauche en ce en bouclant (les
bits qui sorte à gauche se retrouvent à droite). Voici un
ptit exemple :
mov AH, C0 ; AH = 1100 0000
rol AH, 1 ; on décale les bits de AH d'un
cran sur la gauche --> AH = 1000 0001
mov CL, 2
rol AH, CL ; on décale les bits de CL cran
sur la gauche --> AH devient 0000 0110
Une fois que le roulement est fait, le résultat est "xoré"
avec les 3 derniers octets de notre code. En fait il en prend quatre vu
qu'il demande un dword mais le quatrième est à 00. Jusque
là rien de spécial : on fait des calculs sur le pass que
l'on a entré, notre code généré est lui intact.
Et là... C'est le drame ;-)
Une partie de notre code généré (octets 2 à
5) est mise dans ebx puis "roré" avec le pass modifié.
Ces deux putains de code se crypent l'un avec l'autre (et vise et versa
:-).
Et puis histoire de nous embrouiller encore un peu plus, on fait en xor
mais dans le sens inverse : notre code généré sur
notre code entré. Et alors si par une quelconque magie ce sont
les mêmes alors on ne fait pas le jump, eax est mis à 1.
On retourne dans notre routine de début, on teste eax est si il
vaut 1 alors c gagne.
C'est plutôt difficile à expliquer mais essayez de touver
comme ça marche et vous verrez que c'est super chaud.
La bonne chose c'est que les concepteurs de logiciels ne mettent pas des
tests aussi compliqué que celui là donc le keygening a encore
de beaux jours devant lui. Et le patching encore plus car ce prog est
l'exemple même de comment passer une putain de protection à
la con en modifiant deux ou trois octets.