Devilz's Keygenme5, Crc32 + Floating point operations reversing

Data

by Evilcry

 

07/07/2004

UIC's Home Page

Published by Quequero

Where ones sees a limit

Grazie tante evil, preciso come sempre. E voi non avete idea di quanto sia comoda la bswap :)))

the others sees an opporunity

Prepare for what you cannot see, expect the unexpected, a waiting game waiting to see......

WebSite: www.cryptorev.da.ru / http://abcd6.interfree.it
E-mail: [email protected] / [email protected]
IRC frequentato   irc.azzurranet.org   #cryptorev #crack-it - irc.efnet.nl #cracking4newbies #cryptology #win32asm
 

 

Difficoltà

NewBies () Intermedio (X) Avanzato () Master ()

 

 

Devilz's Keygenme5, Crc32 + Floating point operations reversing
Written by Evilcry.

Introduzione

In questo tutorial, andremo ad analizzare, reversare e keygennare il quinto keygenme di Devilz.

Tools usati

URL o FTP del programma

www.google.it

Notizie sul programma

Troverete (crackme+ keygen) tutto in nell' allegato.

Essay

 

Cominciamo col vedere di che tipo di protezione si tratta, immediatamente dopo l'apertura si presenta davanti a noi il classico schema Username/SerialNumber. Inserisco come username 'evilcry' e come serial 'triton', e poi setto un breakpoint su GetDlgItemTexA. Ecco cos'abbiamo di fronte:

004010AD push 0C8h
004010B2 push offset String ;00403940 è il buffer che conterrà il serial
004010B7 push 0C8h
004010BC push [ebp+hDlg]
004010BF call GetDlgItemTextA
004010C4 push 0C8h
004010C9 push offset byte_4039A4 ;4039A4 è il buffer che conterrà l'username
004010CE push 64h
004010D0 push [ebp+hDlg]

004010D3 call GetDlgItemTextA

004010D8 test al, al
004010DA jz loc_40122B ;Se l'username è nullo, salta al messaggio d'errore
004010E0 mov byte_403A08, al ;AL contiene la lunghezza dell'username
004010E5 call sub_401272 ;Call che analizzeremo più approfonditamente
004010EA test al, al ;Se il valore di ritorno della call è 0, allora salta al messaggio d'errore
004010EC jz loc_4011D6

004010F2 call sub_4012DC ;Altra call che analizzeremo in seguito

Passiamo ora ad analizzare cosa fanno le call 00401272 e 004012dc...

00401272 movsx eax, String ;Mette in eax primo carattere del serial che abbiamo inserito
00401279 xor eax, 65h ;Ne fa un xor con il valore 65
0040127C dec al ;decrementa il risultato dello xor
0040127E jnz short loc_4012D8 ;se il risultato è diverso da 0 allora salta al msg d'errore
00401280 movsx eax, byte_403941 ;Prossimi char del serial
00401287 shl eax, 3 ;Shifta a sinistra di 3
0040128A cmp eax, 328h ;e controlla che il risultato sia 328h
0040128F jnz short loc_4012D8 ;se diverso allora salta al msg d'errore
00401291 movsx eax, byte_403942
00401298 movsx ebx, byte_403943
0040129F add al, bl
004012A1 cmp al, 0DFh
004012A3 jnz short loc_4012D8 ;La somma dei 2 char del serial deve dare 0DFh
004012A5 movsx eax, byte_403943
004012AC shl eax, 1
004012AE cmp al, 0D2h
004012B0 jnz short loc_4012D8 ;Il risaultato dello shift dev'essere 0D2h
004012B2 movsx eax, byte_403944
004012B9 rol eax, 2
004012BC cmp eax, 1B0h
004012C1 jnz short loc_4012D8 ;Il risaultato del rol dev'essere 1B0h
004012C3 movsx eax, byte_403944
004012CA movsx ebx, byte_403945
004012D1 sub bl, al
004012D3 ror ebx, 1
004012D5 cmp bl, 7 ;Il risultato della sottrazione e successivo ror dev'essere 7
004012D8
004012D8 setz al ;Nel caso la condizione è TRUE setta al a 1, così all'uscita della call non ci sarà il jump al msg d'errore

