ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÛ
² [x] RingZ3r0 Proudly Presents [x] ²
± ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ±
³ PC98 Trial CrackMe KeyFile Solution ³
Kill3xx
28 Dicembre ,1998
--==[ PREMESSA ]==------------------------------------------------------------------------
LE INFORMAZIONI CHE TROVATE ALL'INTERNO DI QUESTO FILE SONO PER PURO SCOPO DIDATTICO.
L'AUTORE NON INCORAGGIA CHI VOLESSE UTILIZZARLO PER SCOPI ILLEGALI.
--==[ DIFFICOLTA' ]==------------------------------------------------------------------------
scala : *=Novizio, **=Apprendista, ***=Esperto, ****=Guru
target: **1/2
--==[ TOOLS USATI ]==------------------------------------------------------------------------
* PROCDUMP 1.2 (Grom/Stone UCF)
* SoftIce 3.23 (Numega)
* IDA ver 3.80 (Ilflak / DataRescue)
* TASM 5.0 (Imprise)
* HIEW 6.0
--==[ INTRODUZIONE ]==------------------------------------------------------------------------
Ho deciso che avrei scritto un tutorial sul CrackMe realizzato da The+Q e Plushmm, nonappena
la Trail Session dei PC fosse finita ovviamente ;), perche' lo considero un ottimo esempio di
come si possa utilizzare IDA per realizzare in modo piu' semplice e veloce un keymaker.
Il Crackme si compone di 4 prove in ordine di difficolta':
1) trovare la classica combinazione user/code
2) eliminare il nagscreen iniziale
3) trovare il serial valido
4) costruire un keyfile valido che ci consenta di registrare ad ex. il nostro nome
Ma veniamo subito al dunque:
--==[LA PROVA DEL KEYFILE]==------------------------------------------------------------------
Quello che qui ci interessa e' l'ultima prova.. la piu' interessante a mio avviso (delle prime
tre ne parlero' velocemente alla fine).
Ora so che qualcuno di voi gia' scalpita :) e vorrebbe iniziare a martellare i tasti F8..F12,
calma calma.. una cosa che consiglio a tutti prima di iniziare una cracking session e' di
sedersi comodamente e fare qualche considerazione sul target che avete di fronte:
1) partiamo avvantaggiati dal fatto che sappiamo che si tratta di un keyfile quindi possiamo
evitare il solito workaround (FileMon/RegMon,ecc.)
2) e' un crackme presumibilmente scritto in asm.. quindi il codice e'quasi esclusivamente
dedicato alla protezione.. il che ci facilita molto nella fase di identificazione delle
routine ..eheh niente overbloated code come in delphi o vb :)
3) se e' scritto in asm come pensiamo e' lecito attenderci anche trucchetti sporchi ;)
ad ex. antisIce, codice automodificante,ecc..
4) analizzando la strutura de PE-Header con Procdump (o un qualsiasi PE Browser) ci accorgiamo
subito che l'exe e' crittato/packato.. e che il decrypter sta nell'ultima section Q_Plush.
Dopo questa breve ma importante preview del nostro target, possiamo anche passare la parola
al nostro fido sIce.
Dato che e' un keyfile i breakpoint da provare subito sono quelli sulle API winsuxx per
l'apertura/lettura di un file: quindi BPX su CreateFileA, ReadFile, GetFileAttributesA,
GetFileSize..
Con nostra somma gioia sIce non brekka.. ohoh cosa succede mai qui?.. nulla di particolare il
simpatico The+Q ha usato le obsolete _lopen,_lread,_lclose,ecc. per distrarci un po':)
togliamo tutti i vecchi breaks con "BC *" e mettiamo un "BPX _lopen" e "bpx _lread"..
//Nota per eventuali newbies//---------------------------------------------------------------
dato che volenti o nolenti passerete parecchio tempo con target windogz e' buona cosa che vi
procuriate l'help del SDK con tutti prototipi della API e vi studiate bene almeno le piu'
importanti.. e' fondamentale che sappiate cosa viene passato ad un'API quando viene invocata.
//------------------------------------------------------------------------------------------//
et voila' sIce brekka in mezzo alla routine di lettura del keyfile: vediamo che il prog
usa _lread per leggere 0xA4 bytes da un file di nome "crackme.key" e poi lo chiude..
se effettivamente non sono stati letti 164 bytes la funct segnala l'errore con eax=-1..
bene, allora noi mettiamo un BPX all'address in cui inizia la funct..togliamo BPX _lopen,ecc...
usciamo da sIce.. ci costruiamo un bel file crackme.key lungo 164 bytes pieno di 0xFF..
rientriamo in quella funct mettiamo un BPR Buffer_Address Buffer_Address+164 (o solo BPM se
usate sIceNT) .. P RET (F12) e diamo un'occhiata a cosa fa il prog:
umm.. legge l'ultima dword.. la manipola (ROL 6).. chiama una funct che costruisce una tabella
di 8 elementi (byte) e poi entra in un loop con ecx=14h che chiama quella che ad occhio e' la
nostra funct di decrypt..
aiaiii.. anche ad un'occhiata veloce questa funct ci appare subito lunghetta e piuttosto
articolata.. vabbeh, anche se a malincuore e' il momento di passare da un approccio live del
sIce ad un piu' meditativo :) deadlisting con IDA.
Ora pero' si pone un problema: come possiamo dissassemblare l'exe se e' crittato?
niente paura.. visto che si decritta in mem quando viene eseguito e che , fortunatamente,
la import table non e' alterata, possiamo farne il dump (usando Icedump/The_Owl,Softdump/Qaine,
ADump/UCF per citarne alcuni).. salvare le sections decrittate e sovrascriverle nell'exe
originale (ricordate che e' solo un crypter e quindi le sect hanno le stesse dimensioni anche
dopo il mapping in mem), quindi rimuovere la section Q-Plush e riaggiustare il pe_header
(l'entry point,section table,imagesize,ecc..).. ok ok sembra difficile, ma in fondo e' solo
questione di fare un po' di Cut&Paste :)
vabbeh, siccome noi siamo assolutamente pigri usiamo ProcDump ;) :
nelle option e' meglio che utilizziate:
"recompute obj size" = OFF
"use actual import infos" = ON
selezionate il metodo Standard.. unpack .. premete ok .. salvate l'exe decrittato..
usate il pe-editor di ProcDump .. killate (bello 'sto termine :))) la section del decrypter
oramai inutile (se non per studiarla ;).. se avete eseguito correttamenete queste operazioni
ora potete disassemblare allegramente con IDA :)
Tralasciando il codice di contorno quello che ci interessa inizia da 00402471:
loc_0_402471: ; CODE XREF: sub_0_40100C
call fnReadKeyFile ; apre/legge in mem il KeyFile - 00402551
cmp eax, 0FFFFFFFFh
jz loc_0_402530
mov cl, 6
mov eax, ds:CRC ; usa l'ultima dword del keyfile = CRC
rol eax, cl ; rol 6
mov ds:Seed, eax ; come seed iniziale
call fnBuildMagicTab ; prepara la tabella Magic 0..8
questo e' quello che avevamo gia' visto in sIce.. ora noterete i nomi che ho assegnato
in IDA.. questo merita un discorso a parte..
"Nomina Sunt Consequentia Rerum" , diceva S. Agostino; ma possiamo anche invertire
questo assunto e considerare gli oggetti che ci circondano in relazione al significato
che noi stessi gli diamo (no no non ho fumato cose strane ;)
imparare a commentare con IDA e' fondamentale.. e' quello che puo' fare la differenza..
iniziando da quel poco che capite del prog assegnate i nomi alle locazioni,alle proc, ai loop,
alle variabile nello stack,ecc. vedrete che man mano che aggiungente nomi/commenti anche lo
schema piu' ingarbugliato si dipanera'.
//Esempio pratico//---------------------------------------------------------------------------
00402481 mov eax, ds:40245Dh
premete il tasto CTRL + R quindi premete CRTL + R.. scegliete full 32 offset.. ora avrete
00402481 mov eax, ds:dword_0_40245D
cliccate su dword_0_40245D per andare a quell'offset.. premete tasto "N" ed assegnate il
nome CRC.. ora tornate a 402481 (premete ESC) ed avrete :
mov eax, ds:CRC
da adesso in poi quando farete CRTL+R su ds:40245Dh IDA scrivera' da solo CRC.. ed
aggiungera' una xref per quell'address. Quanto alla call, basta che cliccate su CODE XREF:
sub_0_40100C.. premete "N".. assegnate il nome e d'ora in poi vedrete sempre call
fnReadKeyFile invece di una piu' oscura call sub_0_402551. Ancora: per aggiungere commenti
usate il tasto ":" per i commenti singoli, e ";" per quelli ripetibili (provate ad esempio
ad aggiungerne uno all'inizio del codice di fnReadKeyFile scrivendo qualcosa
tipo "read keyfile , return eax=0 -> ok"
questi sono solo alcuni comandi che IDA vi mette a disposione per commentare il codice..
potete creare strutture, array, rinominare i parametri/variabili creati con un
stackframe esp+ebp,ecc.. IDA e' davvero interattivo, gli potete far fare quello che volete..
e' questa la differenza con wdasm : un dissamblatore per quanto intelligente non puo' capire
quello che invece potete capire voi usando il vostro cervello :))
Ok fine dell'angolino dell pubblicita' occulta ..ehm comprate IDA se avete i $ ofcoz!;)
//------------------------------------------------------------------------------------------//
Tornando al disasm quello che ci interessa ora e' la call fnBuildMagicTab:
fnBuildMagicTab proc near ; CODE XREF: DATA:0040248D
mov edi, offset Magic1 ; offset MagicTable
mov al, 55h ; seed iniziale "U"
mov ecx, 8
loc_0_4025D8: ; CODE XREF: fnBuildMagicTab+12
inc al
xor eax, 12h ; costruisce la Table con 8 Magics
stosb
loop loc_0_4025D8
retn
fnBuildMagicTab endp
qui non c'e' molto da dire tranne che la tabella e' sempre la stessa a prescindere dal
keyfile utilizzato.. good!
mov ecx, 14h ; decrypt loop = 8 byte x 20 = 160 bytes
mov esi, offset KeyFileBuffer
loc_0_40249C: ; CODE XREF: DATA:004024A4
call fnDecrypt ; decritta 2 dowrd x loop
add esi, 8 ; bf_index = bf_index + 8
loop loc_0_40249C
anche qui va da se'.. ora guardiamoci fnDecrypt..
mov edi, [esi] ; input : dword1 = buffer(bf_index)
mov ebx, [esi+4] ; dword2 = buffer(bf_index+4)
push esi
; <<--- pattern
mov cl, ds:Magic1
mov eax, edi
rol eax, cl
call fnAdjustXorMask ; X = funct(dword1 ROL MagicX)
xor ebx, eax ; dword2 = dword2 xor X
mov cl, ds:Magic2
mov eax, ebx
ror eax, cl
call fnAdjustXorMask ; Y = funct(dword2 ROR MagicY)
xor edi, eax ; dword1 = dword1 xor Y
; pattern --->>
mov cl, ds:Magic3
mov eax, edi
rol eax, cl
call fnAdjustXorMask ; come sopra ma con MagicX = Magic3
xor ebx, eax
mov cl, ds:Magic4
mov eax, ebx
ror eax, cl
call fnAdjustXorMask ; come sopra ma con MagicY = Magic4
xor edi, eax
.... continua stesso pattern x 10...
pop esi
mov [esi], edi ; output : buffer(bf_index) = dword1
mov [esi+4], ebx ; buffer(bf_index+4) = dword2
quello che salta subito all'occhio e' il pattern che si ripete con la sola variante
dei Magic utilizzati.. in pratica vengono decrittate 2 dword per volta, con una
sequenza di xor in cui la xor_mask per dword1 e' resa in funzione di dword2 ad ogni step.
I ROL , ROR non ci importano piu' di tanto visto che sono invarianti rispetto a dword1 e
dword2.. qualche grattacapo in piu' ce lo puo' dare fnAdjustXorMask:
fnAdjustXorMask proc near ; CODE XREF: fnDecrypt+14
push ecx
mov ecx, ds:Seed
shr ecx, 18h
ror eax, cl
xor eax, ds:Seed ; mask = (mask ror (seed shr 18)) xor seed
call fnAdjustSeed ; adjust seed
pop ecx
retn
fnAdjustXorMask endp
fnAdjustSeed proc near ; CODE XREF: fnAdjustXorMask+12
push eax
mov eax, 51656854h
cdq
imul ds:Seed
add eax, 6D6D482Bh
sub ds:Seed, eax ; seed = seed - (High(seed * const1) + const2)
pop eax
retn
fnAdjustSeed endp
qui come si vede la mask e' aggiustata ad ogni step in base al valore di Seed
che a sua volta e' modificato ad ogni step.. ebbravi The+Q & Plushmm :))
A questo punto l'algoritmo usato e' chiaro, anche se qualcuno potrebbe pensare
che non sia facile da reversare.. naaa, non per noi ;)
quello che dobbiamo fare ora per ottenere un crypter e quindi il nostro keymaker e'
invertire ogni step..
avendo le dword finali (il testo da crittare) quello che vogliamo e' ripercorrere a
ritroso la catena..
quindi come primo passo invertite tutti "pattern" partendo dall'ultimo verso il primo,
in questo modo i Magic saranno usati in ordine esattamente inverso e con i ROL,ROR
siamo a posto.. ora dobbiamo invertire la relazione che lega dword1 e dword2:
_pattern proc
X = funct(dword1 ROL MagicX)
dwordB = dword2 xor X
Y = funct(dwordB ROR MagicY)
dwordA = dword1 xor Y
_pattern endp
la cui inversa e':
_inv_pattern proc
dword1 = dwordA xor funct(dwordB ROR MagicY)
dword2 = dwordB xor funct(dword1 ROL MagicX)
_inv_pattern endp
bene.. fin qui ci siamo direte voi.. ma come la mettiamo con funct :
dobbiamo invertire anche quella? e poi come facciamo con fnAdjustXorMask che usa un seed
diverso ad ogni step!
azz quante domande! :)
Se guardiamo attentamente il dissasemblato quello che si nota e' che Seed viene inizializzato
prima del loop usando CRC ROL 6 e quindi si evolve usando fnAdjustSeed in modo indipendente
da dword1 e dword2, ma in relazione agli step percorsi..
ok e' fatta :) , ci basta codare una funct che ad ogni ciclo del loop esterno del crypter
costruisca una tabella con tutti i valori di Seed per quella iterazione (12 pattern =
24 entry nella tabella) e la usiamo al posto di fnAdjustSeed..
Per ottenere il crc basta che applichiate la stessa funzione usata dai programmatori sul
buffer (159byte+null) che volete criptare e quindi che salviate il valore come ultima
dword del buffer stesso.. et voila' avete il vostro keyfile!
Se mi avete seguito (io mi sono perso a dir il vero :) ora siete in grado di scrivere il
keymaker .. potete usare qualsiasi linguaggio (c/c++/asm/pascal).. io vi consiglio di
scriverlo in asm win32 perche' cosi' potete usare un'altra comodissima funzione di IDA
(te pareva ;)):
se avrete commentato bene il listato in IDA potrete esportare i disasm delle varie funzioni
(basta evidenziare il blocco e selezionare Menu File->Produce output file->Produce ASM file)
in un file .asm che e' direttamente utilizzabile in TASM una volta aggiunte le varabili
utilizzate al segmento dati.. in tutto 3min di lavoro :) un po' di cut&paste per invertire
l'algo di fnDecript e fnAdjustXorMask nel modo in cui abbiamo visto detto prima.. altri 4min..
un po' di coding per aggiungere la procedura per la costruzione della Seed_Table 3/4min...
shekerate con cura ed in 10/12min avrete fatto il keymaker.. DO YOU FEEL IDA'S POWER ?! :)
Per quelli pigri come me, vi aggiungo i src del mio keymaker.. mi raccomando non prendetelo
come esempio di coding in w32asm! ;)
--==[LE ALTRE TRE PROVE]==--------------------------------------------------------------------
Ok.. la prova del keyfile e' archiviata.. adesso come promesso parlero' brevemente
(eheh non vorrete che vi dica tutto io.. vi perdereste il gusto di provare :) ) delle
restanti tre prove: user code, nag screen e serial number:
a) lo user/code e' la parte piu' semplice: alla calculation ci arrivate con il classico
BPX GetDlgItemTextA.. qui il vostro nome e' manipolato in un loop che usa
ROR 8 + ADD (402111), mentre il code usa ROL 8 + ADD (4020F9).. quindi segue il check:
Result_Code = Result_Name ROR 8 (40211E)
non mi dilungo oltre su questa protezione perche' la soluzione e' davvero semplice..
b) anche il serial e' tutto sommato semplice tranne per il fatto che vengono utilizzate
istruzioni FPU (vi consiglio il cap. 14 della ArtOfAssembly completamente dedicato alla FPU)
ad ogni buon conto il break da sIce e' il solito BPX GetdlgItemTextA; quanto alla calculation
questa passa attraverso un ciclo che controlla la formattazione del serial : xxxxxxxx-yyyyyyyy
con x e y = numeri ed una funct di conversione ascii_to_long (402D17)..
le condizioni infine sono quattro basate sulla stessa equazione:
2^[trunc(c*X)]*2^[(c*X)-trunc(c*X)] mod Y = j
con c = 1*log2(k) ;
e rispettivamente k=[13,18,7,12] ; j=[2,7,18,23]
l'unica nota importante qui e' l'implementazione di un semplice check antisIce ,CreateFileA
per detectare il vxd di sIce w9x/NT (402D93), che se presente porta a falsare i calcoli
utilizzando un k fasullo nell'ultimo check.
c) Per quel che riguarda la prova del nag remover.. beh .. le regole impongono di non patchare
l'esegibile dumpato.. ora le possibili soluzioni sono piu' di una ma mi limitero' ad elencarne
le tre piu' comuni:
1) apihook : la import table non e' alterata quindi l'hook e' possibile.. ma non e' molto
pratico come metodo perche' impone la presenza di un loader.. passiamo oltre :)
2) patching from inside (C) xAONON (alla vbox per intenderci).
questa soluzione e' abbastanza semplice da realizzare soprattutto considerando che non
abbiamo bisogno di utilizzare WriteProcessMemory in quanto: a) siamo nello stesso spazio
di indirizzamento b) la section code e' gia' writable (lo deve essere visto che il crypter
la modifica senza utilizzare WriteProcessMemory).
Se steppate nel crypter noterete che termina con il classico jmp entrypoint (=eax)..
ora una piccola correzione a questo jmp e possiamo trasformarlo in jmp patch_thunk:
passiamo cioe' passare il controllo ad thunk ricavato alla fine della section Q_Plush
(dove c'e' il logo The+Q&Plushmm andra' benissimo :) che fungera' da mem patcher:
xor edx,edx
mov [eax+nag_box_ra],edx
ripristinate i reg e lo stack come nell'originale
jmp eax
con eax = entrypoint (gentilmente calcolatoci dal programma) e nag_box_ra = l'offset
rispetto all'entrypoint della "Call NagBox" .. cosi' da traformarla in E800000000
3) reverse dell'algoritmo di encryption: si tratta cioe' di calcolate la sequenza di byte
corretta da sostituire nell'exe per ottenere che la call venga decrittata come E800000000
(qui conviene che utilizziate un BPM address_Call_Nagbox per semplificarvi il compito).
L'encryption non e' molto complessa.. anzi a dir il vero e' un semplice xor con una mask
calcolata ad ogni step :
loc_40604D: ; CODE XREF: Q_Plush:00406070
push ecx
lodsd
xor eax, [ebp+402848h]
mov ecx, [ebp+402848h]
shr ecx, 18h
rol eax, cl
attenzione pero' che la section successiva (.DATA) e' stata crittata con un seed iniziale
calcolato sulla base del crc dei dati in chiaro della precedente section (.CODE):
anche questo problema e' aggirabile senza troppa fatica: basta che vi segnate il valore del
seed corretto e lo hardcodate (miii che brutto termine :)) nel codice al posto di:
004060B2 mov word ptr [ebp+402848h], 0FFFFh
004060BB mov word ptr [ebp+40284Ah], 0FFFFh
e saltate la routine di calcolo andando direttamente all'adress 00406135.
--==[CONCLUSIONI]==--------------------------------------------------------------------------
Questo e' tutto.. adesso e' giunto il momento di tirare le somme:
quello che mi premeva dimostrarvi in questo tutorial e' che IDA e' uno strumento davvero
eccezionale, specie con target piuttosto elaborati come il keyfile di questo CrackMe, e
soprattutto di come possa essere usato per facilitarvi il coding di un keymaker/keygen.
Non so se sono stato chiaro.. ma almeno ci ho provato :)