Zoom Icon

NewBies Simple KeyGenMe ita

From UIC

Simple KeygenMe

Contents


NewBies Simple KeyGenMe ita
Author: Zyrel
Email: marcolagalla@yahoo.it
Website: Unfair-Gamers
Date: 10/01/2011 (dd/mm/yyyy)
Level: Half brain is enough
Language: Italian Flag Italian.gif
Comments: Veramente semplice, adatto ai newbies.



Introduzione

Ciao a tutti, questo è il primo tutorial che pubblico su quequero, ed è rivolto (come avrete capito dal titolo) agli utenti che hanno appena deciso di intrapprendere questa strada. Non mi dilungo ulterioriormente, limitandomi a citare direttamente dal glossario della UIC la definizione di cosa è un KeygenMe:

« Programma scritto appositamente per essere crackato/reversato. Tipicamente l'unica funzionalità del crackme è proprio la sua protezione, che può prendere a modello protezioni esistenti (come quelle dei programmi commerciali) o più frequentemente proporre qualcosa di nuovo ed originale. I crackme sono scritti da reverser e si indirizzano ad altri reverser, ed hanno scopi puramente didattici ».

In questo specifico caso noi non andremo a crackare il programma (o meglio lo faremo giusto per completezza, ma non è quello il nostro obiettivo) facendo sì che ritenga ogni seriale corretto, ma andremo invece a "sniffare" il codice per poter dedurre l' algoritmo che genera il seriale corretto.



Tools & Files


URL o FTP del programma

Per l' occasione andremo ad usare un piccolo programma creato da me: KeygenMe#1


Essay

Bene, ora io darei per scontato che voi conosciate almeno le basi dell' assembly e almeno un linguaggio di programmazione (per creare il keygen).

Ok, inizia il tutorial vero e proprio, avviamo il programma tanto per farci un idea di come funziona:

KeyGenMe-1.png

Bene cosa notiamo? Dunque:

 1^ Textbox: non editabile che ci mostra un numero (intero).
 • 2^ Textbox: in cui ci viene chiesto di inserire il nostro seriale.
 • Bottone: serve per il check.

Non c'è nessuna richiesta di un nick o altro, quindi mi pare logico pensare che l' algoritmo che genera il seriale si basi proprio su quel numero; bene ora ci chiediamo: perchè proprio quel numero? (67 nello screen), ecco se provate a riaprire il programma vi troverete un altro numero, sempre intero, ma ogni volta diverso.

Perfetto carichiamo il target (chiamerò spesso così per il resto del tutorial il KeygenMe) in Olly e fermiamoci a dare un occhiata al listato:

00401240 K> $  55             PUSH EBP
00401241    .  89E5           MOV EBP,ESP
00401243    .  83EC 08        SUB ESP,8
00401246    .  C70424 0200000>MOV DWORD PTR SS:[ESP],2
0040124D    .  FF15 3C614000  CALL DWORD PTR DS:[<&msvcrt.__set_app_type>]           ;  msvcrt.__set_app_type
00401253    .  E8 A8FEFFFF    CALL Keygen.00401100
00401258    .  90             NOP
00401259    .  8DB426 0000000>LEA ESI,DWORD PTR DS:[ESI]
00401260    $  55             PUSH EBP
00401261    .  8B0D 54614000  MOV ECX,DWORD PTR DS:[<&msvcrt.atexit>]                ;  msvcrt.atexit
00401267    .  89E5           MOV EBP,ESP
00401269    .  5D             POP EBP                                                ;  kernel32.7671D0E9
0040126A    .  FFE1           JMP ECX
0040126C       8D7426 00      LEA ESI,DWORD PTR DS:[ESI]
00401270    .  55             PUSH EBP
00401271    .  8B0D 48614000  MOV ECX,DWORD PTR DS:[<&msvcrt._onexit>]               ;  msvcrt._onexit
00401277    .  89E5           MOV EBP,ESP
00401279    .  5D             POP EBP                                                ;  kernel32.7671D0E9
0040127A    .  FFE1           JMP ECX

Questo è l' entrypoint del programma, ma a noi questa parte non interessa, piuttosto scrollate verso il basso:

 • Qui vediamo una chiamata alla CreateWindowExA:
