DeC Defeating
(crackme decaffeinato)

Data

by Spider

 

1 Aprile 2004

UIC's Home Page

Published by Quequero


Quant'è bella giovinezza,
che si fugge tuttavia!
Chi vuol esser lieto, sia:

di doman non c'è certezza.

              Lorenzo il Magnifico

Grazie spi, bel tute.

Tre cose solamente m'ènno in grado,
le quali non posso ben ben fornire,
cioè la donna, la taverna e 'l dado:
queste mi fanno 'l cuor lieto sentire.

                            Cecco Angiolieri

 

....

Home page: http://bigspider.has.it o http://bigspider.cjb.net
E-mail: spider_xx87 (AT) hotmail (DOT) com
Nick, canale IRC frequentato: ^Spider^ su irc.azzurra.org
canali #crack-it e #asm

....

Difficoltà

( )NewBies (X)Intermedio ( )Avanzato ( )Master

 

In questo tutorial ci occuperemo del crackme DeC (decaffeinato? :P) di x86.


DeC Defeating
Written by Spider

Introduzione

Da tanto tempo non reversavo nulla per mancanza di tempo (e non credo che in futuro cambierà qualcosa...). Ogni tanto però mi ritornava il pallino, e finalmente qualche giorno fa mi sono deciso. Il crackme di x86 mi sembrava un buon target, perciò mi sono armato di debugger, e via! :) 

Tools usati

Tools usati:

- SoftICE
- IDA Pro
- Un qualunque HexEditor
- Un dumper (ad esempio il wark di Ntoskrnl :))

Si presuppone nel lettore una conoscenza almeno di base dell'utilizzo dei tools elencati.

URL o FTP del programma

http://quequero.org/crackme/filez/DeC.zip

Notizie sul programma

Il crackme è condito di un po' di tricks antidebugger, codice criptato e qualche controllo di integrità sul codice. L'obiettivo del crackme è quello di far apparire la MessageBox di congratulazioni.

Essay

Eseguiamo il crackme (possibilmente senza softice caricato), e compare la MessageBox di about e poi quella di errore. Nel readme.txt x86 parla di faults, e questo fa già presupporre la presenza di SEH. E quando in un crackme ci sono le seh, ci si può tipicamente preparare per tutta una serie di altri tricks. Perciò meglio essere cauti: carichiamo il softice, e eseguiamo il crackme dal Symbol Loader, per steppare fin dall'entry point:

CODE:00401000 sub ecx, ecx      ;ecx servirà come contatore
CODE:00401002 mov esi, 00401199
CODE:00401007 mov al, [esi+ecx] ;prende un byte
CODE:0040100A rol al, cl        ;lo rolla di cl bytes
CODE:0040100C xor al, 78        ;lo xora con 0x78
CODE:0040100E mov [esi+ecx], al ;e infine lo rimette a posto
CODE:00401011 add esi, ecx
CODE:00401013 cmp esi, 004011B9 ;abbiamo finito?
CODE:00401019 jz 0040101E       ;se sì salta fuori
CODE:0040101B inc ecx           ;altrimenti incrementa il contatore
CODE:0040101C jmp 00401002      ;e ripeti ancora

