PC98 Trial CrackMe KeyFile Solution
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 :)