00401344   |> \C74424 2C 0000>MOV DWORD PTR SS:[ESP+2C],0                            ; |
0040134C   |.  8B45 08        MOV EAX,[ARG.1]                                        ; |
0040134F   |.  894424 28      MOV DWORD PTR SS:[ESP+28],EAX                          ; |kernel32.BaseThreadInitThunk
00401353   |.  C74424 24 0000>MOV DWORD PTR SS:[ESP+24],0                            ; |
0040135B   |.  C74424 20 0000>MOV DWORD PTR SS:[ESP+20],0                            ; |
00401363   |.  C74424 1C 9600>MOV DWORD PTR SS:[ESP+1C],96                           ; |
0040136B   |.  C74424 18 9001>MOV DWORD PTR SS:[ESP+18],190                          ; |
00401373   |.  B8 00000080    MOV EAX,80000000                                       ; |
00401378   |.  894424 14      MOV DWORD PTR SS:[ESP+14],EAX                          ; |kernel32.BaseThreadInitThunk
0040137C   |.  B8 00000080    MOV EAX,80000000                                       ; |
00401381   |.  894424 10      MOV DWORD PTR SS:[ESP+10],EAX                          ; |kernel32.BaseThreadInitThunk
00401385   |.  C74424 0C 0000>MOV DWORD PTR SS:[ESP+C],0CF0000                       ; |
0040138D   |.  C74424 08 0040>MOV DWORD PTR SS:[ESP+8],Keygen.00404000               ; |ASCII "Keygen Me  #1"
00401395   |.  C74424 04 0030>MOV DWORD PTR SS:[ESP+4],Keygen.00403000               ; |ASCII "WindowsApp"
0040139D   |.  C70424 8801000>MOV DWORD PTR SS:[ESP],188                             ; |
004013A4   |.  E8 A70B0000    CALL <JMP.&USER32.CreateWindowExA>                     ; \CreateWindowExA
 • Qui troviamo una parte interessante, con due chiamate, ad rand() e srand()
004013AC   |.  8945 F4        MOV [LOCAL.3],EAX                                      ; |||kernel32.BaseThreadInitThunk
004013AF   |.  C70424 0000000>MOV DWORD PTR SS:[ESP],0                               ; |||
004013B6   |.  E8 F50A0000    CALL <JMP.&msvcrt.time>                                ; ||\time
004013BB   |.  890424         MOV DWORD PTR SS:[ESP],EAX                             ; ||kernel32.BaseThreadInitThunk
004013BE   |.  E8 FD0A0000    CALL <JMP.&msvcrt.srand>                               ; |\srand
004013C3   |.  E8 D80A0000    CALL <JMP.&msvcrt.rand>                                ; |[rand

Bene da qui capiamo come viene generato il numero su cui (come vi ho anticipato) si basa l' algoritmo. Deduciamo quindi che utilizza le funzioni srand() e rand() utilizzando come seme delle prima la funzione time(), troviamo una chiamata alla rand() nuovamente più sotto ma non ci interessa.

Benissimo runniamo con F9 e proviamo ad inserire un seriale a casaccio, io userò il buon vecchio 1234567890 ;) Ci compare una MessageBox che ci avverte che il seriale non è corretto, bhè io penso che il programma possa essere fatto più o meno così:

// vediamo solo la parte che ci interessa
unsigned long s_fake; // nostro seriale che inseriamo (sbagliato)
if (s_fake == generate()) {
MessageBox (NULL, "Il seriale è corretto","Ottimo lavoro", MB_OK);
}
else {
MessageBox (NULL, "Il seriale è sbagliato","Tenta ancora", MB_OK);
}

Bene quindi non perdiamo tempo ;) Ricarichiamo il target su Olly premendo CTRL+F2 e nella Commandline digitiamo:

bpx MessageBoxA
 N.B. chi non avesse il plugin della CmdLine può trovarlo Qui.

In questo modo abbiamo piazzato un bp (BreakPoint) su tutte le chiamate alla suddetta API (attenzione alle maiuscole/minuscole, la cmdline è case-sensitive), ora runniamo nuovamente premendo F9 ed inseriamo il nostro seriale fittizio (per me 1234567890), appena prima che la MsgBox compaia per dirci che il nostro seriale è sbagliato il debugger ci brekka qui:

004017D7   |.  E8 E4070000    CALL <JMP.&USER32.MessageBoxA>                         ; \MessageBoxA

Se guardate appena più sopra vedrete:

0040178A       3B45 F8        CMP EAX,DWORD PTR SS:[EBP-8]                           ;  EAX contiene il nostro seriale mentre EBP punta al seriale corretto
0040178D   |.  75 29          JNZ SHORT Keygen.004017B8                              ; |salto condizionato, se la Z flag è 0 salta a 004017B8
0040178F   |.  C74424 0C 0000>MOV DWORD PTR SS:[ESP+C],0                             ; |
00401797   |.  C74424 08 E140>MOV DWORD PTR SS:[ESP+8],Keygen.004040E1               ; |ASCII "Ottimo lavoro"
0040179F   |.  C74424 04 EF40>MOV DWORD PTR SS:[ESP+4],Keygen.004040EF               ; |ASCII "Seriale Corretto"
004017A7   |.  C70424 0000000>MOV DWORD PTR SS:[ESP],0                               ; |
004017AE   |.  E8 0D080000    CALL <JMP.&USER32.MessageBoxA>                         ; \MessageBoxA
004017B3   |.  83EC 10        SUB ESP,10
004017B6   |.  EB 27          JMP SHORT Keygen.004017DF
004017B8   |>  C74424 0C 0000>MOV DWORD PTR SS:[ESP+C],0                             ; |
004017C0   |.  C74424 08 0041>MOV DWORD PTR SS:[ESP+8],Keygen.00404100               ; |ASCII "Tenta ancora ;)"
004017C8   |.  C74424 04 1041>MOV DWORD PTR SS:[ESP+4],Keygen.00404110               ; |ASCII "Seriale Sbagliato"
004017D0   |.  C70424 0000000>MOV DWORD PTR SS:[ESP],0                               ; |
004017D7   |.  E8 E4070000    CALL <JMP.&USER32.MessageBoxA>                         ; \MessageBoxA

Ho commentato il listato nei punti importanti, ma la cosa che ci interessa di più al momento è quel:

0040178A       3B45 F8        CMP EAX,DWORD PTR SS:[EBP-8]                           ;  EAX contiene il nostro seriale mentre EBP punta al seriale corretto
0040178D   |.  75 29          JNZ SHORT Keygen.004017B8                              ; |salto condizionato, se la Z flag è 0 salta a 004017B8

Ora apriamo una piccola parentesi:

Il Cracking

Credo che lo abbiate già intuito, ma se noi invertissimo quel salto potremmo far si che se anche un seriale fosse sbagliato il target farebbe apparire la MsgBox di conferma (volete provare? provate a settare la Zero flag su 1, vedrete che non effettuerà il salto e apparirà la giusta MsgBox) Non mi soffermo molto perchè non è il nostro scopo e passo direttamente alla soluzione:

Primo metodo: sostituire il JNZ (Jump if Not Zero) con un JE (Jump if Equal)
  • Secondo metodo: sostituire il JNZ con un JMP ovvero un salto incodizionato ad 0040178F, MsgBox di conferma
  • Terzo metodo: noppare il tutto facendo così apparire la MsgBox

Sinceramente preferisco il primo metodo, quindi andiamo a fare un piccola patch di appena un byte avendo così:

0040178D      /74 29          JE SHORT Keygen.004017B8                               ;  salto condizionato, se la Z flag è 0 salta a 004017B8

E infatti nel box sottostante:

Jump is NOT taken

Ora per rendere le modifiche permanenti andate a leggervi il tutorial su come effettuare il manual patching oppure per fare prima (se già sapete come si fà manualmente) Click destro->Copy to executables->All modifications->Save.


Individuazione e studio dell' algoritmo:

Ora entriamo nella parte vera e propria del nostro tutorial: l' analisi dell algoritmo. Ci eravamo lasciati qui:

0040178A       3B45 F8        CMP EAX,DWORD PTR SS:[EBP-8]                           ;  EAX contiene il nostro seriale mentre EBP punta al seriale corretto

Se scrollate ancora più in su, subito sotto la GetDlgItemInt trovate questa CALL:

00401757   |.  E8 BA000000    CALL Keygen.00401816

Uhmm...una call...sotto la funzione che legge il valore (numerico) della textbox....vicina al salto condizionato... Bhè direi che può essere interessante, quindi piazziamo un bp con F2 sulla CALL e runniamo inserendo il solito seriale, ci brekka sulla CALL, premendo F7 steppiamo dentro alla CALL e veniamo portati su una routine che, mi ci gioco le babbucce è quella della generazione del seriale:

00401816   /$  55             PUSH EBP
00401817   |.  89E5           MOV EBP,ESP
00401819   |.  83EC 0C        SUB ESP,0C
0040181C   |.  8B45 08        MOV EAX,DWORD PTR SS:[EBP+8]
0040181F   |.  0FAF45 08      IMUL EAX,DWORD PTR SS:[EBP+8]
00401823   |.  8945 F8        MOV DWORD PTR SS:[EBP-8],EAX
00401826   |.  8B45 08        MOV EAX,DWORD PTR SS:[EBP+8]
00401829   |.  0FAF45 F8      IMUL EAX,DWORD PTR SS:[EBP-8]
0040182D   |.  8945 F4        MOV DWORD PTR SS:[EBP-C],EAX
00401830   |.  8B45 F8        MOV EAX,DWORD PTR SS:[EBP-8]
00401833   |.  0FAF45 F4      IMUL EAX,DWORD PTR SS:[EBP-C]
00401837   |.  0345 08        ADD EAX,DWORD PTR SS:[EBP+8]
0040183A   |.  8945 FC        MOV DWORD PTR SS:[EBP-4],EAX
0040183D   |.  8B45 FC        MOV EAX,DWORD PTR SS:[EBP-4]
00401840   |.  C9             LEAVE
00401841   \.  C3             RETN

Come vedete non fà altro che fare qualche semplice calcolo con il valore contenuto in EAX, che è proprio il numerino randomico mostrato dal programma nella textbox disabilitata. Infatti il valore restituito da un API è sempre contenuto in EAX (dalla GetDlgItemInt). Per un ulteriore riprova digitate d EAX nella CmdLine e vedrete che contiene proprio quel numero.

Direi che il codice è chiarissimo, però vediamolo nei tratti fondamentali:

0040181F   |.  0FAF45 08      IMUL EAX,DWORD PTR SS:[EBP+8]
00401829   |.  0FAF45 F8      IMUL EAX,DWORD PTR SS:[EBP-8]
00401833   |.  0FAF45 F4      IMUL EAX,DWORD PTR SS:[EBP-C]
00401837   |.  0345 08        ADD EAX,DWORD PTR SS:[EBP+8]

Che tradotto in pseudo-codice sarebbe:

0040181F moltiplica eax per sè stesso, quindi eax^2
00401829 moltiplica eax *(eax ^2) quindi eax^3
00401833 moltiplica (eax^2) * (eax^3) quindi eax^5
00401837 somma (eax^5) + eax

Quindi la formula che genera il seriale è: (Eax^5) + Eax dove Eax è il numero randomico. Però qui sorge l' unica pseudo difficoltà di questo CrackMe, il risultato del calcolo viene gestito come dword e quando Eax è maggiore di 84 (il che capita spesso) allora Eax^5 supera 0xFFFFFFFF, il che genera errori di overflow che rendono invalido agli occhi del programma il seriale anche se potenzialmente corretto.

Un keygen deve seguire precisamente i calcoli effettuati dal programma, quindi: Eax * Eax * Eax * (Eax^2) + Eax evitando così un overflow, anzi più correttamente la soluzione più adeguata sarebbe: seriale = ((random^5) and &HFFFFFFFF + random) and &HFFFFFFFF con due And logici.

Il KeyGen

Infine il nostro beneamato keygen ;) Io ve lo presento in due linguaggi: C++ e ASM.

#include <iostream>
using std::cout;
using std::cin;

int main() {
    int eax;
    unsigned long serial;
    cout << "Inserire il numero restituito dal programma: ";
    cin >> eax;
    serial = eax * eax;
    serial = serial * eax;
    serial = serial * (eax*eax);
    serial = serial + eax;
    cout << "\nIl seriale corretto: " << serial << "\n";
    system("pause");
    return 0;
}


.486
.model  flat, stdcall
option  casemap :none   ; case sensitive


include windows.inc

uselib  MACRO   libname
        include         libname.inc
        includelib      libname.lib
ENDM

uselib  user32
uselib  kernel32

;dichiarazione prototipi delle funzioni del programma
DlgProc         PROTO :DWORD,:DWORD,:DWORD,:DWORD       ;funzione finestra
MakeSerial      PROTO :DWORD                                                    ;funzione genera-seriali
ErrorProc               PROTO                                                                   ;funzione di errore dell'immissione del code

;impostazione nomi per gli oggetti della finestra
IDC_TXTCODE     equ     1001
IDC_TXTSERIAL   equ     1002
IDC_CMDGEN              equ     1003
IDC_CMDEXIT             equ     1004

;dati non inizializzati
.data?
hInstance               DWORD ? ;dd can be written as dword
lptranslated    DWORD ? ;variabile per capire se il codice inserito e' valido
hWindow         DWORD ? ;handle window replicato per la messagebox

;dati inizializzati
.data
szerrortext                     db "Valore del Code non corretto o assente!",0
szerrorcaption                  db "ERRORE",0

.code
ErrorProc proc
        ;cancella sia il code che il serial
        invoke SetDlgItemText,hWindow,IDC_TXTCODE,NULL
        invoke SetDlgItemText,hWindow,IDC_TXTSERIAL,NULL
        ;messaggio di errore
        invoke MessageBox,hWindow,ADDR szerrortext,ADDR szerrorcaption,MB_ICONERROR
        Ret
ErrorProc EndP

start:
        invoke  GetModuleHandle, NULL ;ritorna l'imagebase dell'exe (handle) da passare per creare la finestra
        mov     hInstance, eax
        invoke  DialogBoxParam, hInstance, 101, 0, ADDR DlgProc, 0 ;creazione finestra
        invoke  ExitProcess, eax ;esce dal processo
; -----------------------------------------------------------------------
DlgProc proc    hWin    :DWORD,
                uMsg    :DWORD,
                wParam  :DWORD,
                lParam  :DWORD

        .if     uMsg == WM_COMMAND ;se viene premuto un tasto...
                .if     wParam == IDC_CMDGEN ;se viene premuto il tasto genera...
                        mov eax, hWin
                        mov hWindow, EAX
                        invoke GetDlgItemInt,hWin,IDC_TXTCODE,ADDR lptranslated,FALSE ;se esiste, prende l'intero dalla casella di testo code
                        .if lptranslated == TRUE
                                invoke MakeSerial, EAX ;calcola il seriale
                                invoke SetDlgItemInt,hWin,IDC_TXTSERIAL,EAX,FALSE ;scrive il valore nella textbox
                        .else
                                invoke ErrorProc ;chiama il messaggio d'errore
                        .endif
        .elseif wParam == IDC_CMDEXIT ;se viene premuto il tasto exit...
                        invoke EndDialog,hWin,0
                .endif
        .elseif uMsg == WM_CLOSE ;se viene selezionata la X
                invoke  EndDialog,hWin,0
        .endif

        xor     eax,eax
        ret
DlgProc endp

MakeSerial Proc code:DWORD
        ;inizia a calcolare il seriale solo 2 moltiplicazioni ed un'addizione, non c'e' nulla da spiegare
        mov eax, code
        imul eax, eax
        mov edx, eax
        mov eax, code
        imul eax, edx
        mov ecx, eax
        mov eax, edx
        imul eax, ecx
        add eax, code
        Ret
MakeSerial EndP

end start


Note Finali

E cosi siamo giunti alla fine del mio primo tute su Quequero :) Bè spero di essere riuscito a scrivere il tutto in modo chiaro e non troppo palloso, per qualunque dubbio o chiarimento potete contattarmi via mail, via pm sul foro della UIC, su UG, dove preferite :)

Un ringraziamento particolare va ad EvOlUtIoN per il source in ASM del Keygen (che risolse in 40 secondi netti xd) e per alcune precisazioni.

Zyrel.


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.