004012DB retn

Come potete vedere il codice di questa call è semplicissimo, in poche parole controlla che i primi 6 caratteri del serial seguono determinate "condizioni". Penso sia inutile riscrivere quali sono le condizioni, quindi procediamo direttamente con il reverse (per altro sono operazioni semplicissime da reversare):

1°Char = Essendo (x Xor 65 - 1 = 0), basta prendere il valore prima di 65 cioè 64h (d)

2°Char = Shl x,3 = 328h quindi invertiamo l'operazione come Shr 328,3 cioè 65h (e)

3-4°Char = x + y = 0DFh in 2 secondi con la calcolatrice otteniamo 76h = (v) e 69h= (i)

5°Char = Shl x,1 = 0D2h invertiamo anche qui come Shr 0D2,1 = 7Ah = (l)

6°Char= Senza perdere tempo, i primi 5 bytes sono il nome dell'autore del crackme è devilz quindi devil(z) :)

Questa prima call è fatta, se i caratteri sono quelli giusti, setz dovrebbe settare AL=1. Procediamo ora con la prossima call 004012dc:

004012E1 movsx eax, ss:String[ebp] ;Ebp fa da indice di String (il vettore che contine il serial)
004012E8 test al, al
004012EA jz short loc_40130E ;Se la stringa è finita salta giù, alla prossima routine
004012EC shl eax, 1
004012EE cmp eax, 5Ah ;Il risultato dello shift dev'essere 5Ah
004012F1 jz short loc_40130B
004012F3 cmp eax, 60h
004012F6 jl short loc_401348 ;Se il risultato è minore di 60h allora salta
004012F8 cmp eax, 8Ch
004012FD jg short loc_401348 ;Salta anche se è maggiore di 8Ch
004012FF cmp eax, 82h
00401304 jge short loc_40130B ;Passa al prossimo carattere se invece il risultato è maggiore o uguale a 82h
00401306 cmp eax, 72h
00401309 jg short loc_401348
0040130B
0040130E loc_40130E:
0040130E shl ebp, 1 ;Shifta a sinistra il contatore (contine il numero di caratteri processati prima)
00401310 xor ebp, 45h ;Effettua lo xor di quello che ne esce con 45h
00401313 dec ebp ;Sottraigli 1
00401314 jnz short loc_401348 ;Se il risultato finale è diverso da 0 allora salta al msg d'errore

 

Il primo pezzo di routine controlla che i caratteri del serial che stanno immediatamente dopo 'devilz' rispettano alcune condizioni. Considerando che dobbiamo uscire da questo ciclo di check (004012E1-00401309) senza far saltare l'esecuzione fuori dal ciclo stesso, la cosa migliore è far sì che i caratteri diano 82h. Come prima facciamo:

Shl x,1= 82h, che revesato da Shr 82h,1= 41h (A)

Quindi comiciamo a mettere dopo devilz, una serie di A. Passiamo ora all'altra porzione di codice (da 0040130E per intenderci), attraverso la quale saremo in grado di determinare quale dev'essere la lunghezza del nostro serial. Per sapere quindi la lunghezza corretta vediam che

((Shl x,1) xor 45) - 1 = 0 che è molto semplice da reversare, considerando che il risultato dello xor dev'essere 1 (che poi sottratto da 0) prendiamo come secondo operando (dello xor sempre) 44h. Perciò

Shr 44,1 = 22h (34d)

capiamo facilmente che il serial dev'essere lungo 34. Con il prossimo pezzo di codice invece capiremo qual'è lo "scheletro" del nostro serial:

00401314 jnz short loc_401348
00401316 mov cl, byte_403950
0040131C mov bl, byte_403959
00401322 sub cl, bl ;sottrae tra loro due caratteri precisi del serial
00401324 jnz short loc_401348 ;se il risultato è diverso da 0 salta al msg d'errore
00401326 xor byte_403946, 2Dh ;il 6° carattere a partire da devilz, xorato per 2Dh
0040132D jnz short loc_401348 ;deve dare 0
0040132F mov bl, byte_40394B
00401335 rol ebx, 2
00401338 cmp bl, 0B4h ;il B° carattere Rol x,2=0B4h
0040133B jnz short loc_401348
0040133D mov bl, byte_403950 ;il carattere è lo stesso di quello spostato in cl alla locazione 00401316
00401343 xor bl, 2Ch
00401346 dec bl ;che xorato poi sottratto deve dare 0
00401348 setz al ;se tutte queste condizioni sono rispettate, si passa alla prossima routine, altrimenti msg d'errore
0040134B retn

 

