Zoom Icon

Lezione 7 Keygening

From UIC Archive

Keygening

Contents


Lezione 7 Keygening
Author: anonymous
Email: .
Website: .
Date: 15/10/2008 (dd/mm/yyyy)
Level: Some skills are required
Language: Italian Flag Italian.gif
Comments:



Introduzione

Salve a tutti. In questa lezione cercherò di farvi affrontare un argomento molto interessante: la costruzione di generatori di chiavi in grado di registrare i programmi, tipicamente shareware.


Tools

Lezione 7: il crackme che useremo
OllyDbg
IDA
gcc: il gcc è incluso in dev-cpp
Masm: il compilatore assembly


Essay

Keygen

Un keygen è un (normalissimo) software in grado di produrre dei codici (in qualsiasi formato, sia che abbiano la forma di un file, sia che siano una sequenza di numeri e lettere) che possono “registrare” uno o più programmi. Il termine “registrare” sta a significare che il codice prodotto dal keygen viene validato dal software targa che, da quel momento in poi, muterà il suo comportamento in modo da eliminare dei vincoli o delle restrizioni che erano precedentemente imposti.

Come funziona?

Sostanzialmente, la difficoltà sta nel fatto che esistono moltissime tipologie di software targa da registrare con un keygen; il tipo di protezione che viene utilizzata determina anche il grado di difficoltà che si avrà per costruire un keygen in grado di rimuoverla. Per questo motivo, ho intenzione di far sfumare questo paragrafo per integrarlo, a pezzetti, in quelli successivi, spiegando, di volta in volta, quali sono i principi di funzionamento di un keygen, man mano che ne incontrerò l’esigenza.

La protezione più semplice

Ovvero quella che analizzeremo e crackeremo con questo tutorial. Di seguito vi mostrerò come son fatti i programmi shareware più stupidi della Terra. Tipicamente, per costruire un keygen per software di questo tipo son sufficienti 20 o 30 minuti di lavoro. Molto spesso, un software propone all’utente una schermata che gli chiede di inserire un nome utente (tipicamente uno pseudonimo, un nickname o, spesso, un indirizzo e-mail) e un codice di attivazione. Nel caso più comune, il nome utente è del tutto a scelta (più o meno vincolata o condizionata) dell’utilizzatore; il codice di attivazione invece può essere in funzione del nome utente precedentemente inserito oppure no. In entrambi i casi, il software “stupido” agisce circa in questo modo:

  1. Chiede il nome utente U
  2. Chiede il codice di attivazione S;
  3. Genera un proprio codice X, che può dipendere ( X = f(U) ) o meno ( X = f(…) ) da U, invisibile all’utente, tale che X registra il programma correttamente;
  4. Verifica se S = X: se l’uguaglianza è vera, il software passa nella modalità registrato, altrimenti notifica un errore (opzionalmente).

Si vede subito che, per verificare la correttezza del codice immesso dall’utente, il programma targa ne genera uno valido, per poi confrontarlo semplicemente con quello inserito da tastier all’atto della registrazione. Per i geni di turno, è evidente che la funzione f è il generatore di chiavi; in breve, il programma da registrare contiene, al suo interno, l’esatta procedura che fornisce un seriale corretto. Il lavoro di questo caso consiste nell’individuare tale procedura e replicarla, incapsulata in un programma esterno, per farne un keygen. Dato che, per ora, vi ho già rotto le scatole con un sacco di informazioni teoriche, mettiamo un po’ da parte i libri e

Iniziamo l’opera!

Il software che vi presento chiede un codice di attivazione che dipende esclusivamente dal nome utente. Il software targa è stato progettato (grazie alla pazienza di un mio amico) apposta per questo tutorial. Prendendo come esempio un caso mediamente frequente, è stato scritto usando le librerie MFC (in C++ dunque), con una semplice interfaccia grafica in ambiente Windows. Il software lo trovate come allegato di questo tutorial e prende il nome di “keygen_me.exe”.

