WinZip 10.0 Keygenning |
||
Data |
by "Graftal" |
|
gg/mese/aaaa |
Published by Quequero |
|
Because the music do! |
Graft, bravo come al solito, grazie mille! |
Because the music do! |
.... |
E-mail: [email protected] Graftal @ irc.azzurra.org #crack-it #cryptorev #asm |
.... |
Difficoltà |
( )NewBies (X)Intermedio ( )Avanzato ( )Master |
Introduzione |
Salve a tutti. In questo tutorial ci occuperemo di un programma storico quale WinZip: arrivato alla decima incarnazione, i programmatori hanno deciso di cambiare (finalmente) la routine
di protezione:
ed è qui che entriamo in gioco noi. Sedetevi comodi e godetevi il viaggio.
=)
Allegato
Tools usati |
Tool usati:
Visual Studio 2003 + OpenSSL per codare il keygen.
RSA Tool v2 reperibile facilmente su goooogle =)
URL o FTP del programma |
Beh non ci va tanta fantasia per capirlo.
Essay |
Benissimo, cominciamo subito ad aprire il programma con ollydbg e notare che
non è né packato né niente. La nag ci accoglie in modo
a dir poco caloroso, e noi ricambiandolo andiamo su Enter Registration Code
e inseriamo Graftal come username e 1234567890 come serial. Soliti bp quali
GetWindowText e GetDlgItemText e poi facciamo OK. Il secondo di questi bp
ci fa atterrare alla zona di codice seguente:
004136FF |> PUSH 101
; /Count = 101 (257.); Case 1 of switch 00413704 |. MOV EDI,WINZIP32.0054F350 ; |ASCII "Graftal" 00413709 |. PUSH EDI ; |Buffer => WINZIP32.0054F350 0041370A |. PUSH 0C80 ; |ControlID = C80 (3200.) 00413710 |. CALL DWORD PTR DS:[<&USER32.GetDlgItemTe>; \GetDlgItemTextA ------------------- ~snip~ ------------------ 00413724 |. PUSH 27 ; /Count = 27 (39.) 00413726 |. MOV ESI,WINZIP32.0054F454 ; |ASCII "1234567890" 0041372B |. PUSH ESI ; |Buffer => WINZIP32.0054F454 00413731 |. PUSH EBX ; |hWnd 00413732 |. CALL DWORD PTR DS:[<&USER32.GetDlgItemTe>; \GetDlgItemTextA |
Ciò che a noi realmente interessa qui sono le variabili Count che ci possono dare una POSSIBILE indicazione anche sulla lunghezza del serial. Di sicuro poi ci servono per fare il keygen, se non altro per quanto riguarda la lunghezza massima del nickname. Adesso ci tocca trovare il punto di check del serial: facciamo però attenzione qua, siccome il programma è molto dispersivo e ci ho messo un attimino a trovare la giusta locazione da andare a studiare e reversare. La cosa migliore da fare in questi casi, e steppare oltre tutto e tutti, per vedere se ci sono qualche variabili o qualche check che possono attirare la nostra attenzione: a quel punto, andremo più nel dettaglio. Da dove siamo ora nel codice, cominciamo a steppare fino ad arrivare ad un codice simile a questo:
0041383D |. CALL WINZIP32.004133BB 00413842 |. TEST AL,AL 00413844 |. POP ECX 00413845 |. JE WINZIP32.004138E3 ; salta molto più giù mostrandoci la beggar off 0041384B |. PUSH EDI 00413851 |. MOV EDI,WINZIP32.0051A808 ; ASCII "WinZip" 00413856 |. PUSH EDI 00413857 |. CALL WINZIP32.00499F4B 0041385D |. PUSH WINZIP32.0051A8F0 ; ASCII "SN1" 00413862 |. PUSH EDI 00413863 |. CALL WINZIP32.00499F4B 00413868 |. CALL WINZIP32.00412B2A 0041386D |. PUSH WINZIP32.0051A810 ; ASCII "winzip32.ini" 00413872 |. PUSH 0 00413874 |. PUSH 0 00413876 |. PUSH WINZIP32.0051A830 ; ASCII "rrs" |
Possiamo facilmente intuire che il grosso del check DOVREBBE avvenire nella call che ho sopra scritto: infatti quel je nel caso dei dati che abbiamo immesso salta e quindi il nome e il serial non vengono salvati, se non altro non come verrebbero salvati dei serial corretti. Facciamo F9 e rifacciamo OK, steppiamo e questa volta arrivati alla call 004133BB (quella qua sopra insomma) steppiamoci dentro con F7. A questo punto ci ritroviamo di fronte una marea di codice, per la maggior parte assolutamente inutile. Come capirlo? Beh, è importante steppare oltre tutto il codice la prima volta per poi trovare il posto in cui andare a “scavare” =). Facendo cosi, arriviamo dopo un po’ a queste istruzioni:
00413514 |. PUSH EDI ; /String 00413515 |. CALL DWORD PTR DS:[<&KERNEL32.lstrlenA>] ; \lstrlenA 0041351B |. CMP EAX,2 0041351E |. JL SHORT WINZIP32.0041352B 00413520 |. CMP EAX,100 00413525 |. JG SHORT WINZIP32.0041352B |
Altra cosa da ricordarsi codando il keygen: la lunghezza dell’username deve essere compresa tra 2 e 0x100. Se continuiamo a steppare noteremo comparire anche il nostro serial number, e poi basta, la call finisce. Adesso ci tocca trovare la giusta call da steppare e analizzare. Una prima scelta da fare, potrebbe essere quella di analizzare questa call:
0041348E |. MOV EDI,WINZIP32.0054F350 ; ASCII "Graftal" 00413493 |. LEA EAX,DWORD PTR SS:[EBP-438] 00413499 |. MOV ECX,EDI 0041349B |. CALL WINZIP32.004124D2 |
Poiché il nostro username lo troviamo in giro, è possibile che nella call subito sotto, o in quelle ad esso successive, siano presenti istruzioni interessanti da analizzare. La prima call, tuttavia, anche solo ad un colpo d’occhio molto superficiale, ci fa capire che là dentro non accade niente di importante. Nella call dopo, troviamo:
004E6980 /MOV EAX,DWORD PTR DS:[ECX] 004E6982 |MOV EDX,7EFEFEFF 004E6987 |ADD EDX,EAX 004E6989 |XOR EAX,FFFFFFFF 004E698C |XOR EAX,EDX 004E698E |ADD ECX,4 004E6991 |TEST EAX,81010100 004E6996 |JE SHORT WINZIP32.004E6980 004E6998 |MOV EAX,DWORD PTR DS:[ECX-4] 004E699B |TEST AL,AL 004E699D |JE SHORT WINZIP32.004E69D1 004E699F |TEST AH,AH 004E69A1 |JE SHORT WINZIP32.004E69C7 004E69A3 |TEST EAX,0FF0000 004E69A8 |JE SHORT WINZIP32.004E69BD 004E69AA |TEST EAX,FF000000 004E69AF |JE SHORT WINZIP32.004E69B3 004E69B1 \JMP SHORT WINZIP32.004E6980 |
Dopo aver reversato per un po’ di tempo, si comincia a capire che valori quali 0x7EFEFEFF e simili, soprattutto in questi contesti, sono call inutili ai nostri fini. Ciò è un ottimo criterio per eliminare un po’ delle call che si incontrano nel cammino =). Detto questo (trovo importante spiegare certi modi con cui “eliminare” il junk code, soprattutto perché è uno dei motivi che inducono il reverser a lasciar perdere il programma, o che comunque gli fanno perdere una miriade di tempo), usiamo un secondino e per pochissimo il cervello: se il check della lunghezza dell’username immesso è dopo, è altamente probabile che tutto il codice che lo precede non serva. Supposto ciò, saltiamo tutto fino ad arrivare all’strlen che vi avevo scritto prima e in particolare, quando siamo su questa call, senza fare step-into, vediamo quanto vale eax (ovvero uno dei parametri pushati alla call):
00413532 |. PUSH 0 00413534 |. PUSH EAX 00413535 |. CALL WINZIP32.004E68F0 ; questa call -------------------- ~snip~ -------------------------- 00413554 |> MOV ESI,12C 00413559 |. PUSH ESI 0041355A |. LEA EAX,DWORD PTR SS:[EBP-144] 00413560 |. PUSH EAX 00413561 |. PUSH EDI 00413562 |. CALL WINZIP32.00411EA8 ; poi analizziamo questa |
EAX vale “bcom”, stringa che troviamo relativamente spesso in queste istruzioni: non facciamoci ingannare, non serve a niente sta stringa => non serve a niente nemmeno la call. Se volete analizzarla bene, ma sappiate che questo è uno dei modi in cui il programma ci fa perdere tempo prezioso: dobbiamo essere rapidi a trovare l’algo, altrimenti ci si annoia e si lascia perdere. E ciò non va bene =). Tuttavia, la seconda call già contiene qualcosa che può attirare l’attenzione: dando uno sguardo d’insieme, notiamo anche una costante, 0x1021. Se qualcuno di voi aveva già reversato il winzip 8 o il 9, avrà sicuramente notato che questa era la mask usata dal programma per poi generare il serial. Già qui dovrebbe accendersi una lampadina.. però se uno non ne sa niente della tecnica di protezione delle versioni vecchie come fa a capire? No problem, steppiamo nella call e vediamo che serial viene generato:
---------- ~snip dei loop precedenti, ci interessa solo il serial già generato e completato ------------------- 00411EF9 |. PUSH ECX ; /Arg5 00411EFA |. MOVZX EAX,AX ; | 00411EFD |. PUSH EAX ; |Arg4 00411EFE |. PUSH WINZIP32.0051D9E8 ; |Arg3 = 0051D9E8 ASCII "%04X%04X" 00411F03 |. PUSH DWORD PTR SS:[EBP+10] ; |Arg2 00411F06 |. MOV BYTE PTR DS:[54F4C8],1 ; | 00411F0D |. PUSH DWORD PTR SS:[EBP+C] ; |Arg1 00411F10 |. CALL WINZIP32.0050755E ; \WINZIP32.0050755E ---------- ~snip~ ------------------------ |
Ed ecco nello stack, subito
dopo la call di cui (nota per i più sgrammaticati: non è un
errore, “cui” non “qui”) sopra – che in realtà
è un semplicissimo sprintf – troviamo il nostro bel serial:
"56AE08A3"
Che effettivamente è il mio serial per le versioni precedenti di winzip.. se non lo si sapesse, si potrebbe sempre provare a inserirlo come serial per ottenere una bella beggar off, che magari ti dice anche che il tuo serial è vecchio e che dovresti aggiornarlo per la versione 10 del programma. Usciti dalla call, vediamo che alcune cosette vengono fatte al serial generato poco fa, e altre call si susseguono, inutili, le une dopo le altre, fino ad arrivare finalmente al nostro serial:
004135CB |. MOV EBX,WINZIP32.0054F454 ; ASCII "1234567890" 004135D0 |. PUSH EBX 004135D1 |. PUSH EDI 004135D2 |. CALL WINZIP32.004120FB ; ci conviene dare un’occhiata qua =) |
Ora, esattamente come abbiamo fatto prima con il nostro username, ora cominciamo a vedere se le call presenti subito sotto il nostro serial sono interessanti o meno. Cominciamo con la prima:
00412118 |. MOV DWORD PTR SS:[EBP-150],EAX 0041211E |. MOV DWORD PTR SS:[EBP-14C],EAX 00412124 |. MOV DWORD PTR SS:[EBP-154],EAX 0041212A |. MOV DWORD PTR SS:[EBP-158],0FFFF 00412134 |. CALL WINZIP32.004E8F04 ; lasciamo perdere questa call, troppo complicata =) ------------- ~snip~ --------------------- 0041214B |> /MOVZX EAX,BYTE PTR DS:[EDI] ; prende il primo char dell’username 0041214E |. |PUSH EAX 00412154 |. |TEST EAX,EAX ; se eax == 1 00412156 |. |POP ECX 00412157 |. |JE SHORT WINZIP32.0041215E 00412159 |. |MOV AL,BYTE PTR DS:[EDI] ; il char va bene e.. 0041215B |. |MOV BYTE PTR DS:[ESI],AL ; ..viene storato in un’altra variabile 0041215D |. |INC ESI 0041215E |> |INC EDI 00412162 |.^\JNZ SHORT WINZIP32.0041214B ; si rifà fino alla fine dell’username 00412164 |> LEA EAX,DWORD PTR SS:[EBP-148] ; username in eax 0041216A |. PUSH EAX 0041216B |. MOV BYTE PTR DS:[ESI],0 0041216E |. CALL WINZIP32.004E6950 ; ritorna strlen(username) 00412173 |. CMP EAX,12C ; se minore di 0x12C.. 00412178 |. POP ECX 00412179 |. JB SHORT WINZIP32.0041218C ; ..allora ok 00412192 |. PUSH EAX ; eax = username 00412193 |. CALL WINZIP32.00507CB3 ; CharLower(username) --------------------- ~blabla~ ------------------ |
Allora questo è un po’ di codice commentato: vi ho messo solo l’inizio della call siccome è molto lunga. Se steppate anche un po’ superficialmente nel resto del codice, noterete che vi vengono riproposte altre istruzioni molto simili o uguali a quelle che abbiamo analizzato prima, mentre altre totalmente nuove e che ad una prima occhiata potrebbero sembrare importanti. Ma come facciamo a capire che la call è pressoché inutile? Diciamo che prima di tutto o ci va un po’ di culo, o si può vedere che vengono eseguiti tanti tanti calcoli, ma alla fine non compaiono più né il nostro serial, né il nostro username, né lunghezze che possano corrispondergli né niente di niente. E’ a questo punto che bisogna andare ad analizzare un’altra call per vedere se almeno là succede qualcosa: in caso contrario, la call tralasciata era probabilmente importante =). Ecco la prossima call:
004135D7 |. LEA EAX,DWORD PTR SS:[EBP-438] 004135DD |. PUSH EBX 004135DE |. PUSH EAX 004135DF |. CALL WINZIP32.00411E19 |
Steppiamoci dentro:
00411E19 /$ PUSH EBP 00411E1A |. MOV EBP,ESP 00411E1C |. PUSH DWORD PTR SS:[EBP+8] 00411E1F |. CALL WINZIP32.00507CB3 ; CharLower(username) è importante! 00411E24 |. POP ECX 00411E25 |. PUSH DWORD PTR SS:[EBP+C] 00411E28 |. MOV ECX,DWORD PTR DS:[55CE00] 00411E2E |. PUSH DWORD PTR SS:[EBP+8] 00411E31 |. PUSH WINZIP32.0051D9E0 ; ASCII "SU_STD" 00411E36 |. CALL WINZIP32.004975F8 ; dobbiamo assolutamente steppare qua dentro 00411E3B |. TEST AL,AL 00411E3D |. JE SHORT WINZIP32.00411E48 00411E3F |. MOV BYTE PTR DS:[54E5FB],1 00411E46 |. JMP SHORT WINZIP32.00411E79 00411E48 |> PUSH DWORD PTR SS:[EBP+C] 00411E4B |. MOV ECX,DWORD PTR DS:[55CE00] 00411E51 |. PUSH DWORD PTR SS:[EBP+8] 00411E54 |. PUSH WINZIP32.0051D9D8 ; ASCII "SU_PRO" 00411E59 |. CALL WINZIP32.004975F8 ; e poi qua dentro 00411E5E |. TEST AL,AL 00411E60 |. JE SHORT WINZIP32.00411E72 00411E62 |. MOV BYTE PTR DS:[54E5FB],1 00411E69 |. MOV BYTE PTR DS:[54E5FD],1 00411E70 |. POP EBP 00411E71 |. RETN 00411E72 |> MOV BYTE PTR DS:[54E5FB],0 00411E79 |> MOV BYTE PTR DS:[54E5FD],0 00411E80 |. POP EBP 00411E81 \. RETN |
Bene bene bene.. SU_STD = STANDARD e SU_PRO = PROFESSIONAL..
almeno teoricamente =). Tra un po’ scopriremo se è veramente
così o no.
Allora, cominciamo con lo steppare dentro alla prima call segnalata, quella
subito sotto SU_STD:
-------------------- ~snip~ ------------------------- 00497641 |. FF70 08 PUSH DWORD PTR DS:[EAX+8] 00497644 |. FF70 04 PUSH DWORD PTR DS:[EAX+4] ; "-----BEGIN RSA PUBLIC KEY-----MBcCEADjFVf2ZJjxOFl+bqOr3O8CAwEAAQ==-----END RSA PUBLIC KEY-----" 00497647 |. FF37 PUSH DWORD PTR DS:[EDI] 00497649 |. E8 A9C80600 CALL WINZIP32.00503EF7 |
Ehi ehi, cosa vedono le
mie pupille! RSA? Ma che bello! =)
Quello che viene pushato è il contenuto di un PEM Key. Per alcuni,
una domanda sorgerà spontanea: ma cos’è un PEM Key? Diciamo
che è un file .pem che viene usato per storare le chiavi rsa necessarie
per criptare e decriptare dati. In questo caso, abbiamo un bel public key
(per informazioni particolareggiate riguardo all’rsa, vi rimando al
tutorial di evilcry sull’argomento, veramente bello). Teniamo quindi
gli occhi aperti per eventuali funzioni inerenti RSA.
Se diamo un’occhiata all’interno della call, vedremo che la libreria
del winzip chiamata wzeay32 contiene le funzioni che riguardano l’rsa,
e, tanto per fare un esempio, ci sono due call a funzioni chiamate d2i_RSAPublicKey
e PEM_ASN1_read_bio. Facendo una piccolissima ricerca su internet, poiché
avevo notato la netta similitudine tra librerie quali libeay32 e wzeay32,
si può facilmente scoprire che wzeay32 è adibito alla gestione
delle funzioni OpenSSL all’interno del winzip =). Teniamolo a mente
che ci servirà dopo nel fare il keygen (sebbene non sia indispensabile).
Adesso cominciamo a steppare, tenendo a mente che nel momento in cui incontriamo del codice dove viene pushata una stringa che ha come contenuto "..\..\common\WzReg.c" o qualcosa di simile, la call subito precedente è inutile e quindi non perdiamo tempo ad analizzarla. Detto questo, proseguiamo a steppare, diamo un’occhiata premendo Enter quando siamo su una call per vedere se può servire a qualcosa, e in caso contrario continuiamo fino a che non arriviamo qua:
00497760 |. PUSH EAX 00497761 |. PUSH DWORD PTR SS:[EBP-44] 00497764 |. PUSH DWORD PTR DS:[EDI] 00497766 |. CALL WINZIP32.00503FC6 ; ed ecco qua la parte importante! |
Steppiamo e vediamo cosa
ci aspetta:
00503FEC |. CMP DWORD PTR SS:[EBP+C],0 ; username = 0? 00503FF0 |. JE WINZIP32.005041EB ; se sì, beggar off 00503FF6 |. TEST BYTE PTR DS:[ESI+8],2 00503FFA |. JE WINZIP32.00504197 00504000 |. MOV EDI,DWORD PTR SS:[EBP+14] ; serial = 0? 00504003 |. TEST EDI,EDI 00504005 |. JE WINZIP32.005041BB ; se sì, beggar off 0050400B |. CMP DWORD PTR SS:[EBP+18],1E ; lunghezza serial = 1E? |
Ecco qua una cosa fondamentale: il nostro serial è lungo 0x0B (perché viene contato anche lo zero finale) mentre dovrebbe essere lungo 0x1E caratteri. Facciamo F9 e inseriamo questo serial: 12345678901234567890123456789. Ok, ritorniamo dove eravamo prima, sorbiamoci un paio di altri check che vanno a buon fine, fino a raggiungere questo loop:
00504039 |> /CMP EDX,100 00504041 |. |MOV AL,BYTE PTR DS:[ECX+EDI] 00504044 |. |CMP AL,2D 00504046 |. |JE SHORT WINZIP32.00504050 00504048 |. |MOV BYTE PTR SS:[EBP+EDX-138],AL 00504050 |> |INC ECX 00504051 |. |CMP ECX,1D 00504054 |.^\JL SHORT WINZIP32.00504039 |
Non l’ho commentato perché c’è poco da capire: semplicemente toglie dal serial tutti i trattini che dovrebbero esserci, ma che noi ancora non abbiamo messo, e riposiziona i caratteri nella stringa in modo che non ci siano “buchi” (abbiamo appena tolto i trattini, quindi..). Tenete sempre presente che 0x2D = ‘-‘. Subito dopo questo loop, c’è una bella call che andiamo ad analizzare a fondo perché è fondamentale. Vediamo come inizia:
00504FDD |> /MOV EAX,EDI ; lunghezza serial senza trattini in eax 00504FDF |. |SHL EAX,3 ; eax * 8 00504FE2 |. |PUSH 5 00504FE4 |. |CDQ 00504FE5 |. |POP ECX 00504FE6 |. |IDIV ECX ; eax / 5 con resto in edx 00504FE8 |. |TEST EDX,EDX ; resto = 0? (quindi eax multiplo di 5?) 00504FEA |. |JNZ WINZIP32.005050F5 ; se no, salta e beggar off |
Bene, quindi dobbiamo mettere tanti trattini finché
il nostro 29 diventa un multiplo di 5. Mettiamo quindi 4 bei trattini e rendiamo
così il nostro serial: 12345-78901-34567-90123-56789
Attenzione a SOSTITUIRE i caratteri che c’erano prima con i trattini,
e non ad aggiungerli, se no il check che abbiamo analizzato prima dà
beggar off =). Il codice che vi mostro ora è molto lungo, ma non fatevi
spaventare, le cose veramente importanti le spiego là dentro e subito
dopo vi chiarisco il tutto spiegandovelo senza i commenti, siccome verrebbe
un pasticcio mostruoso fare le due cose assieme:
00505011 |> /MOV EAX,DWORD PTR SS:[EBP+8] 00505014 |. |MOVZX EAX,BYTE PTR DS:[EAX] 00505017 |. |PUSH EAX 00505018 |. |CALL WINZIP32.004E9127 ; lasciamo perdere, inutile 0050501D |. |CMP AL,41 ; al = char del serial 00505020 |. |JNZ SHORT WINZIP32.00505026 ; ..lo lavora in modo da dargli un valore.. 00505022 |. |XOR CL,CL ; ..in modo che questo sia sempre formato.. 00505024 |. |JMP SHORT WINZIP32.00505062 ; ..da soli 5 bit significativi. 00505026 |> |CMP AL,42 00505028 |. |JBE SHORT WINZIP32.00505032 0050502A |. |CMP AL,49 0050502E |. |SUB AL,42 00505030 |. |JMP SHORT WINZIP32.00505060 00505032 |> |CMP AL,49 00505034 |> |JBE SHORT WINZIP32.0050503E 00505036 |. |CMP AL,4F 00505038 |. |JNB SHORT WINZIP32.00505040 0050503A |. |SUB AL,43 0050503E |> |CMP AL,4F 00505040 |> |JBE SHORT WINZIP32.0050504A 00505042 |. |CMP AL,53 00505044 |. |JNB SHORT WINZIP32.0050504C 00505046 |. |SUB AL,44 00505048 |. |JMP SHORT WINZIP32.00505060 0050504A |> |CMP AL,53 0050504E |. |CMP AL,5A 00505050 |. |JA SHORT WINZIP32.00505056 00505052 |. |SUB AL,45 00505054 |. |JMP SHORT WINZIP32.00505060 00505056 |> |CMP AL,30 ; Switch (cases 30..39) 00505058 |. |JB SHORT WINZIP32.005050D3 0050505A |. |CMP AL,39 0050505E |. |SUB AL,1A ; Cases 30 ('0'),31 ('1'),32 ('2').. 00505060 |> |MOV CL,AL 00505062 |> |MOV EAX,DWORD PTR SS:[EBP+C] ; dopodichè tutti i valori ottenuti.. 00505065 |. |PUSH 8 ; sono “mischiati” come spiegato dopo 00505067 |. |CDQ 00505068 |. |POP EBX 00505069 |. |IDIV EBX 0050506B |. |CMP EDX,7 ; Switch (cases 0..7) 0050506E |. |JA SHORT WINZIP32.005050BF 00505070 |. |JMP DWORD PTR DS:[EDX*4+505104] 00505077 |> |SHL CL,3 ; Case 0 of switch 0050506B 0050507A |. |JMP SHORT WINZIP32.005050B8 0050507E |. |SHR AL,2 00505081 |. |OR BYTE PTR DS:[ESI],AL 00505083 |. |INC ESI 00505084 |. |SHL CL,6 00505087 |. |JMP SHORT WINZIP32.005050B8 00505089 |> |SHL CL,1 ; Case 2 of switch 0050506B 0050508B |. |JMP SHORT WINZIP32.005050A9 0050508D |> |MOV AL,CL ; Case 3 of switch 0050506B 00505092 |. |OR BYTE PTR DS:[ESI],AL 00505094 |. |INC ESI 00505095 |. |SHL CL,4 00505098 |. |JMP SHORT WINZIP32.005050B8 0050509A |> |MOV AL,CL ; Case 4 of switch 0050506B 0050509E |. |OR BYTE PTR DS:[ESI],AL 005050A0 |. |INC ESI 005050A1 |. |SHL CL,7 005050A4 |. |JMP SHORT WINZIP32.005050B8 005050A6 |> |SHL CL,2 ; Case 5 of switch 0050506B 005050A9 |> |OR BYTE PTR DS:[ESI],CL 005050AB |. |JMP SHORT WINZIP32.005050BF 005050AD |> |MOV AL,CL ; Case 6 of switch 0050506B 005050AF |. |SHR AL,3 005050B2 |. |OR BYTE PTR DS:[ESI],AL 005050B4 |. |INC ESI 005050B5 |. |SHL CL,5 005050B8 |> |MOV BYTE PTR DS:[ESI],CL 005050BA |. |JMP SHORT WINZIP32.005050BF 005050BC |> |OR BYTE PTR DS:[ESI],CL ; Case 7 of switch 0050506B 005050BE |. |INC ESI 005050BF |> |MOV EBX,DWORD PTR SS:[EBP-4] ; Default case of switch 0050506B 005050C2 |. |DEC EDI 005050C3 |. |INC DWORD PTR SS:[EBP+8] 005050C6 |. |INC DWORD PTR SS:[EBP+C] 005050C9 |> |TEST EDI,EDI 005050CB |.^\JG WINZIP32.00505011 |
Ok, cerchiamo di spiegare in modo organico: cosa succede
all’inizio del loop? In pratica prende i caratteri dell’username
e sottrae al codice ascii un valore predefinito in base al valore del carattere
preso dal serial. Se per esempio il carattere è un numero (ed è
quindi compreso tra 0x30 e 0x39) gli sottrae 0x1A e così via. Questo
serve ad avere un valore dove i bit significativi sono massimo 5. Dopo di
questo, nella seconda parte del loop se notate c’è uno switch
con 8 case.. 8 come i bit che formano un byte no? =) Comunque, qua praticamente
cosa fa: immaginate di eseguire
Abbiamo questi 3 byte
10011011 11110101 10111100
Tramite la prima parte del loop li trasformiamo in:
00011011 00010101 00011100
Tramite la seconda parte del loop, togliamo i 3 bit iniziali e trasformiamo il tutto in questo:
11011 10101 11100
Quindi ora il primo byte, tanto per fare un esempio, sarà:
11011101
E così via per tutti gli altri byte. Come vedete è ben diverso da quello che avevamo prima (100011011). Però i caratteri sono multipli di 5 (al contrario del mio esempio) e quindi alla fine togliendo 3 bit da ogni byte, si riesce a formare un numero “giusto” di byte (non rimangono dei bit in più che non raggiungono l’ottava insomma, come invece è accaduto nel mio esempio). Va bene, spero che sia chiaro, perché era una parte importante =) (in caso contrario rileggete il tutto o mandatemi una mail).
Eccoci arrivati ad un’altra parte importante: la generazione di un particolare byte. Fino a qui, abbiamo visto che il programma prende il nostro serial, lo modifica opportunamente per ottenere una stringa lunga 120bit (15 caratteri ASCII). Questa stringa, il nostro “serial numerico”, ha un byte finale che deve essere molto particolare. Il programma, nel pezzo di codice qui sotto, si prefigge il compito di controllare la sua effettiva validità; vediamo secondo quali parametri:
005040A5 |> /MOV EAX,DWORD PTR DS:[EDI] ; vediamo quanto vale edi tra poco.. 005040A7 |. |TEST EAX,EAX 005040A9 |. |MOV BL,1 ; attenzione a questo mov, è da mettere nel keygen 005040AB |. |JLE SHORT WINZIP32.005040E3 005040AD |. |MOV DWORD PTR SS:[EBP-148],EAX 005040B3 |> |/MOV EAX,DWORD PTR SS:[EBP-140] 005040B9 |. ||PUSH 8 005040BB |. ||CDQ 005040BC |. ||POP ECX 005040BD |. ||IDIV ECX 005040BF |. ||MOV ESI,EDX 005040C1
|. ||MOV EDX,80
; 0x80 = 005040C6 |. ||MOV ECX,ESI 005040C8 |. ||SAR EDX,CL ; shr edx, cl 005040CA |. ||TEST BYTE PTR SS:[EBP+EAX-14],DL ; ebp+eax-14 = il nostro serial numerico 005040CE |. ||JE SHORT WINZIP32.005040D2 ; jump se il bit = 0 005040D0 |. ||INC BL 005040D2 |> ||INC DWORD PTR SS:[EBP-140] 005040D8 |. ||DEC DWORD PTR SS:[EBP-148] 005040DE |.^|\JNZ SHORT WINZIP32.005040B3 005040E0 |. |MOV ESI,DWORD PTR SS:[EBP+8] 005040E3 |> |MOVZX EAX,BL ; il numero di bit in eax 005040E6 |. |CDQ 005040E7 |. |PUSH 2 005040E9 |. |POP ECX 005040EA |. |IDIV ECX ; dividilo per 2 005040EC |. |MOV ECX,DWORD PTR SS:[EBP-144] ; numero che vale 7 e che viene decrementato ad ogni loop 005040F2 |. |MOV AL,BYTE PTR SS:[EBP-5] ; ultimo byte del serial numerico in al 005040F5 |. |SHL DL,CL 005040F7 |. |XOR AL,DL 005040F9 |. |TEST DL,AL ; (ultimo byte) and (valore di dl) 005040FB |. |JNZ WINZIP32.0050418B ; se salta, beggar off 00504101 |. |ADD EDI,4 ; passa alla dword successiva 00504104 |. |DEC DWORD PTR SS:[EBP-144] ; decrementa il numero di cui parlavo prima 0050410A |. |CMP EDI,WINZIP32.0054B734 ; ASCII "WZRFWZUD" 00504110 |.^\JL SHORT WINZIP32.005040A5 ; quando il numero è minore di 3, smette il loop |
Allora, questo come vedete è un doppio loop. Vi spiego
ora come funziona. Sostanzialmente, prende il numero 0x80 (che è un
numero che trasformato in binario vale 1000000) e facendo un and (‘test’)
controlla se il primo bit del primo byte del nostro “serial numerico”
è uguale a 1. Se lo è, il je non salta e bl, che tiene il conto
di tutti i bit che valgono 1 nei primi 25 bit del serial numerico, viene incrementato.
Perché ho detto i primi 25 bit? Vediamo un po’ cosa contiene
edi: le prime 4 dword sono uguali a 0x19 e l’ultima a 0x14. Se sommate
tutto, vedete che viene 0x78, ovvero 120. Come i nostri bit =). Da questo
capiamo che il winzip conta quanti bit = 1 ci sono nei primi 25 bit del serial
numerico, poi nei successivi 25 e cosi via finché non ne rimangono
solo più 20. Oltretutto, quel 0x80 viene dimezzato ad ogni loop: questo
perché 0x40 vale
Ok, abbiamo quasi finito, tenete duro =). Se guardiamo il resto della call in cui siamo, vediamo due chiamate interessanti: RSA_public_decrypt e SHA1.
RSA_private_encrypt, RSA_public_decrypt - low level signature operations
#include <openssl/rsa.h>
int RSA_private_encrypt(int flen, unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
int RSA_public_decrypt(int flen, unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
These functions handle RSA signatures at a low level.
RSA_private_encrypt() signs the flen bytes at from (usually a message digest with an algorithm identifier) using the private key rsa and stores the signature in to. to must point to RSA_size(rsa) bytes of memory.
padding denotes one of the following modes:
RSA_PKCS1_PADDING
PKCS #1 v1.5 padding. This function does not handle the algorithmIdentifier specified in PKCS #1. When generating or verifying PKCS #1 signatures, rsa_sign(3) and rsa_verify(3) should be used.
RSA_NO_PADDING
Raw RSA signature. This mode should only be used to implement cryptographically sound padding modes in the application code. Signing user data directly with RSA is insecure.
RSA_public_decrypt() recovers the message digest from the flen bytes long signature at from using the signer's public key rsa. to must point to a memory section large enough to hold the message digest (which is smaller than RSA_size(rsa) - 11). padding is the padding mode that was used to sign the data.
RSA_private_encrypt() returns the size of the signature (i.e., RSA_size(rsa)). RSA_public_decrypt() returns the size of the recovered message digest.
On error, -1 is returned; the error codes can be obtained by err_get_error(3).
Questa è la descrizione delle funzioni RSA_public_decrypt e del complementare RSA_private_encrypt (quest’ultimo lo useremo nel keygen). Quindi il nostro famoso “serial numerico” deve essere un valore ritornato da RSA_private_encrypt, dopodichè il programma provvederà a decriptarlo e a ottenere un numero. Vediamo tutto questo, e vediamo pure tra poco cosa se ne fa di questo numero:
00504112 |. |PUSH 3 00504114 |. |PUSH DWORD PTR DS:[ESI+4] 00504117 |. |LEA EAX,DWORD PTR SS:[EBP-24] 0050411A |. |PUSH EAX ; valore di ritorno, facciamo un Follow in dump su EAX 0050411B |. |LEA EAX,DWORD PTR SS:[EBP-14] 0050411E |. |PUSH EAX 00504121 |. |POP ESI 00504122 |. |PUSH ESI 00504123 |. |CALL DWORD PTR DS:[55EF28] ; wzeay32.RSA_public_decrypt |
Se avete fatto il Follow un dump, tenete presente il valore della prima dword, perché la ritroviamo a breve..
Andando poco più in giù nel listato, c’è la funzione di hashing SHA1. Fossi in voi, avrei già intuito cosa sta per succedere, ma non vi rovino ancora la sorpresina :P.
SYNOPSIS
#include <openssl/sha.h>
unsigned char *SHA1(const unsigned char *d, unsigned long n,
unsigned char *md);
DESCRIPTION
SHA1()
computes the SHA-1 message digest
of the n bytes at
d and places it in md (which must have space for SHA_DIGEST_LENGTH
== 20 bytes of output). If md is NULL, the digest is placed in a static array.
Ora che sappiamo che parametri prende la funzioni di hash, vediamo il disasm:
00504140 |. |PUSH EAX ; variabile dove contenere l’hash 00504141 |. |PUSH DWORD PTR SS:[EBP+10] ; lunghezza dell’hash (7 nel caso di Graftal) 00504144 |. |PUSH DWORD PTR SS:[EBP+C] ; stringa da hashare (graftal in LOWERCASE mi raccomando) 00504147 |. |CALL DWORD PTR DS:[55EF20] ; call a SHA1 hash 0050414D |. |PUSH ESI 0050414E |. |LEA EAX,DWORD PTR SS:[EBP-14] 00504151 |. |PUSH 0 00504153 |. |PUSH EAX 00504154 |. |CALL WINZIP32.004E68F0 00504159 |. |PUSH 0E 0050415B |. |LEA EAX,DWORD PTR SS:[EBP-38] 0050415E |. |PUSH EAX 00504162 |. |PUSH EAX 00504163 |. |CALL WINZIP32.004E7E20 00504168 |. |PUSH ESI 00504169 |. |LEA EAX,DWORD PTR SS:[EBP-24] 0050416D |. |LEA EAX,DWORD PTR SS:[EBP-14] 00504170 |. |PUSH EAX 00504171 |. |CALL WINZIP32.004E95C0 ; qui c’è il check vero e proprio |
Tutto ciò non era proprio fondamentale, la parte importante la troviamo nella call che vi ho segnato e che ora andiamo ad analizzare, anche se non molto a fondo, siccome è inutile:
----- ~ snip ~ ----- 004E961D |. REPE CMPS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI] ; esi = hash del nostro username, edi = hash ricavato dal serial 004E961F |. JE SHORT WINZIP32.004E9648 ; se sono uguali, salta e il serial è corretto, olè! ----- ~ snip ~ ----- |
Ehi ehi ehi! Che cosa vedo! =)
In edi è presente il valore trovato dalla funzione RSA_public_decrypt! Ma che bello eh? Ecco svelato il “segreto”: se la funzione RSA ritorna l’hash del nostro username, siamo registrati =).
Va bene, noi abbiamo parlato tanto
di RSA ma.. non pensate manchi qualcosa ancora per completare il quadro? Eh
già, e le chiavi? Dove le mettiamo? Quali sono? =). Tutte domande a
cui risponderò a breve. Per prima cosa, vi ricordate che il programma
aveva due modalità di registrazione,
----- ~ snip ~ ----- 00504112 |. 6A 03 |PUSH 3 00504114 |. FF76 04 |PUSH DWORD PTR DS:[ESI+4] ; Follow in dump su questa variabile 00504117 |. 8D45 DC |LEA EAX,DWORD PTR SS:[EBP-24] 0050411A |. 50 |PUSH EAX 0050411B |. 8D45 EC |LEA EAX,DWORD PTR SS:[EBP-14] 0050411E |. 50 |PUSH EAX 00504121 |. 5E |POP ESI 00504122 |. 56 |PUSH ESI 00504123 |. FF15 28EF5500 |CALL DWORD PTR DS:[55EF28] ; RSA_public_decrypt ----- ~ snip ~ ----- |
Siccome è una strutture di bignum, può confondere un po’ le idee all’inizio, ma guardando poco più in basso al nostro bel Follow in dump possiamo notare una bella fila di numeri lunghi 0x0F che sono pure puntati poco più in alto nella struttura. Guarda caso la funzione prende 0x0F byte.. Sarà un caso? Naa :P. Segnatevi, ovviamente al contrario, quei byte e ecco qua la prima chiave, il nostro N per il SU_STD: E31557F66498F138597E6EA3ABDCEF.
Ripetendo la stessa cosa per il SU_PRO: 9D845872E117501775DD51D9044945.
Adesso usate l’RSA Tool v2 (se non l’avete fate un bel google e trovatelo, è relativamente conosciuto) e ditegli di trovare p e q, senza preoccuparvi dell’esponente che è il solito 0x10001. Vi scrivo qua tutti i dati:
SU_STD:
n = E31557F66498F138597E6EA3ABDCEF
p = EC5A57BA9E77CFB
q = F5F5BE226C8F59D
e = 0x10001
d = 16B67CCCD53502F0870D775052FB49
SU_PRO:
n = 9D845872E117501775DD51D9044945
p = C2C17D26DBC6E83
q = CF0D008F6995E97
e = 0x10001
d = 1E84F8063A2AA42B29DD5578421E79
Sono stati tutti ricavati dal RSA Tool. Con questi dati, possiamo finalmente codare il keygen =).
~ KeyGen ~
Per quanto riguarda il keygen, non c’è molto da dire: vi riassumo qua i passi per il keygen e vi allego il src al keygen fatto da me. Non me ne vogliate, io avevo cominciato a scrivere il keygen in asm, ma quando ho deciso di implementare le OpenSSL ho preferito scriverlo in C: di conseguenza certi pezzi sono fatti in asm nel sorgente, ma si capiscono piuttosto bene; diciamo solo che è anti-estetico. E poi se non capite quelli, cosa vi mettete a reversare? :P
Ok allora, il nostro keygen deve fare esattamente questo:
1) Prendere l’username e metterlo in lowercase.
2) Fare l’hash della stringa ottenuta.
3) Inizializzare le chiavi RSA.
4) Fare un RSA_private_encrypt sul nostro hash.
5) Creare l’ultimo byte contando i bit = 1 presenti negli altri.
6) Creare il serial in caratteri facendo il processo inverso fatto dal programma e mettendo i trattini.
7) Tutto questo per tutti e due i tipi di serial ovviamente, standard e professional.
Ecco, come compito a casa, codate un keygen bello pulito e inviatemi i src come al solito alla mail latfarg (at) gmail (dot) com =).
Graftal
Note finali |
Questo era il tutorial, spero vi sia piaciuto :P. Ho messo un bel po’ a scriverlo per penuria (:P) di tempo, ma alla fine cel’ho fatta. Vorrei ringraziare tutti quelli che conosco, in particolare Eimiar, il mitico evilcry ;), il clan di UT2004 di cui faccio parte e come sempre il Que che pubblica molto gentilmente il mio materiale :P.
Se avete commenti, consigli, dubbi inviate tutto a latfarg(at)gmail(dot)com =)
Alla prossima.
Disclaimer |
Vorrei ricordare che il software va comprato e non rubato, dovete registrare il vostro prodotto dopo il periodo di valutazione. Non mi ritengo responsabile per eventuali danni causati al vostro computer determinati dall'uso improprio di questo tutorial. Questo documento è stato scritto per invogliare il consumatore a registrare legalmente i propri programmi, e non a fargli fare uso dei tantissimi file crack presenti in rete, infatti tale documento aiuta a comprendere lo sforzo che ogni sviluppatore ha dovuto portare avanti per fornire ai rispettivi consumatori i migliori prodotti possibili.
Reversiamo al solo scopo informativo e per migliorare la nostra conoscenza del linguaggio Assembly.