2Dh corrisponde al trattino, perciò seguendo le direttive di questo pezzo di codice saremo in grado di costruire lo "scheletro" del nostro seriale valido. Ecco come dovrebbe risultarvi la forma di un serial corretto:

devilz-AAAA-AAAA-AAAAAAAA-AAAAAAAA

Per facilitare il discorso, chiameremo ogni sigolo pezzo (compreso tra due trattini) in modo diverso. I nomi che gli daremo saranno, PrimaWord, SecondaWord, PrimaDword e SecondaDword.

Se appunto mettiamo questo serial, dovremmo ottenere questo messaggio: "You are Halfway.... You Need To Progress :p"

Passiamo ora a vedere cosa succede dopo questa call. Innanzitutto possiamo notare la presenza di 4 call tutte le stesse, chiamate in successione:

0040110E call sub_40134C ;call che studieremo in seguito
...
00401123 call sub_40134C
...
00401138 call sub_40134C
...
0040114C call sub_40134C

 

Se notate le call sono quattro, proprio come i "pezzi" di serial. Infatti andando ad analizzare l'nterno di questa funzione:

0040134C xor ebx, ebx
0040134E movsx eax, ss:String[ebp] ;mette in eax un byte di uno specifico "pezzo" di serial
00401355 cmp al, 41h
00401357 jge short loc_401369 ;Controlla che il carattere non sia maggiore di 'A'
00401359 sub al, 30h ;sottrae trenta così da eax passiamo a semplice char es (39-30=9)
0040135B shl eax, cl
0040135D add ebx, eax
0040135F sub ecx, 4
00401362 inc ebp
00401363 cmp ebp, edx
00401365 jnz short loc_40134E
00401367 jmp short locret_401377
00401369 loc_401369: ;Nel caso si trattase si una lettera, procedi con l'altra routine
00401369 sub al, 37h ;che alla fin fine fa la stessa cosa di quella dei numeri
0040136B shl eax, cl
0040136D add ebx, eax
0040136F sub ecx, 4
00401372 inc ebp
00401373 cmp ebp, edx
00401375 jnz short loc_40134E
00401377 retn

 

che, senza stare a perder tempo, altro non è che una funzione di conversione, che trasforma il nostro serial da hex a stringa semplice. Ogni call elabora un pezzo di serial. Adesso che sappiamo cosa fanno queste call, possiamo procedere oltre con l'analisi.

00401151 mov dword_403A11, ebx
00401157 call sub_401378 ;call che analizzeremo in seguito
0040115C test al, al
0040115E jz loc_401212 ;se la call restituisce lo status FALSE allora salta al msg d'errore
00401164 call sub_401421 ;call che analizzeremo in seguito
00401169 test al, al
0040116B jz loc_401212

00401171 call sub_401499 ;call che analizzeremo in seguito

Passiamo ad analizzare la call 00401378

00401378 push 104h
0040137D push offset Buffer ; lpBuffer
00401382 call GetWindowsDirectoryA
00401387 mov dword_403A3C, 0
00401391 push 104h
00401396 push 403A3Dh
0040139B push offset FileSystemFlags
004013A0 push offset MaximumComponentLength
004013A5 push offset VolumeSerialNumber
004013AA push 104h
004013AF push offset VolumeNameBuffer
004013B4 push offset Buffer
004013B9 call GetVolumeInformationA
004013BE mov ecx, VolumeSerialNumber ;mette in ecx il SeriaNumber del nostro HDD
004013C4 mov ebx, VolumeSerialNumber ;mette in ecx il SeriaNumber del nostro HDD
004013CA and ecx, 0FFFFh ;Elimina la HighWord del SeriaNumber e lascia la LowerWord
004013D0 shr ebx, 10h ;Fa la stessa cosa dell'istruzione sopra
004013D3 add ecx, ebx ;somma le due word
004013D5 cmp cx, word_403A09 ;PrimaWord dev'essere ugualle a cx (cioè alla somma)

004013DC setz al ;Sennò setta al FALSE

Questo pezzettino di codice ci permette di trovare il valore di PrimaWord, possiamo facilmente intuire che questo valore dipende solamente dall'hardisk in cui si trova il crackme.

004013DF movsx edx, byte_403A08 ;mette in edx la lunghezza del nostro username
004013E6 and ebx, 5Fh ;and tra 5Fh e la LowerWord del SerialNumber
mov V1,ebx
...

Da questo pezzo di codice possiamo vedere, che adesso si comincia a lavorare con l'username. C'è anche da dire che il risulto dell'AND tra la LowerWord del SerialNumber del HDD ed il valore 5Fh andrà in una variabile che ho chiamato V1, la quale sarà usata in una routine che vedremo presto. Andiamo ora ad analizzare la call 00401421:

00401421 xor ecx, ecx
00401423 xor edx, edx
00401425 mov cl, byte_403A08 ;in cl la lunghezza del username che abbiamo inserito
0040142B mov dword_403A15, 0FD12EA30h
00401435 loc_401435:
00401435 movzx eax, byte_4039A4[edx] ;mette in eax un char corrispondente all' username
0040143C mov ebx, dword_403A15 ;la prima volta 0FD12EA30h in ebx, poi sara aggiornato - chiamiamo la locazione 403A15h 'Delta'
00401442 and ebx, 0FFh ;ne rimane solo il byte meno significativo (LSB) 30h
00401448 xor eax, ebx
0040144A mov eax, dword_[eax*4+40342A] ;mette in eax una dword appartenene ad una tabella (40342A), questa tabella è in altre parole una Crc32_Table
00401451 mov ebx, dword_403A15
00401457 shr ebx, 8
0040145A xor eax, ebx
0040145C mov dword_403A15, eax
00401461 inc edx

00401462 loopne loc_401435

Abbiamo davanti a noi la più classica delle routine di Crc32, riconoscibilissima anche a 100 km di distanza :). Per chi non lo sapesse CRC significa Cyclic Redundancy Check o a seconda dei casi anche Cyclic Redundancy Code, spesso (quasi sempre) i CRC vengono utilizzati come controlli d'integrità di codice o di dati. Adiamo avanti:

00401464 not eax ;NOT del crc
00401466 mov ecx, dword_403A0D ;in ecx andrà PrimaDword
0040146C xor eax, ecx
0040146E jnz short loc_401495 ;se PrimaDword è uguale a not(crc) allora procedi, sennò salta al msg d'errore
00401470 mov eax, dword_403A15 ;mette nuovamente in eax il crc
00401475 and eax, 0FFFFh ;lascia solo la HigherWord
0040147A shr ecx, 10h ;lascia solo HigherWord del not(crc)
0040147D and eax, ecx ;AND HigherWord(not),HigherWord
0040147F and ax, 0FF00h
00401483 mov cx, word_403A0B ;403A0B contiene il valore corretto (che andrà in SecondaWord)
0040148A and cx, 0FF00h ;lascia solo il byte più significativo della LowerWord
0040148F not ax
00401492 and ax, cx

00401495 setz al ;Se eax è 0 setta AL a TRUE

Da questo pezzetto di codice possiamo ricavare il giusto valore di PrimaDword e successivamente di SecondaWord, infatti se prendiamo il codice da 00401470 in poi e ne aumentiamo il livello di astrazione (The+Q di +Fravia docet) saremo in grado con due semplici passaggi di reversare il codice:

Se abbiamo infatti: (PrimaDword >> 10h) && Delta

allora avremo: SecondaWord == ((PrimaDword >> 10h) && Delta)

che ritradotto in asm:

not eax ;not(crc)
mov PrimaDword, eax
mov ecx, eax
mov eax, Delta
shr ecx, 10h
and eax, ecx
and ax, 0FF00h

mov SecondaWord, ax

Questa è solo la prima parte della routine per ottenere SecondWord, infatti come potete vedere dalla relazione, alcune cose ancora non le abbiamo considerate (leggete più sotto, la parte che tratta del logaritmo).

Usciti da questa call, continuiamo la nostra analisi:

00401169 test al, al
0040116B jz loc_401212
00401171 call sub_401499 ;call che analizzeremo in seguito
00401176 mov eax, dword_403A11

0040117B bswap eax

Vediamo cosa c'è dentro la call 00401499:

0040149A mov edx, dword_403A15 ;Delta in edx
004014A0 not edx ;not(Delta)
004014A2 mov eax, dword_403A15 ;Delta in eax
004014A7 call sub_40158A ;Call particolare di cui parleremo dopo
004014AC and edx, 0FFh
004014B2 mov ecx, edx
004014B4 xor edx, edx
004014B6 div ecx
004014B8 mov al, byte_40382A[edx] ;40382A è un array di valori fissi
004014BE and eax, 0ABh
004014C3 mov ebx, dword_403A61 ;Ricordate la variabile V1?..adesso la incontriamo nuovamente
004014C9 xor eax, ebx
004014CB mov dword_403A61, eax ;V1 = V1 ^ Rand() -> semplice xor reversabilissimo
004014D0 mov edx, dword_403A15 ;nuovamente Delta in edx
004014D6 bswap edx
004014D8 mov eax, dword_403A15 ;nuovamente Delta in eax
004014DD add eax, edx
004014DF call sub_40158A ;Stessa call di sopra

 

Questa routine merita qualche spiegazione. Non è necessario studiare approfonditamente la call 40158A, perchè contiene al suo interno un piccolo algoritmo composto da shift, rotate ecc. di cui non c'interessa granchè dato che non dobbiamo reversarlo. Chiamerò questa call 'Algo()' per motivi di semplicità, c'è da notare che prima della chiamata ad Algo() viene messa in eax ed edx la variabile Delta. Analizzando i valori di ritorno eax ed edx di Algo(), possiamo notare che alla prima chiamata (004014A7h):

1°Algo(): eax = Delta, edx =! Delta

Alla seconda chiamata di Algo() possiamo intendere eax e edx come:

2°Algo(): eax=Delta+bswap(Delta), edx=bswap(Delta)

Procediamo con lo studio della routine:

004014E4 and edx, 0FFh
004014EA mov ecx, edx
004014EC xor edx, edx
004014EE div ecx
004014F0 mov al, byte_40382A[edx] ;in al un byte della tabella di valori fissi
004014F6 and eax, 0FBh
004014FB mov dword_403A65, eax ;chiameremo 403A65 'V3', in questo caso quindi V3 = Algo()
00401500 mov ecx, 50h
00401505 mov eax, dword_403A15 ;Delta in eax
0040150A add eax, 2004h
0040150F xor eax, 4002h
00401514 imul eax, ecx
00401517 loopne loc_40150A
00401519 and eax, 0FFh
0040151E mov dword_403A75, eax ;chiameremo 403A75 'V5', che in questo caso sarà V5 = Algo()

00401523 fild dword_403A75 ;fild V5, cioè mette V5 in St0 (dovete conoscere un minimo di architettura del FPU - FloatingPointUnit)

Vediamo ora cosa succede all'uscita di questa call:

00401176 mov eax, dword_403A11 ;in eax SecondaDword
0040117B bswap eax ;bswap(SecondaDword)
0040117D fild dword_403A71 ;mette uno in St0 (V5 passa a St1)
00401183 mov ebx, dword_403A15 ;Delta in ebx
00401189 add eax, ebx ;bswap(SecondaDword) + Delta
0040118B xor eax, 2004h
00401190 bswap eax
00401192 mov dword_403A69, eax ;mette il risultato in 403A69
00401197 fyl2x ;calcola Z = Y log(sub2)(X) dove X è preso da St0 e Y da st1
00401199 fstp dword_403A6D ;mette il risultato del logaritmo in 403A6D
0040119F fld dword_403A69 ;mette 403A69 in St0
004011A5 fcomp dword_40392E ;confronta St0 con 40392E
004011AB fstsw ax
004011AE sahf ;salva AX nei flag AF CF PF SF ZF
004011AF jb short loc_401212
004011B1 call sub_40152B ;call che analizzeremo dopo
004011B6 test al, al
004011B8 jz short loc_401212 ;se al è FALSE salta alla messagebox d'errore
004011BA push 0
004011BC push offset aEurika
004011C1 push offset aCongratulation
004011C6 push hWnd ; hWnd
004011CC call MessageBoxA

 