Per prima cosa, si fa un’analisi superficiale del programma, iniziando ad eseguirlo. Ci troviamo di fronte a questa situazione:
Sshot-1 k.jpg
Evidentemente, nel primo campo va inserito il vostro nome e nel second campo, qualsiasi cosa che possa assomigliare a un qualsiasi codice di attivazione.Inserisco i dati, premo ‘Register’ e il risultato mi piace moltissimo:
Sshot-2 k.jpg
Una formidabile MessageBox che mi informa del fatto che i dati che ho inserito non rappresentano una registrazione valida. Vedete, quando una registrazione va male, il minimo comportamento eccitante potrebbe comportare la soluzione del programma targa. In questo particolare caso, il nostro lavoro sarà facilitato di molto perché il programma fa uso di una MessageBox. Se riuscissimo a captarla, potremmo facilmente individuare il punto del programma in cui si iniziano a testare i criteri di validità dei dati inseriti. E’ a questo punto che metteremo le mani su di un debugger, e, per questo tutorial, userò (useremo :S) Ollydbg. Chiudiamo il crackme e ri-apriamolo attraverso Ollydbg (attenzione, questo non è un corso sulle funzioni elementari di questo debugger, quindi non mi soffermerò molto nelle spiegazioni del suo funzionamento; supporrò invece che sappiate bene di cosa sto parlando; tuttavia richiamerò qualcosa per i più smemorati; lo stesso discorso vale per il linguaggio assembler). Appena caricato il processo, facciamo click sul tasto ‘Play’ e invochiamo (con Alt+F1) la command-line per settare un breakpoint proprio sulle funzioni MessageBoxA e MessageBoxW:
Sshot-3 k.jpg
In realtà, sapendo che il programma è scritto con le nuove MFC, non è sbagliato supporre che venga usata di default la versione unicode della funzione; nonostante ciò, non ci costa nulla settare un punto di arresto su entrambe le versioni, in modo da garantire che anche un programmatore scaltro non possa fregarci con queste cose stupide che potrebbero rovinarci la vita per giorni. Fatto questo, reinseriamo i dati nel programma, clicchiamo di nuovo su ‘Register’ e… POP! Il debugger interrompe l’esecuzione del programma proprio sulla prima istruzione della funzione MessageBoxW (versione unicode, come previsto).
Sshot-4 k.jpg
Arrivati a questo punto, diamo il fuoco al debugger e, con il tasto F8 (step over instruction), facciamo pedissequamente avanzare l’esecuzione delle istruzioni fino ad arrivare alla RETN 10, all’indirizzo 7E3E657A. Quando eseguiremo anche quest’ultima istruzione, andremo a finire nel punto immediatamente successivo a quello delle librerie MFC, che hanno invocato MessageBoxW. Bene.. facciamolo!
Sshot-5 k.jpg
Ritorniamo proprio all’indirizzo 78316909. Non è finita, non siamo ancora nel crackme (intuitivamente, anche se non è una regola, l’elevato valore del registro EIP, ci dice che non siamo in una zona di codice tanto comune). Per raggiungere il punto, dobbiamo usare F8 per eseguire altre due RETN: la prima a 78316920; la seconda a 78313790. Dopo l’esecuzione con F8 di quest’ultima, siamo finalmente nel crackme!
Sshot-6 k.jpg
Come vedete ci siamo (notate ora l’intuitiva diversità degli indirizzi a cui si trovano le istruzioni): 0040161A è l’indirizzo dello spazio di indirizzamento del programma sorgente compilato. Buttiamo via il debugger e, per un’analisi più accurata e più ad ampio raggio, passiamo a leggere il crackme da IDA disassembler. Apriamolo, disassembliamo il programma, premiamo G e inseriamo il succitato indirizzo virtuale. Siamo qua:
Sshot-7 k.jpg
Grazie all’aiuto di ida, possiamo facilmente notare che loc_401603 è l’inizio di una sequenza di operazioni che ci faranno inesorabilmente cadere nell’avviso d’errore. Come prima informazione, sappiamo che NON dobbiamo arrivare a questo punto. Per ricordarcene, posizioniamo il cursore lampeggiante sulla scritta in alto a sinistra (loc_401603), premiamo il tasto N (reName) e scriviamo qualcosa di mnemonico, come “Err_loc_01”. Subito dopo, continuiamo la nostra ricerca dell’inizio della procedura di validazione dei nostri dati. Dobbiamo raggiungere il punto in cui il programma è saltato a errore. Per farlo, posizioniamo il cursore lampeggiante in alto a destra (sulla scritta che vi ho cerchiato in rosso) e premiamo il tasto INVIO (quella scritta è il riferimento all’istruzione che ci ha portato a Err_loc_01:
Sshot-8 k.jpg
Come vedete, l’ultima istruzione ci ha portato all’errore. Guardiamo tre istruzioni prima: se quel jnz fosse stato eseguito, sicuramente non saremmo finiti a errore (presumibilmente, dato quello che abbiamo visto prima su ida, saremmo riusciti a registrare il crackme). Questo significa che prima di quel jnz, sono state fatte delle prove per vedere se i nostri dati erano validi e che queste non siano andate a buon fine. Allora dobbiamo studiare il codice che c’è sopra quei test, per cercare di capire che cosa succede. Appena sopra a questo codice, abbiamo questa situazione:
Sshot-9 k.jpg
E’ fantastico: c’è una wsprintf che mette insieme due interi senza segno, separandoli da un trattino. Intuitivamente, significa che un seriale valido ha una forma simile a “12345-12345”; mi ero sbagliato quando ho provato a prevedere che fosse composto da soli numeri. Beh, seguendo quanto vi ho detto all’inizio del documento, quando il programma arriva a quella wsprintf, non è difficile supporre che sono già state calcolate le parti destra e sinistra del seriale valido e che ora il programma vuole concatenarle per poi eseguire (nel loop che inizia a loc_4015D8) un confronto char per char con quello che abbiamo inserito noi da tastiera. Per esempio, ora, con un debugger, potremmo sniffare il seriale corretto, testando la memoria puntata da EAX e da ECX. Uno dei registri punterà al seriale che abbiamo inserito noi, l’altro, con ogni probabilità, punterà a quello corretto. Verificatelo se volete, per esercizio con olly (ripeto: questo documento non deve e non vuole insegnarvi a usare olly). Proprio più sopra del loop che inizia a loc_4015B8, c’è un altro loop, molto più interessante:
Sshot-10 k.jpg
Io dico subito che questo è il loop che calcola il seriale corretto, partendo dal nostro username. Non solo, a prima vista, sono anche in grado di affermare con certezza che, nella prima istruzione, word ptr [esp+edx*2+0Ch] è la parte che punta al nome utente che abbiamo immesso da tastiera. Di questa parte, EDX è il registro che funge da indice. Questa deduzione l’ho fatta secondo alcuni criteri non molto difficili da capire: prima di tutto, i char unicode hanno fatto sì che il compilatore usasse un puntatore a word, anziché a byte. In secondo luogo, in esp+edx*2+0Ch, EDX è l’unico addendo che non conta nulla nello stack; inoltre, a 004015AE, c’è un incremento di 1, tipico di un indice di un classico ciclo for. Non potendo, in questo caso, andare a caso, verifichiamo che quell’ambaradan di un’istruzione prelevi effettivamente i wchar del nome utente (mamma santa.. per prelevare i vari caratteri è stata compilata un’istruzione che spende 3 cicli di clock per carattere :S). Apriamo dunque Ollydbg, apriamo il programma targa, visualizziamo la command-line e con la riga bp 004015A0 mettiamo un breakpoint sull’istruzione movzx. Facciamo ‘play’, inseriamo i dati e clicchiamo su ‘Register’, nel crackme. Appena il debugger poppa, andiamo nella sua linea di comando e chiediamogli, gentilmente, di visualizzare i byte che partono dall’indirizzo esp+edx*2+c. Per farlo, diamo il comando db esp+edx*2+c.
Sshot-11 k.jpg
Ecco qua. Vi ho cerchiato in rosso la parte di buffer che ci dimostra che siamo riusciti a trovare il nome utente e, dunque, l’algoritmo che lo utilizza per il calcolo del seriale corretto (quella che inizialmente abbiamo soprannominato funzione f). Ritorniamo al codice assembler: .text:0040159D lea ecx, [ecx+0] .text:004015A0 .text:004015A0 loc_4015A0: .text:004015A0 movzx ebx, word ptr [esp+edx*2+0Ch] .text:004015A5 add ecx, ebx .text:004015A7 imul eax, ecx .text:004015AA mov ebx, eax .text:004015AC shr ebx, cl .text:004015AE add edx, 1 .text:004015B1 cmp edx, esi .text:004015B3 mov ecx, ebx .text:004015B5 jl short loc_4015A0 Adesso inizia il bello. Come vedete, questa cosa qua è molto scarsa.. sono un po’ di istruzioni in croce, senza alcuna sub-routine, senza codice incomprensibile, senza niente di niente. E’ veramente banale keygennare questo programma. Come vi avevo accennato poco fa, lo keygenneremo in un modo e mezzo: lo faremo in C, ma anche in assembler. E’ ora di motivare questa mia scelta. Vedete, per quanto possa sembrare disumano, moltissimi keygen sono più facili da fare in assembler (non è questo il caso; ora le due cose sono simil-equivalenti) perché, nel caso di procedure arzigogolate e lunghe, risulta spesso difficoltoso reinterpretare tutto il codice per poi riscriverlo in C. E’ per questo motivo che molti keygenner “professionisti” (non è ironico.. mi riferisco proprio a quelli che lo fanno per crew di cracking o anche su commissione o a pagamento ecc…) preferiscono fare un copia/incolla del listato assembler, aggiustare le cose che vanno aggiustate per farlo compilare, abbellire il tutto con una gradevole interfaccia grafica e compilare la torta! Beh, lo faremo anche noi per prova. Analizziamo prima di tutto il codice assembler. La prima istruzione ci fa capire che ci serve ECX nella computazione. Dato che è fuori dal loop, dobbiamo procurarcelo posizionando il debugger all’istruzione movzx. Magari ce ne servono altri, quindi ve li stampo tutti:
Sshot-12 k.jpg
Da cui ricaviamo che ECX deve essere uguale a 1. EDX funge da contatore, quindi dobbiamo metterlo a 0 (infatti è 0 anche nella figura). L’istruzione imul eax, ecx, ci suggerisce che anche EAX va inizializzato; dunque, dalla figura, vediamo che va anch’esso messo a 1. L’istruzione cmp edx, esi, ci fa notare che il contatore EDX viene comparato a ESI. Intuitivamente capiamo che ESI è il registro che contiene la lunghezza del nome utente. Ne troviamo conferma quando vediamo, dalla figura, che ESI = 6, proprio ad indicare la lunghezza del nome che ho inserito. Ora che sappiamo cosa dobbiamo aggiustare prima del calcolo, possiamo scrivere un programmino frettoloso che funzioni correttamente: .486 .model flat, stdcall option casemap: none

include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc

includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib

.data
       username db "quellochevolete", 0
       seriale  db 255 dup(0)
       formato  db "%u-%u", 0


.code main:

       ; serve il valore di ecx e di eax all'inizio
       ; della procedura di calcolo. li prelevo dal
       ; monitor del debugger, proprio mentre sono sopra
       ; l'istruzione movzx. edx continua a farci da contatore
       ; questa volta non moltiplicato per 2 perche' siamo ANSI.
       ; esi deve essere la lunghezza del nome utente.
       mov ecx, 1 ; aggiustata
       mov eax, 1 ; aggiustata
       mov edx, 0 ; aggiustata
       mov esi, 6 ; aggiustata

loc_4015A0:

       movzx ebx, byte ptr [username+edx] ; aggiustata
       add ecx, ebx
       imul eax, ecx
       mov ebx, eax
       shr ebx, cl
       add edx, 1
       cmp edx, esi
       mov ecx, ebx
       jl loc_4015A0
       ; come mostratoci dal programma, i numeri da concatenare
       ; per ottenere il serial sono memorizzati in eax e ecx,
       ; dal precedente algoritmo
       push eax
       push ecx
       push offset formato
       push offset seriale
       call wsprintf
       add esp, 16 ; sistemo lo stack dopo aver chiamato una funzione dichiarata con cdecl (cerca su msdn.microsoft.com)


       push 0
       push 0
       push offset seriale
       push 0
       call MessageBoxA
       push 0
       call ExitProcess

end main Signori, se compiliamo ed eseguiamo, otteniamo questo:
Sshot-13 k.jpg
Beh, a prima vista direi che ce l’abbiamo fatta! Voglio farvi notare che il keygen, anche se molto rudimentale, è stato fatto senza nemmeno prendere in considerazione che cosa facesse l’algoritmo per calcolare questi due valori. Se notate il codice assembler che ho ottenuto aggiustando le parti interessanti del listato, vi accorgerete che mi son limitato a replicare quanto c’era scritto nel disassemblatore. Ho lasciato invariata perfino l’etichetta del loop. Adesso facciamo una cosa un po’ più human-like e proviamo a fare la stessa operazione in C. A questo punto è necessario capire che succede per poi riscriverlo. Vediamo: loc_4015A0:

       movzx ebx, byte ptr [esp+edx*2+0Ch] ; prendiamo username[edx] in ebx
       add ecx, ebx                        ; ecx = ecx + ebx
       imul eax, ecx                       ; eax = ecx*eax
       mov ebx, eax                        ; ebx = eax
       shr ebx, cl                         ; ebx = ebx >> cl
       add edx, 1                          ; edx = edx + 1
       cmp edx, esi                        ; if(edx < esi)
       mov ecx, ebx                        ; ---- ecx = ebx
       jl loc_4015A0                       ; ritorna a loc_004015A0

In C, con tutte le inizializzazioni di EAX, ECX ed ESI, e con un nome utente variabile, il keygen diventa questo:

  1. include <stdio.h>
  2. include <stdlib.h>
  3. define MAXLOAD 1024

int main(int argc, char **argv) { char username[MAXLOAD]; unsigned int eax = 1; unsigned int ecx = 1; unsigned int edx = 0; unsigned int esi; unsigned int ebx;

/* chiedo il nome utente */ printf("Inserisci il nome utente: "); gets(username);

/* calcolo il seriale */ esi = strlen(username); for(edx = 0; edx<esi; edx++) { ebx = (unsigned int) username[edx]; ecx = ecx + ebx; eax = eax*ecx; ebx = eax; ebx = ebx >> (ecx & 0xFF); ecx = ebx; }

/* stampo su schermo */ printf("Il seriale e' %u-%u\n", ecx, eax); return EXIT_SUCCESS; } Per semplicità, anche in C, ho chiamato le variabili con i nomi dei registri. Ho fatto una specie di copia/incolla dell’assembler anche in C… comunque la cosa è concettualmente diversa dal copia/incolla fatto direttamente in assembler: ad esempio a quel codice C potrei attuare delle ottimizzazioni (quanto meno per la lettura…) comunque non è questo quello che per ora importa. Compilate, eseguite, generate e…
Sshot-15 k.jpg
Facendo un altro po’ di attenzione al programma, scoprirete facilmente che c’è un altro vincolo da aggiungere: il nome utente deve essere almeno di 4 caratteri mentre il seriale (ma questo è automatico) deve essere almeno di 3 caratteri (immagino che chi ha programmato il crackme, ha pensato di verificare che il seriale fosse almeno “0-0”.. boh..).



Note Finali

Note finali.


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.