Già in queste prime righe troviamo qualcosa di interessante: un layer di decrypt. Il decrypt va dall'offset 401199 all'offset 4011B9, cioè all'interno della sezione di codice. Tenete presente questo codice perché incontreremo molti decrypt simili nel crackme. Mentre siamo in softice, per uscire dal ciclo mettiamo un breakpoint all'indirizzo 40101E. NOTA: E' ininfluente in questo caso, ma da ora in poi vi consiglio di utilizzare i bpm (breakpoint on memory address) anziché i semplici bpx. Infatti l'inserimento di un bpx altera temporaneamente il codice (infatti il debugger mette il byte 0xCC al posto dell'istruzione), mentre i bpm utilizzano i debug registers e non toccano il codice. Quindi in questo caso il comando da dare al softice sarebbe "bpm 40101E x", dove la "x" serve per indicare che è un breakpoint in esecuzione e non in lettura o scrittura.

CODE:0040101E call 0040118E

Questa call ci porta qui:

CODE:0040118E call 00401199
CODE:00401193 sub ecx, ecx
CODE:00401195 add ecx, 4
CODE:00401198 ret

Un bel salto al codice appena decriptato. Da un disassembler non vedrete nulla di sensato a quell'address, mentre continuando a steppare da softice abbiamo davanti ciò che segue:

CODE:00401199 sub ecx, ecx
CODE:0040119B mov esi, 00402000
CODE:004011A0 mov al, [esi+ecx]
CODE:004011A3 rol al, cl
CODE:004011A5 xor al, 78
CODE:004011A7 xor al, cl
CODE:004011A9 mov [esi+ecx], al
CODE:004011AC add esi, ecx
CODE:004011AE cmp esi, 004021FF
CODE:004011B4 jz 004011B9
CODE:004011B6 inc ecx
CODE:004011B7 jmp 0040119B
CODE:004011B9 ret

Praticamente identico al precedente... Decripta tutti i dati dall'indirizzo 402000 fino all'indirizzo 4021FF, con lo stesso algoritmo di decrypt visto prima.

CODE:00401023 mov [004020BD], esp
CODE:00401029 push 0040128D
CODE:0040102E call KERNEL32!SetUnhandledExceptionFilter
CODE:00401033 mov [004020B9], eax

Qui abbiamo l'installazione di un SEH frame all'indirizzo 0040128D. Vedremo dopo a che serve e cosa contiene.

CODE:00401038 sub ebx, ebx
CODE:0040103A push 00402058
CODE:0040103F call KERNEL32!GetLocalTime
CODE:00401044 mov bx, [00402064]
CODE:0040104B mov [004020B3], bx

Il crackme preleva il data e ora. Da softice possiamo vedere che ciò che viene messo in bx e poi nell'indirizzo 4020B3 sono i secondi. Poiché non vengono usati subito, possiamo immaginare che il programma userà dopo questi dati che sta salvando, quindi appuntiamo questo fatto e andiamo avanti.

CODE:00401052 sub ecx, ecx
CODE:00401054 mov esi, 00402074 
CODE:00401059 mov al, [esi+ecx]
CODE:0040105C cmp al, 0
CODE:0040105E jz 00401068
CODE:00401060 xor al, 78
CODE:00401062 mov [esi+ecx], al
CODE:00401065 inc ecx
CODE:00401066 jmp 00401059
;Fin qui decripta una stringa 0-terminated all'indirizzo 402074.
;Dopo il decrypt possiamo vedere che la stringa è "OllyDbg"

CODE:00401068 push 0           ; lpWindowName
CODE:0040106A push 00402074    ; lpClassName
CODE:0040106F call USER32!FindWindowA
CODE:00401074 cmp eax, 0
CODE:00401077 jnz 004011BC
;Cerca una finestra della classe "OllyDbg", se la trova si incazza e salta
;all'indirizzo 004011BC dove ci sono un paio di jmp che fanno bloccare il crackme.

CODE:0040107D sub ecx, ecx
CODE:0040107F mov esi, 0040207C
CODE:00401084 mov al, [esi+ecx]
CODE:00401087 cmp al, 0
CODE:00401089 jz 00401095
CODE:0040108B xor al, 36h
CODE:0040108D xor al, 38h
CODE:0040108F mov [esi+ecx], al
CODE:00401092 inc ecx
CODE:00401093 jmp 00401084
CODE:00401095 push 0            ; lpWindowName
CODE:00401097 push 0040207C     ; lpClassName
CODE:0040109C call USER32!FindWindowA
CODE:004010A1 cmp eax, 0
CODE:004010A4 jnz 004011BC
;Idem, ma stavolta la finestra cercata è "FileMonClass"

Ovviamente se state usando OllyDbg non dovete consentire il salto che c'è all'offset 00401077.
Andiamo avanti:

CODE:004010AA call 0040126B

Chiama questo codice:

CODE:0040126B sub ecx, ecx
CODE:0040126D mov esi, 004011C9
CODE:00401272 mov al, [esi+ecx]
CODE:00401275 rol al, cl
CODE:00401277 xor al, 78
CODE:00401279 mov [esi+ecx], al
CODE:0040127C add esi, ecx
CODE:0040127E cmp esi, 0040122A
CODE:00401284 jz 004010AF
CODE:0040128A inc ecx
CODE:0040128B jmp short 0040126D

Questo ciclo decripta il codice da 004011C9 a 0040122A.

CODE:004010AF sub ecx, ecx
CODE:004010B1 mov esi, 00402089
CODE:004010B6 mov edi, 0040209C
CODE:004010BB mov al, [esi+ecx]
CODE:004010BE mov bl, [edi+ecx]
CODE:004010C1 cmp al, 0
CODE:004010C3 jz 004010CD
CODE:004010C5 xor al, bl
CODE:004010C7 mov [edi+ecx], al
CODE:004010CA inc ecx
CODE:004010CB jmp 004010BB
CODE:004010CD call 004011C9

Questo ciclo è un po' diverso da quelli visti finora: decripta una stringa all'indirizzo 40209C xorando ogni suo byte con una chiave che si trova all'indirizzo 00402089. La stringa risultante è "\\.\SICE", che puzza (anzi ne è la firma) di antisoftice... Ecco infatti cosa c'è all'indirizzo 004011C9 (decrittata poco fa):

CODE:004011C9 push 0
CODE:004011CB push 80
CODE:004011D0 push 3
CODE:004011D2 push 0
CODE:004011D4 push 3
CODE:004011D6 push C0000000
CODE:004011DB push 0040209C    ; lpFileName
CODE:004011E0 call KERNEL32!CreateFileA
CODE:004011E5 mov [004020B5], eax
CODE:004011EA cmp eax, FFFFFFFF
CODE:004011ED jz 004010D2   ;questo deve saltare
CODE:004011F3 push 004011BC ;solita locazione che fa bloccare il crackme
CODE:004011F8 ret

Subito dopo c'è del codice simile:

CODE:004010D2 sub ecx, ecx
CODE:004010D4 mov esi, 00402092
CODE:004010D9 mov edi, 004020A5
CODE:004010DE mov al, [esi+ecx]
CODE:004010E1 mov bl, [edi+ecx]
CODE:004010E4 cmp al, 0
CODE:004010E6 jz 004010F0
CODE:004010E8 xor al, bl
CODE:004010EA mov [edi+ecx], al
CODE:004010ED inc ecx
CODE:004010EE jmp 004010DE
CODE:004010F0 call 004011FA

Stavolta la stringa che viene decrittata all'indirizzo 4020A5 è "\\.\NTSICE". Non pasto il codice della call perché è identico a quello visto prima (chiama CreateFileA e controlla il valore di ritorno).

Da notare che comunque questo trick potrebbe non funzionare... Nel mio PC ad esempio ho DriverStudio 3.0 e WindowsXP, e softice non viene rilevato :)

