07/11/2011
In questo articolo mostriamo un semplice reverse-engineering e crack di uno shareware (non ne riporterò il nome, ho oscurato parzialmente le immagini :-) ). Il gioco permette un massimo di 30 partite dopodichè dovremo registrare il prodotto.
Lo scopo del tutorial sarà quello di aggirare tale limitazione "registrando" a modo nostro il gioco. Per prendere un pò di dimestichezza con l'assembly delle architetture x86 vedere questi link:
http://en.wikibooks.org/wiki/X86_Assembly
http://www.cs.virginia.edu/~evans/cs216/guides/x86.html
http://win32assembly.programminghorizon.com/links.html
Andremo ad usare il famoso debugger Windows OllyDbg 1.1.
Il debugger deve avere il plugin della command line di Quequero, scaricabile qui.
Una volta avviato il programma abbiamo la seguente schermata:
sulla barra in alto notiamo la scritta "Non registrato, versione di valutazione (30 partite)" a conferma del fatto che in effetti la limitazione consiste proprio nel numero massimo di partite che è possibile giocare. Se clicchiamo su "?" notiamo la voce di menu "Registrazione" la quale ci porta a questo popup:
Da questo popup capiamo che la registrazione è il classicissimo sistema [username,chiave], dove "username" è scelto dall'utente e "chiave" è appunto il seriale che ci viene fornito acquistando regolarmente il programma. Inserendo dei dati a caso otteniamo:
A questo punto abbiamo capito molto di più: il seriale è funzione dell'username e da qualche parte nel codice deve esistere un controllo di corrispondenza tra il seriale introdotto da noi e quello calcolato dal programma stesso. Ciò significa che esiste un punto all'interno del programma dove viene effettuato un controllo tra il seriale inserito con quello calcolato: dall'esito di tale controllo abbiamo due possibilità: se il seriale inserito è errato triggeriamo il messaggio di errore (imbocchiamo sempre questa strada se non conosciamo il seriale) altrimenti otteniamo la registrazione corretta del gioco.
Il nostro scopo sarà quello di individuare tale punto e modificarlo in maniera tale da forzare sempre la seconda via (quella della registrazione) indipendentemente dal seriale che inseriamo.
Iniziamo ora ad indagare più approfonditamente nel codice:
Il messaggio di errore che otteniamo quando inseriamo un seriale errato non è altro (in questo caso) che il risultato di una chiamata alla MessageBoxA, la tipica funzione API di notifica degli errori registrata all'interno di User32.dll.
Per una lista completa delle API Windows vedere questo link
Come inizio, per avvicinarci al punto in cui viene controllato il seriale, possiamo intanto cercare di piazzare un breakpoint sulla call che invoca MessageBoxA e poi controllare nei dintorni del codice.
Apriamo OllyDbg, carichiamo l'eseguibile e attendiamo che il debugger finisca l'analisi del codice macchina. A questo punto premiamo Alt+F1 per far apparire la linea di comando e piazziamo il breakpoint sulla chiamata a MessageBoxA, cioè scriviamo
bpx MessageBoxA
Adesso premiamo F9 per far partire il programma. Comparirà la schermata inziale. Andiamo su "?" e poi su "Registrazione", inseriamo i soliti dati a caso e clicchiamo su "Ok". Il debugger blocca il programma alla locazione 77D5050B all'interno del modulo USER32.dll di Windows, modulo che contiene molte delle API del sistema operativo tra cui la nostra MessageBoxA.
A questo punto osserviamo come è stato caricato lo stack (finestra in basso a destra di ollydbg); questi sono gli ultimi dati inseriti:
73D9DE23 CALL to MessageBoxA from MFC42.73D9DE1D 00110526 |hOwner = 00110526 (''Registra ***********'',class=''#32770'',parent=00050514) 0041C82C |Text = "Mi dispiace, registrazione non corretta." 0041C860 |Title = "Errore" 00000000 \\Style = MB_OK|MB_APPLMODAL 004182C8 ********.004182C8 00406124 RETURN to ********.00406124 from <JMP.&MFC42.#4224> 0041C82C ASCII "Mi dispiace, registrazione non corretta." 0041C860 ASCII "Errore"
E' chiaro che sono stati appena caricati i dati relativi ad una finestra di errore e che è stata invocata la funzione MessageBoxA nella libreria USER32.dll. A noi non interessa tanto ispezionare il codice da qui in poi (sappiamo già che steppando in avanti con F8 verrà visualizzato il messaggio di errore), quanto invece vedere in che punto del modulo del programma è stata fatta la richiesta di visualizzazione del popup, perchè è proprio in quel punto che avviene il controllo.
Per far questo osserviamo che nello stack, alla settima riga, è presente (ovviamente) l'indirizzo di ritorno al chiamante, ovvero l'indirizzo 00406124:
00406124 RETURN to ********.00406124 from <JMP.&MFC42.#4224>
Evidenziamo questa riga, premiamo il tasto destro del mouse e selezioniamo "Follow in disassembler". Il debugger ci mostra il punto di chiamata che il nostro programma invoca in caso di errore. Notiamo che tale chiamata è inserita all'interno di una funzione che inizia a partire da 00406111. Eccola qua:
00406111 PUSH 0 00406113 PUSH ********.0041C860 ;ASCII "Errore" 00406118 PUSH ********.0041C82C ;ASCII "Mi dispiace, registrazione non corretta." 0040611D MOV ECX,EBP 0040611F CALL <JMP&MFC42.#4224>; ; punto di chiamata a USER32.DLL 00406124 MOV ECX,DWORD PTR SS:[ESP+30C] 0040612B POP EDI 0040612C POP EBP 0040612D MOV DWORD PTR FS:[0],ECX 00406134 ADD ESP,310 0040613A RETN
La funzione non fa altro che caricare lo stack con i parametri da passare alla MessageBoxA e cioe "MB_OK|APPL_MODAL", la stringa di titolo ("Errore") e la stringa del corpo del messaggio ("Mi dispiace, registrazione non corretta"). Questo è il punto del programma in cui i dati relativi al messaggio di errore vengono inseriti nello stack. Osservando bene il listato prodotto dal debugger notiamo che l'istruzione alla locazione 00406111 (PUSH 0) è stata raggiunta tramite un salto di qualche tipo (simbolo ">" accanto a "PUSH 0"). Scorrendo in alto nel codice individuiamo i punti di salto, e cioè:
00405FB7 JB ********.00406111
e, un pò più in basso:
0040605F JE ********.00406111
Questo significa che esistono due punti di salto che ci fanno raggiungere il messaggio di errore! Il primo salto (JB) è preceduto da un test un pò sospetto per via del valore immediato 6:
CMP ECX,6
Per indagare sulla natura di questo controllo occorre risalire tutta la funzione e piazzare un breakpoint all'inizio, e cioè alla locazione 00405F60. Fatto questo chiudiamo il programma e riavviamo l'esecuzione con F9. Andiamo in "?" e di nuovo in "Registrazione" ed inseriamo altri dati a caso. Premendo "Ok" ci fermiamo correttamente in 00405F60, cioè all'inizio della nostra funzione. Steppando con F8 osserviamo che in ECX viene effettivamente caricata la lunghezza in caratteri dell'username da noi inserito. A questo punto è chiaro che lo scopo delle due istruzioni<
00405FB4 CMP ECX,6 00405FB7 JB ********.00406111
è quello di controllare la lunghezza dell'username. Nel caso in cui questa risultasse strettamente inferiore (JB = Jump if Below) a 6 verrebbe invocato il salto giungendo in 00406111, cioè al messaggio di errore. Ecco quindi svelato un primo controllo: l'username deve essere almeno di 6 caratteri.
Chiudiamo il programma, e riavviamo con F9. Ritorniamo alla registrazione avendo cura di introdurre un username a caso ma lungo almeno 6 caratteri e premiamo "Ok". Il debugger ci riporta alla funzione di prima ma adesso siamo sicuri di superare il controllo, ed infatti steppando oltre con F8 così avviene.
Superato il controllo entriamo nella parte più interessante e cioè quella in cui presumibilmente viene generato il seriale e poi confrontato con quello da noi inserito. Analizziamo in dettaglio tutta la funzione e per ogni parte ne diamo una breve descrizione: L'entry-point della funzione è all'indirizzo 00405F60. All'inizio vengono invocate due CALL alla API GetWindowTextA per il recupero dell'username e del seriale introdotto nei form:
00405F60 MOV EAX,DWORD PTR FS:[0] 00405F66 PUSH -1 00405F68 PUSH ********.00415BCB 00405F6D PUSH EAX 00405F6E MOV DWORD PTR FS:[0],ESP 00405F75 SUB ESP,304 00405F7B LEA EAX,DWORD PTR SS:[ESP+4] 00405F7F PUSH EBP 00405F80 MOV EBP,ECX 00405F82 PUSH EDI 00405F83 PUSH 100 00405F88 MOV ECX,DWORD PTR SS:[EBP+60] 00405F8B PUSH EAX 00405F8C CALL <JMP.&MFC42.#3873> ; GetWindowTextA (recupero username) 00405F91 LEA ECX,DWORD PTR SS:[ESP+20C] 00405F98 PUSH 100 00405F9D PUSH ECX 00405F9E MOV ECX,DWORD PTR SS:[EBP+64] 00405FA1 CALL <JMP.&MFC42.#3873> ; GetWindowsTextA (recupero seriale) 00405FA6 LEA EDI,DWORD PTR SS:[ESP+C]
A questo punto viene utilizzata l'istruzione REPNE SCAS per ottenere la lunghezza della stringa puntata da ES:[EDI], cioè l'username. La lunghezza viene determinata semplicemente andando a contare il numero di caratteri prima del terminatore \0 di stringa.
00405FAA OR ECX,FFFFFFFF ; inizializza ECX 00405FAD XOR EAX,EAX ; EAX = 0 00405FAF REPNE SCAS BYTE PTR ES:[EDI] ; trova il terminatore di stringa 00405FB1 NOT ECX ; complemento a uno... 00405FB3 DEC ECX ; ...di ECX
Adesso ECX contiene la lunghezza dell'username. Le prossime due istruzioni verificano che sia superiore o uguale a 6, altrimenti si va in 00406111, cioè al messaggio di errore:
00405FB4 CMP ECX,6 00405FB7 JB ********.00406111
Fatto ciò viene generato il seriale a partire dall'username, ad esempio con l'username "gianluca" viene generato il seriale "QWHMEFKC":
00405FBD MOV EAX,DWORD PTR DS:[41D1C8] 00405FC2 PUSH EBX 00405FC3 LEA EDX,DWORD PTR SS:[ESP+10] 00405FC7 PUSH ESI 00405FC8 PUSH EDX 00405FC9 LEA ECX,DWORD PTR DS:[EAX+FC] 00405FCF CALL ********.004010FA 00405FD4 MOV ECX,DWORD PTR DS:[41D1C8] 00405FDA ADD ECX,0FC 00405FE0 CALL ********.00401438 00405FE5 MOV EAX,DWORD PTR DS:[41D1C8] 00405FEA LEA EDX,DWORD PTR SS:[ESP+114] 00405FF1 PUSH 100 00405FF6 PUSH EDX 00405FF7 LEA ECX,DWORD PTR DS:[EAX+FC] 00405FFD CALL ********.00401168
Si fanno ora puntare i registri ESI e EAX rispettivamente al seriale inserito da noi e quello generato internamente:
00406002 LEA ESI,DWORD PTR SS:[ESP+114] 00406009 LEA EAX,DWORD PTR SS:[ESP+214]
Adesso avviene il controllo di eguaglianza tra le due stringhe. Questo pezzo di codice controlla passo a passo le coppie di caratteri corrispondenti nei due seriali. Se risultano tutti identici si salta alla locazione 00406034 altrimenti si va in 00406038:
00406010 MOV DL,BYTE PTR DS:[EAX] ; carica la prima coppia di caratteri 00406012 MOV BL,BYTE PTR DS:[ESI] 00406014 MOV CL,DL 00406016 CMP DL,BL 00406018 JNZ SHORT ********.00406038 ; se i caratteri sono diversi esci 0040601A TEST CL,CL 0040601C JE SHORT ********.00406034 ; prima stringa terminata? se si esci 0040601E MOV DL,BYTE PTR DS:[EAX+1] ; carica la seconda coppia di caratteri 00406021 MOV BL,BYTE PTR DS:[ESI+1] 00406024 MOV CL,DL 00406026 CMP DL,BL 00406028 JNZ SHORT ********.00406038 ; se i caratteri sono diversi esci 0040602A ADD EAX,2 0040602D ADD ESI,2 ; avanze di 2 in entrambe le stringhe 00406030 TEST CL,CL 00406032 JNZ SHORT ********.00406010 ; prossimi due caratteri
Veniamo adesso al punto critico della fase di registrazione. Come abbiamo già accennato, quando il precedente pezzo di codice termina può portarci o in 00406034 oppure in 00406038. Nel primo caso il registro EAX viene caricato con 0 (XOR EAX,EAX) e poi si prosegue in 0040603D. Nell'altro caso il registro EAX viene inizializzato con -1. In ogni caso poi si prosegue a partire da 0040603D. EAX diviene quindi la flag del controllo di prima: se vale 0 è stato inserito un seriale corretto, se vale -1 il seriale era sbagliato.
00406034 XOR EAX,EAX ; OK! Il seriale era giusto e quindi 00406036 JMP SHORT ********.0040603D ; poniamo EAX = 0 e saltiamo in 0040603D 00406038 SBB EAX,EAX ; Il seriale era sbagliato e quindi 0040603A SBB EAX,-1 ; poniamo EAX = -1 e andiamo avanti
Il programma ora prosegue controllando il valore di EAX e agendo di conseguenza. Se vale 0 allora verrà invocata la registrazione del programma, se vale -1 verrà lanciato il popup di errore.
0040603D MOV EDX,DWORD PTR DS:[41D1C8] 00406043 XOR ECX,ECX 00406045 TEST EAX,EAX 00406047 SETE CL 0040604A MOV DWORD PTR DS:[EDX+73C],ECX 00406050 MOV EAX,DWORD PTR DS:[41D1C8] 00406055 POP ESI 00406056 POP EBX 00406057 MOV ECX,DWORD PTR DS:[EAX+73C] 0040605D TEST ECX,ECX 0040605F JE ********.00406111 ; vai al messaggio di errore 00406065 LEA ECX,DWORD PTR SS:[ESP+C] ; da qui in poi abbiamo... 00406069 PUSH ECX ; ...il processo di registrazione 0040606A LEA ECX,DWORD PTR DS:[EAX+70C] 00406070 CALL <JMP.&MFC42.#860> 00406075 MOV EAX,DWORD PTR DS:[41D1C8] 0040607A LEA EDX,DWORD PTR SS:[ESP+C] 0040607E PUSH EDX 0040607F LEA ECX,DWORD PTR DS:[EAX+FC] 00406085 CALL ********.00401537 0040608A PUSH ********.0041C87C ; ASCII "************" 0040608F LEA ECX,DWORD PTR SS:[ESP+C] 00406093 CALL <JMP.&MFC42.#537> 00406098 PUSH ********.0041C86C ; ASCII " - Registrato" 0040609D LEA ECX,DWORD PTR SS:[ESP+C] 004060A1 MOV DWORD PTR SS:[ESP+318],0 004060AC CALL <JMP.&MFC42.#941> 004060B1 PUSH ********.0041C868 ; ASCII " a " 004060B6 LEA ECX,DWORD PTR SS:[ESP+C] 004060BA CALL <JMP.&MFC42.#941> 004060BF LEA ECX,DWORD PTR SS:[ESP+C] 004060C3 PUSH ECX 004060C4 LEA ECX,DWORD PTR SS:[ESP+C] 004060C8 CALL <JMP.&MFC42.#941> 004060CD MOV EDX,DWORD PTR SS:[ESP+8] 004060D1 MOV ECX,DWORD PTR DS:[41D1C8] 004060D7 PUSH EDX 004060D8 CALL <JMP.&MFC42.#6199> 004060DD PUSH 0 004060DF MOV ECX,EBP 004060E1 CALL <JMP.&MFC42.#2645> 004060E6 LEA ECX,DWORD PTR SS:[ESP+8] 004060EA MOV DWORD PTR SS:[ESP+314],-1 004060F5 CALL <JMP.&MFC42.#800> 004060FA POP EDI 004060FB POP EBP 004060FC MOV ECX,DWORD PTR SS:[ESP+304] 00406103 MOV DWORD PTR FS:[0],ECX 0040610A ADD ESP,310 00406110 RETN ; fine registrazione
Forzare la registrazione è abbastanza semplice: è sufficiente forzare EAX = 0 anche nel caso di seriale errato. Abbiamo infatti scoperto che EAX = 0 è sinonimo di "I seriali erano identici" e quindi basta imporlo in luogo dell'istruzione che pone EAX = -1.
In pratica sarà sufficiente modificare il seguente codice:
00406038 SBB EAX,EAX 0040603A SBB EAX,-1
nel seguente:
00406038 XOR EAX,EAX 0040603A NOP 0040603B NOP 0040603C NOP
Operazione facile e indolore dato che le istruzioni di rimpiazzo non occupano più spazio di quelle da sostituire (sempre 5 byte in tutto). Con questa modifica imponiamo EAX = 0 sia nel caso di seriale corretto che errato e quindi forziamo il programma ad effettuare la registrazione. Ecco uno snapshot della registrazione; notare il codice macchina in rosso corrispondente alla parte modificata: