mankind's SOARP crackme | ||
Data |
written by -=Zero_G=- |
|
18/06/2004 |
Published by Quequero | |
|
Grazie mille zero, per la precisione e l'accuratezza del tutorial, continua cosi' mi raccomando. |
Hold Infinity in the palm of your hand and Eternity in an hour." - W. Blake - |
|
|
|
Difficoltà |
( )NewBies (X)Intermedio ( )Avanzato ( )Master |
Salve a tutti! Questo è un tutorial per capire come
funziona il SOARP crackme di mankind (acronimo di "Simulation of A
Real Protection" (!)); lo scopo è intercettare la
lettura del seriale un carattere alla volta, reversare l'algoritmo che lo
convalida in base al nome inserito e, per i più volenterosi, creare anche un
keygen adatto allo scopo. :-)
Alla fine vi propongo anche una soluzione
alternativa che può sempre far comodo in queste situazioni...
Introduzione |
Tools usati |
URL o FTP del programma |
Notizie sul programma |
Essay |
OK, fuori non fa (ancora) troppo caldo, ormai fino a domani non ho più voglia di studiare, la musica c'è... siamo a posto, possiamo partire! :-)
Il file eseguibile che si trova nell'archivio che avete scaricato si chiama soarp1.exe, lanciamo OllyDbg e carichiamolo al suo interno; beh, in genere prima si usa un analizzatore tipo PEiD per capire se è stato inserito qualche tipo di protezione/crypter, ma stavolta vi risparmio la fatica e vi dico che questo è codice liscio liscio scritto in assembler (ah, finalmente un programmino veloce e piccolo!)
Per avere un'idea di come funziona, si può
direttamente premere F9 per farlo partire e vedere che fa: la finestra è
composta da due campi di testo ed un Label con scritto "NOT REGISTERED";
se proviamo a scrivere il nostro nome e qualche codice a caso, la situazione
della Label (ovviamente) non cambia.
Bene, da dove cominciare
allora?
Anche se non vi è un controllo esplicito, possiamo facilmente
supporre che questo venga fatto ogni volta che noi inseriamo un carattere, e,
probabilmente, appena il seriale sarà validato, la scritta della Label cambierà;
in cosa cambierà? Boh, chi lo sa... magari ci potrà aiutare una ricerca di
stringhe, proviamo...
Premendo il tasto destro del mouse nella finestra CPU
di Olly scegliamo "Search for --> All referenced text strings" e diamo
un'occhiata alla lista che viene fuori: c'è la caption della finestra, un paio
di stringhe ASCII piene di 0 ed un nome ("HieCracker")... mah! :-/
Va bene,
la tentazione è forte: riavviamo il prog con CTRL+F2 e scriviamo "HieCracker" e
qualche numero a caso... vabbè, non poteva essere così semplice, no!?
;-)
Passiamo a controllare quali funzioni di sistema usa quest'aggeggio, così
magari si vede viene letto il seriale man mano che lo scriviamo: sempre dopo
aver premuto il tasto destro, scegliamo "Search for --> Name (label) in
current module": in questa lista ci sono le "import" del programma, e lì vediamo
varie funzioni per la manipolazione della stringhe (strcmp, strlen, ecc...), ma
anche una luce nel buio: USER32.GetDlgItemTextA. I veterani del
debugging magari ci avrebbero pensato subito a metterci un breakpoint sopra
(oltre a GetWindowTextA), ma almeno così siamo sicuri che è quella giusta e
sicuramente verrà utilizzata dal programma. Se volete vedere in quali punti
viene richiamata premete il destro (tasto molto utile in Olly!) e
scegliete "View call tree" (2 chiamate in totale), oppure direttamente "set
breakpoint on every reference" per avere un breakpoint settato su ogni call di
questa funzione.
Riavviamo il tutto (CTRL+F2 e poi F9) e
cominciamo a scrivere il nome... per ora, calma piatta, quindi almeno il nome
non viene letto mentre lo scriviamo; premiamo TAB o clicchiamo di sotto e
scriviamo un seriale a caso: ZACK, stavolta l'abbiamo preso! :-) subito dopo il
primo carattere ci ritroviamo in mezzo al disassemblato di Olly, più
precisamente all'indirizzo 004010C3, sulla prima chiamata a
GetDlgItemTextA.
NB: >>> [a destra sono evidenziate le API call ed i loro
parametri così come le individua Olly] <<<
Il parametro Buffer di GetDlgItemTextA ci
dice in che area di memoria verrà copiato l'elemento letto: premiamo il destro
alla riga 004010B6 e
scegliamo "Follow in Dump --> Immediate
Constant", premiamo F8 ed in basso a sinistra vedremo il comparire il nome nel
memory dump (quando ho fatto il debug io ce l'avevo nell'indirizzo 00403054, ma questo
può cambiare di volta in volta a causa dell'allocazione dinamica della
memoria).
Perfetto, quindi questo GetDlgItemTextA serviva a leggere il
nome; notiamo anche che il PUSH a 004010C8 lo passa come argomento alla
funzione a 004010FC.
Premiamo prima F8 e poi F7 per entrarci dentro e ci ritroviamo appunto a 004010FC dove troviamo una piccola routine che in sostanza contiene una strlen ed un JNZ:
Cosa succede qui dentro? lo strlen restituisce la
lunghezza di una stringa (e questo il caro mankind se lo poteva anche
risparmiare, dato che già il GetDlgItemTextA lascia in EAX la
lunghezza della stringa letta, ma forse è solo per confonderci un po'...) e se
non è uguale a 0 (JNZ) salta a 00401111; se la stringa è vuota, cioè
non abbiamo scritto nulla, si prosegue fino al RETN che riporta il controllo a
004010D2, dove dopo un JMP verso
004010F3 si riparte daccapo attendendo il nuovo
carattere. Spostando l'attenzione sull'indirizzo puntato dal JNZ cioè 00401111, ci
accorgiamo che c'è l'altra chiamata a GetDlgItemTextA che, senza tanta
fantasia, stavolta si occuperà di leggere il seriale; in ogni caso, è bene
controllare, quindi proseguiamo con F8 fino a 00401123, subito dopo la call:
controllando ancora una volta l'indirizzo Buffer controlliamo nel dump
della memoria dove è finito il nostro seriale (o meglio dove finirà, dato
che per ora ci trovate solo il primo carattere).
[CONSIGLIO: controllate
spesso lo stato delle stringhe in memoria, perché a volte vengono trasformate
direttamente "sul posto", e questo spesso aiuta a capire gli algoritmi di
generazione dei seriali]
Ok, se tutto è andato a buon fine, dovreste
trovarvi all'indirizzo 00401128, dove c'è un LEA: se gli operandi
di un'istruzione fanno riferimento ad indirizzi degni di nota come le stringhe,
queste vengono riportate nel riquadro delle informazioni di Olly in
basso. In questo caso, vediamo che in EAX ci viene caricato l'indirizzo
(00403036) di quella strana stringa che avevamo visto all'inizio ("HieCracker"),
dopodiché, dall'istruzione 0040112E fino alla 00401151, viene
caricata una serie di numeri sopra questa stringa in memoria; infatti, se
controlliamo passo passo quello che succede nell'area di memoria intorno a
00403036, vediamo che "HieCracker" viene sostituito con un'altra stringa
("CrackThis1") un carattere ASCII alla volta: alla fine di questa sostituzione,
EAX, che prima puntava ad "HieCracker", adesso riporta "CrackThis1". Si tratta
di un metodo abbastanza ingegnoso per mascherare delle stringhe che altrimenti
verrebbero individuato subito con una ricerca appropriata.
All'istruzione
00401155, c'è un altro LEA che stavolta (sempre tenendo d'occhio il riquadro in basso
di Olly) carica quella serie di 0 in EAX e poi, dall'istruzione
0040115B
fino alla 00401172, copia altri caratteri ASCII nella zona di memoria dove erano
contenuti prima, con lo stesso effetto di mascherare un'altra stringa
("Kanal23"); dato che questi indirizzi sono delle costanti nel programma, il
tutto lo potete comodamente osservare sempre dall'indirizzo 00403036 in poi, nel
memory dump. Arrivati a 0040117E, c'è una chiamata a strcpy, che
ha come argomenti l'indirizzo dove è contenuto il nostro nome ed un'altra area
di memoria, dove questo verrà prontamente ricopiato (verificate con "Follow in
Dump").
A questo punto, per il programma tutto è pronto per
validare o meno il codice (che per adesso è composto da un solo carattere),
perché il campo del nome non è vuoto ed è stato inserito almeno un carattere del
seriale; adesso capiamo facilmente che è questo il controllo che viene
effettuato ogni volta che ne aggiungiamo uno, e non appena il codice sarà
giusto, qualcosa cambierà sicuramente all'interno della finestra del
programma.
Inizializzazioni escluse, la procedura di controllo va da
004011BB
a 00401266, compreso lo strcmp finale; vediamo cosa fa questa routine dopo lo strcpy di
0040117E...
I primi quattro XOR azzerano i registri ESI, EDI, ECX e
EBP che di solito vengono usati come contatori o indici, quindi conviene
sfruttare la possibilità di commentare le righe in Olly (doppio click
nella parte destra della riga) e rinominarli con delle lettere, così dopo
capiamo meglio quale stiamo usando, come si vede qui sopra.
Come al solito,
lo strlen restituisce la lunghezza della stringa pushata e questo valore (6 nel
mio caso) viene subito dopo copiato da EAX nell'area di memoria 0040311E, e lo stesso
avviene con la stringa che abbiamo scoperto prima ("CrackThis1"), che però
finisce in 0040318B; entrambe le troviamo nei rispettivi indirizzi nel memory
dump, separate da circa 80 parole di memoria.
>>> NB: Da ora in
avanti userò direttamente le lettere i, j, k per gli indici e name, serial, CrackThis1 e
Kanal23
per le stringhe; HieCracker e gli zeri non esistono più, perché servivano solo
per mascherare le altre due. Infine, stringa[i] indica l'i-esimo carattere
della stringa.
Qui di seguito è riportato il primo loop che viene
eseguito subito dopo la copia delle stringhe; a destra ho aggiunto i commenti
così si capisce meglio che fa questa procedura (ricordatevi che AL corrisponde
alle prime due cifre da destra di EAX, e lo stesso vale per CL con ECX e DL con
EDX).
Ho convertito i valori esadecimali in decimali perché erano
praticamente tutti indici, quindi credo che così si capisca meglio; i valori tra
parentesi graffe sono sempre in decimale.
Avete capito cosa succede nel loop? La stringa
CrackThis1 viene sovrascritta un carattere alla volta con un altro carattere che
risulta dalla somma del suo valore ASCII con quello del nome inserito e della
stringa Kanal23, ed il valore finale viene decrementato di 2 e xorato con 9;
questo avviene per tutta la lunghezza del nome che abbiamo immesso nel
programma.
Arrivati all'indirizzo 00401204, in EAX troverete l'area di
memoria (0040318B) dove è contenuto il risultato di queste operazioni sulla
stringa CrackThis1 (nel mio caso con "Zero_G" è venuto EF 3F 36 38 3D C2 68 69
73 31, che in ASCII corrisponde a "'?68=†his1"); qui si capisce che il
seriale sarà sempre lungo 10 caratteri, dato che nasce da una trasformazione di
CrackThis1 che è lungo 10.
Alla termine della procedura precedente, troviamo
un MOV
che scrive 0A {10} in un indirizzo di memoria e poi in ECX, che viene usato come
indice per scorrere la stringa precedentemente calcolata (quella in 0040318B) che
indicherò con new_string; i numeri con cui vengono confrontati i caratteri li ho
lasciati in esadecimale, tanto alla fine il tutto viene riportato in ASCII,
quindi è inutile consocere il valore intermedio.
>>> NB: apprezzate
il mio sforzo di ASCII-art per evidenziare i jumps! ;-)
Così a prima vista può sembrare complicato, ma
basta un'occhiata in più per capire che ogni elemento di new_string (la
stringa di 10 'caratteri' ottenuta tramite somme tra il nome inserito, Kanal23 e
CrackThis1) viene incrementato di 13 {19} [attenzione che le somme sono
modulo FF+1 {256}, perché CL è 1 byte! FF + 1 fa 00!], fino a che non ricade
all'interno del range ASCII delle lettere maiuscole e dei numeri, cioè compreso
tra 30 e 5B, esclusi i caratteri speciali da 3A a 40; i vari controlli per fasce
(da 30 a 3A [numeri], da 3A a 40 [caratteri speciali], da 40 a 5B
[lettere maiuscole]) servono proprio a saltare il secondo intervallo e
ricadere su uno degli altri due.
Il CMP alla fine del loop serve per
controllare quando è stata raggiunta la fine della stringa.
Bene, ora che abbiamo capito come è generato il seriale valido per il nostro nome, guardiamo cosa c'è subito dopo: un bel strcmp seguito da un JNZ!: eheh, il classico good/bay boy jump, che però non avremmo individuato con una semplice ricerca per stringhe tipo "REGISTERED" o "NOT REGISTERED", perché questo non sono dati statici ma vengono scritte un carattere alla volta tramite MOV in aree di memoria adiacenti; lo stesso è successo anche con le due stringhe (CrackThis1 e Kanal23) necessarie per calcolare il seriale (HieCracker era lì solo per confonderci...).
I due parametri di strcmp sono le stringhe da confrontare: una è il seriale corretto, l'altra è sono i caratteri che vengono letti dal TextBox ogni volta che ne aggiungiamo uno; infatti io avevo premuto "1" e per ora c'è soltanto quello.
Se il seriale corrisponde, si prosegue subito sotto
il JNZ,
altrimenti si va a 004012A9, ma in entrambi i casi notiamo che ci sono una lunga serie
di MOV e
poi un SetDlgItemTextA per la finestra del programma; dato che ormai il codice è
sbagliato, usiamo intanto F8 per seguire che fa almeno in questo caso.
Le
parentesi quadre indicano riferimenti alla memoria, in questo caso puntatori a
stringhe.
Beh, alla luce di quello che abbiamo visto per il
seriale, direi che qui si capisce abbastanza chiaramente cosa succede
(oltretutto, mentre fate gli step, vedrete che le stringhe cambieranno in tempo
reale direttamente nella decodifica di Olly): le sfilze di zeri vengono
sovrascritte un carattere alla volta con la scritta "NOT REGISTERED" (quella che
non riuscivamo a trovare con "Search for reference strings"), che viene poi
passata al SetDlgItemTextA per
scriverla nella Label (quella sopra la TextBox del nome) ed allo
strcpy per sovrascriverla al seriale in memoria (controllate l'indirizzo
0040318B con "Follow in Dump" quando siete a 00401302); poi, da 00401307 a
00401334
ci sono i soliti MOV che sovrascrivono i nostri due amici CrackThis1 e Kanal23,
in modo da non lasciare traccia nè del seriale giusto, nè del meccanismo con cui
è stato generato.
Per completezza, potete anche guardare cosa succede nel
caso in cui il seriale è giusto: l'unica cosa che cambia è la stringa che
stavolta è "REGISTERED", perché comunque poi si jumpa a 004012F8 per lo
strcpy e
l'azzeramento delle altre stringhe.
Ottimo! Direi che abbiamo reversato l'intero procedimento! che volete di più? :-)
Adesso a voi la scelta (in ordine di qualità
decrescente):
1) un elegante keygen che genera il seriale
giusto a partire da un nome;
[data la natura
delle operazioni che vengono eseguite sulle stringhe ed i vari XOR vi consiglio di
farlo in C, se potete]
2) un tranquillo serial-fishing per il
vostro nome;
[unico problema: per ogni nome
che inserite dovete rifare il debug per catturare il seriale]
3) un
violento patching per far accettare qualsiasi seriale come
valido.
[no comment]
eheheh, ma forse i più attenti avranno anche intravisto un'altra possibilità di intervento a metà strada tra la 2) e la 3)... ci avete pensato?
Avete mai sentito parlare di Keygen
Injection?
E' una tecnica che permette di sostituire la stampa di
messaggi con quella del seriale giusto, dirottando i PUSH delle funzioni
verso l'indirizzo del seriale, se è sempre presente in memoria.
Riuscite a
vedere se è possibile fare una cosa del genere per questo crackme?
Guardate bene il PUSH a 004012E6: serve per passare a
SetDlgItemTextA il "NOT REGISTERED" ottenuto cambiando gli zeri; ma abbiamo ancora quello
giusto a disposizione? SI'! è sempre in 0040318B, come ci ricordano i parametri
passati a strcmp in 0040125F. Ok, quindi per stamparlo basta pushare al SetDlgItemTextA
quell'indirizzo al posto del puntatore a "NOT REGISTERED", vi torna?
Qui
sotto riporto il PUSH modificato (si fa doppio click nella parte sinistra della
riga e poi "Assemble", dove potete usare direttamente gli mnemonici degli
opcode); premendo il destro e scegliendo "Copy to Executable --> All
modifications --> Copy all", poi destro ancora e "Save File" avrete un nuovo
eseguibile in cui, dopo aver scritto il nome, non appena inserite un carattere
del seriale farlocco, vi viene mostrato quello giusto in alto; ricopiatelo di
sotto e a quel punto comparirà la scritta "REGISTERED". Vittoria! ;-)
Ovviamente il keygen è sempre la scelta migliore da un punto di vista tecnico, in quanto si lascia intatto il programma originale, ma comunque è bene conoscere anche altre strade per raggiungere il nostro scopo.
qualsiasi sia la protezione che vogliamo affrontare, proviamo varie alternative ed usiamo tutte le risorse a disposizione, perché spesso quello che separa l'arrendersi dalla vera soluzione è solo un po' di intuito e pazienza in più.
E' da notare che in fondo questo semplice crackme
si è rivelato molto istruttivo: immagino che spesso avrete visto programmi che
controllano i seriali man mano che vengono scritti e di cui non riuscivate a
trovare stringhe significative, proprio perché mascherate e/o codificate in
run-time.
L'algoritmo di generazione del seriale (quello che dovreste
riscrivere per il keygen) è un po' strano per via del fatto che si basa su
quelle due stringhe fisse, ma il procedimento dello XOR e delle somme di
caratteri per generare un'altra stringa valida in ASCII è abbastanza comune,
quindi fatene tesoro.
E poi, quando se ne presenta l'occasione, il Keygen
Injection, sebbene richieda ugualmente una patch, dà sempre una discreta
soddisfazione! :-)
-=Zero_G=-
Note finali |
Grazie a tutta la UIC e a Quequero
per aver creato quello che oggi è il posto più bello dove sentirsi un reverser
italiano. :-)
Grazie ai Dream Theater, agli Ayreon, agli
Evergrey, ai Rhapsody ed agli Story of the Year per avermi
tenuto compagnia mentre scrivevo questo tutorial, e spero lo continuino a fare
anche in tanti altri momenti.
UN NON GRAZIE all'Università dove
studio, che ultimamente mi sta spillando il sangue dalle vene e non mi lascia
mai tempo per rilassarmi un minuto.
E per finire, il solito elogio ad Escher
e Tolkien che non fa mai male...
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 immane che ogni singolo programmatore ha dovuto portare avanti per fornire ai rispettivi consumatori i migliori prodotti possibili.
Noi reversiamo al solo scopo informativo e di miglioramento del linguaggio Assembly.