CODE:004010F5 xor eax, eax
CODE:004010F7 sub ebx, ebx
CODE:004010F9 sub ecx, ecx
CODE:004010FB sub edi, edi
CODE:004010FD mov esi, 00401000
CODE:00401102 mov al, [esi+edi]
CODE:00401105 add ecx, eax
CODE:00401107 add esi, edi
CODE:00401109 inc edi
CODE:0040110A cmp esi, 004012C5
CODE:00401110 jnz 004010FD
CODE:00401112 mov [004020B1], cx
CODE:00401119 call 0040122B


Qui abbiamo un checksum su tutta la regione di codice tra gli offset 401000 e 4012C5; i bytes vengono sommati su cx e il risultato viene salvato nella word all'indirizzo 4020B1. Se non avete toccato niente dovrebbe risultare F7BF. Entriamo nella call dell'ultima riga:

CODE:0040122B xor eax, eax
CODE:0040122D sub ebx, ebx
CODE:0040122F sub ecx, ecx
CODE:00401231 sub edi, edi
CODE:00401233 mov esi, 00401000
CODE:00401238 mov bl, 11
CODE:0040123A mov al, [esi+edi]
CODE:0040123D add bl, BB ;bl = CC
CODE:00401240 cmp al, bl ;confronta con il carattere prelevato
CODE:00401242 jz 004011BC ;solita locazione di freeze
CODE:00401248 add esi, edi
CODE:0040124A cmp esi, 004012C5
CODE:00401250 jz 00401255
CODE:00401252 inc edi
CODE:00401253 jmp 00401233

In questa parte il programma cerca in tutta la sezione di codice il fatidico byte 0xCC che indicherebbe la presenza di un breakpoint. Noi continuiamo ad usare i bpm, così non abbiamo rogne.

CODE:00401255 mov cx, [004020AF] ;prende il corretto checksum
CODE:0040125C cmp [004020B1], cx ;e lo confronta con quello salvato prima
CODE:00401263 jz 0040126A        ;questo è bene che salti...
CODE:00401265 push 004011BC      ;...altrimenti solito indirizzo di freeze
CODE:0040126A ret

Evidentemente dopo che aveva salvato il checksum dovevamo aspettarci un controllo... ed eccolo qui :)
Proseguiamo.

CODE:0040111E push 00402058      ;preleva le informazioni su data e ora
CODE:00401123 call KERNEL32!GetLocalTime
CODE:00401128 mov ax, [00402064] ;e mette in ax i secondi
CODE:0040112E cmp [004020B3], ax
CODE:00401135 jnz 004011BC