L'istruzione fyl2x come già detto esegue Z= Y log2 (X), voglio solo specificare che 0 < X < Infinito (cioè X dev'essere positivo), mentre per Y Infinito < Y < Infinito. Come prima aumentando il livello di astrazione:

(LowerWord)SecondaDword = V2^bl-(HighWord)SerialDword

dove se V2=1 per la proprietà dei logaritmi avremo log2(V2)=0

Per semplicità chiameremo la locazione 403A69 'V4'.

Passiamo ora ad analizzare la call 40152B:

0040152B finit ;inizializza il copocessore
0040152E fld dword_403A69 ;mette V4 in St0
00401534 fmul dword_403A69 ;moltiplicazione
0040153A fmul dword_403A69
00401540 fmul dword_403A69
00401546 fmul dword_403A6D
0040154C fld dword_403A69 ;rimette V4 in St0
00401552 fmul dword_403A69 ;moltiplica un'altra volta
00401558 fimul dword_403A61 ;(V4^2)*V1
0040155E faddp st(1), st ;(V4^2)*V1 + (le due istruzioni immediatamente dopo)
00401560 fild dword_403A65 ;mette V3 in St0
00401566 fmul dword_403A69 ;V3*V4
0040156C faddp st(1), st
0040156E fild dword_40392A ;40392A contine all'interno il valore fisso 12345h (74565d)
00401574 fabs ;st0 in valore assouluto
00401576 fchs ;cambia il segno di st0
00401578 faddp st(1), st ;somma St1+St0
0040157A fabs ;valore assoluto di St0
0040157C fcomp dword_403932 ;confronta il risultato delle operazioni precedenti con 74565d
00401582 fstsw ax
00401585 sahf
00401586 setb al ;se la condizione è vera setta al a 1
00401589 retn

 

Dentro questa call, come potete ben vedere, troviamo una serie di Floating Point Operations, si tratta di un puro calcolo matematico. Cerchiamo come prima cosa di elevare il livello di astrazione di questa routine per rendere le cose più chiare, e facilmente reversibili, ecco cosa fa "matematicamente" questa call:

Var2 = -.1529248683*((V4^2)*V1+V3*V4-74565.)/ V4^4

Risistemando un pò le cose, ecco che ricaviamo l'equazione finale da risolvere:

(V4^2)*V1+V4*V3-74565=0

Infine vi riporto la routine che ho usato nel keygen per risolvere questa equazione:

finit
fild quattro ;4
fimul V1
fimul h74565 ;-4ac
fild V3
fimul V3 ;
faddp st(1),st ;Delta
fsqrt ;radice quadrata
fild V3
fchs ;cambio il segno
faddp st(1),st
fild V1
fild V1
faddp st(1),st
fdivp st(1),st(0)
fstp SecondaDword
mov eax, SecondaDword
bswap eax
xor eax, 2004h
mov ebx, Delta
sub eax, ebx
bswap eax

mov SecondaDword, eax

Bene, ecco tutto!! ;) spero di essere stato chiaro, se non avete capito qualcosa non esitate a contattarmi. Se invece mi venite a fare richieste di crack..non vi rispondo :->

 

Note finali

Saluto: Quequero, AndreaGeddon, Ironspark, Kratorius, LonelyWolf, ZaiRoN, Misanthropic, Neural_Noise che è da un pò di tempo che non vedo :) Anticrack friends: Bon_Scott, Zero, Kreatief, Hell_Master. Porgo le mie scuse alle persone di cui mi sono dimenticato :)

Disclaimer

Noi reversiamo al solo scopo informativo e di miglioramento del linguaggio Assembly.

Capitoooooooo????? Bhè credo di si ;))))