Vi ricordate che c'era all'indirizzo 4011B3? Vi avevo detto di appuntarlo! C'erano i secondi... Il programma confronta i secondi attuali con quelli di un bel po' di righe prima... Perché? Molto semplice: se non debuggate le due misurazioni saranno uguali, dato che l'esecuzione del codice del crackme è pressoché istantanea. Se debuggate, passerà un po' di tempo e i secondi saranno diversi... e il programma vi sgama :) Quindi se necessario forzate il salto (che NON deve saltare) e andate avanti senza problemi.

CODE:0040113B call 00401181
CODE:00401140 call 0040114A
CODE:00401145 jmp 004012C6

Vediamo la prima call:

CODE:00401181 sub eax, eax
CODE:00401183 add ebx, eax
CODE:00401185 xor ebx, ebx
CODE:00401187 xor ecx, ecx
CODE:00401189 sub edx, edx
CODE:0040118B xor edi, edi
CODE:0040118D ret

Niente di "vitale"... azzera un po' tutti i registri. Vediamo invece l'altra call:

CODE:0040114A push 00402058
CODE:0040114F call KERNEL32!GetLocalTime
CODE:00401154 mov ax, [00402060]
CODE:0040115A mov cx, [00402060]

Qui preleva l'ora e mette in ax e cx rispettivamente l'ora e i minuti (ce ne accorgiamo semplicemente guardando il contenuto dei registri durante il debugging).

CODE:00401161 mov esi, 004012C6
CODE:00401166 mov dl, [esi+edi] ;preleva un byte
CODE:00401169 xor dl, 73        ;lo xora con 73
CODE:0040116C rol dl, cl        ;lo rolla a sinistra di cl bytes (cl = minuti)
CODE:0040116E xor dl, al        ;lo xora con al (al = ora)
CODE:00401170 mov [esi+edi], dl ;e lo rimette a posto
CODE:00401173 add esi, edi
CODE:00401175 cmp esi, 004012F2
CODE:0040117B jz 00401180
CODE:0040117D inc edi
CODE:0040117E jmp 00401161
CODE:00401180 ret

Questo ciclo decripta l'area di codice dall'offset 004012C6 all'offset 004012F2. La differenza con gli altri cicli incontrati finora è che qui le chiavi non sono costanti, ma sono rispettivamente l'ora e i minuti correnti! E soprattutto notiamo che il codice all'indirizzo 004012C6 viene eseguito immediatamente al ritorno dalla call, infatti incontriamo la riga (già vista prima):

CODE:00401145 jmp 004012C6

Dunque decripta il codice con chiavi pseudocasuali e subito dopo lo esegue. Possiamo immaginare che esiste una combinazione di ora e minuti che decripti codice valido, ma normalmente capite bene che non succede. Però il programma non crasha: come mai? Ovviamente perché c'è una SEH! Abbiamo visto all'inizio del codice che veniva installata una seh all'indirizzo 0040128D (per arrivarci, usiamo come al solito un bpm):

CODE:0040128D mov esp, ds:dword_4020BD ;mette a posto lo stack, anche se non serve
CODE:00401293 push offset loc_401299
CODE:00401298 retn
CODE:00401299 push 20
CODE:0040129B push 004020C9
CODE:004012A0 push 004020C9
CODE:004012A5 push 0
CODE:004012A7 call USER32!MessageBoxA
CODE:004012AC push 10
CODE:004012AE push 0040216E
CODE:004012B3 push 00402178
CODE:004012B8 push 0
CODE:004012BA call USER32!MessageBoxA
CODE:004012BF push 0
CODE:004012C1 call KERNEL32!ExitProcess

Le due messagebox sono rispettivamente quella di about e la beggar off. Inoltre, se cerchiamo in memoria, troviamo invece la stringa "Registered" all'indirizzo 402192. Dovrebbe essere dunque chiara la dinamica del crackme: decripta il codice usando come chiavi ora e minuti, e quindi lo esegue: se è errato, viene generata un'eccezione e il controllo passa alla seh, che si preoccupa di visualizzare la beggar off. Il problema è quindi quello di trovare l'ora esatta per il decrypt. Le combinazioni non sono molte: per l'ora abbiamo 24 possibilità, per i minuti 60 possibilità... Ma i minuti vengono usati per un rol, che è periodico con periodo 8... In altre parole se x è una chiave valida, possiamo aggiungere o togliere 8 e ottenere un'altra chiave valida, dunque le combinazioni da provare per i minuti sono solo 8, e in totale 24 * 8 = 192 combinazioni: comunque troppe per un'analisi a mano. Dobbiamo bruteforcare, ma per fare questo ci serve qualche info. L'ideale sarebbe conoscere qualche byte del codice decriptato, in modo da verificarne la presenza per un check del codice... Ma noi abbiamo ben 4 bytes! La stringa registered si trova all'offset 00402192, quindi possiamo aspettarci di trovare, nel codice decriptato, la sequenza di bytes 92 21 40 00. Scriviamo quindi un brute in assembler (il codice seguente è per MASM):

;==========8<==8<==8<==8<==8<==8<==8<==8<==8<==8<==8<==8<============

.586p
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.data

;Codice criptato
CryptedCode dd 8780C290h
            dd 80C3C1CAh
            dd 0C3C1CA57h
            dd 0C284C390h
            dd 90C3C3C3h
            dd 0CA8180C2h
            dd 5180C3C1h
            dd 90C3C1CAh
            dd 0C3AB84C3h
            dd 0C390C3C3h
            dd 0C3C33384h
            db 0C3h

.data?

;Array per il codice decriptato
CryptedTry db 50 dup (?)

.code

start:
    xor edx,edx

    xor eax, eax ;eax = ora
    altraora:
        xor ecx, ecx ;ecx = minuto
        altrominuto:
            xor esi,esi ;usiamo esi come contatore
            decrypt:
                mov dl, byte ptr[CryptedCode + esi]
                xor dl, 73h
                rol dl, cl
                xor dl, al
                mov byte ptr[CryptedTry + esi], dl
                inc esi
                cmp esi, 45
                jbe decrypt

            xor esi,esi ;usiamo esi come contatore
            search:
                cmp dword ptr[CryptedTry + esi], 00402192h
                jnz notfound

                jmp found
               notfound:
                inc esi
                cmp esi, 45
                jbe search
        inc ecx
        cmp ecx, 8
        jb altrominuto
    inc eax
    cmp eax, 24
    jb altraora

    call ExitProcess

found:
    int 3

end start

;==========8<==8<==8<==8<==8<==8<==8<==8<==8<==8<==8<==8<============

Penso che il codice sia abbastanza chiaro da non necessitare ulteriori spiegazioni... L'unico appunto: se viene trovata una combinazione valida, non viene visualizzata una MessageBox o altro, ma viene chiamato solo un int3, che verrà intercettato da softice con un "bpint 3". Ho deciso di fare così perché non è detto che la prima soluzione trovata sia quella esatta (anche se le combinazioni sono MOLTO poche e la probabilità di una soluzione errata è MOLTO bassa). Quando softice poppa, possiamo leggere l'ora in eax e i minuti in ecx. Battendo al softice "bpint 3" ed eseguendo, scopriamo che la soluzione è l'ora 22:05, e per la periodicità del rol possiamo aggiungere ai minuti qualunque multiplo di 8, quindi sono ore valide anche le seguenti: 22:13, 22:21, 22:29, 22:37, 22:45, 22:53. In qualunque di queste ore il crackme visualizzerà la messagebox di congratulazioni ("You are a Goog Reverser!!!!") :)

Di fatto il crackme è già risolto. Se volete potete anche patcharlo per far visualizzare sempre la messagebox di congratulazioni. Io ho fatto così: ho avviato il programma e l'ho debuggato fino alla fine con data ed ora adatti al decrypt... Quindi ho dumpato tutto, e ho forzato tutti i jump necessari e noppato gli altri (è una palla perché si deve scorrere di nuovo tutto il codice, ma ci vogliono 10 minuti). Inoltre eliminiamo anche le istruzioni per il decrypt perché ora il codice è già decriptato. Non ve lo spiego nei dettagli perché ci sono un bel po' di istruzioni da modificare ed è un lavoro ripetitivo, quindi... rimboccatevi le maniche :)

In alternativa si potrebbero alterare le chiamate a GetLocalTime (ritoccando il thunk alla fine della sezione di codice, all'indirizzo 004012FF... Infatti tutte le chiamate a GetLocalTime passano per questo jump :) ) in modo che ritorni sempre la stessa data ed ora, e in questo modo è molto più elegante e rapido... Insomma, potete sbizzarrirvi :)

                                                                                                                 Spider

Note finali

Saluti ad x86 che ha scritto il crackme e ha sbagliato a scrivere la messagebox di congratulazioni ("You are a Goog Reverser")... In realtà x86 è un agente pubblicitario di www.google.com, e quella è pubblicità occulta per invitare noi reverser ad usare google... :)

Disclaimer

Vorrei ricordare che il crackme NON va comprato perché il target era un crackme, quindi crackandolo rendiamo l'autore ancora più contento! :)

Noi reversiamo al solo scopo informativo e di miglioramento del linguaggio Assembly.