- UNIVERSITA' ITALIANA CRACKING -
3° Corso per Studenti

 CODICE AUTOMODIFICANTE


Scarica qui il programma per questo corso (1.9 kb)


Benvenuti a tutti in questo terzo corso della UIC, quest'oggi parleremo ampiamente di codice auto-modifcante (SMC).
Dunque, prima di iniziare vorrei dire che gli esempi presentati sono stati presi da uno dei migliori IMHO tutorial esistenti che è quello di +mammon_ tradotto magnificamente da Little John e che troverete anche nell'Assembly Journal, la versione italiana invece la troverete sul sito di ringzer0.
Allora, il codice automodificante (da ora SMC cioè: Self-Modifyng code) è una delle tecniche più sopraffine di programmazione, l'unico problema è che è "inutile", o meglio, non avrebbe avuto motivo di esistere se non fossero esistiti anche i Reverser ed i virus, infatti l'SMC viene usato all'interno delle routine di protezione per rendere difficoltosa l'individuazione del seriale oppure nei virus per tener nascosto il codice, in più un programma che usa l'SMC è anche instabile, quindi vi starete chiedendo, perchè dobbiamo studiarlo? Bhè, 1° perchè è bello, 2° perchè la cultura è la cosa più preziosa che abbiamo e che dobbiamo ampliare in tutti i campi, 3° perchè la troverete in numerosi crackme ed in qualche programma: Conseal PC Firewall ecc.....
Ah QuèQuè, ma sto SMC che cavolo è? Ok arrivo al dunque, non vi scaldate :).
Il presupposto del SMC è quello di sovrascrivere una sezione o comunque parte di essa, in parole povere significa che se abbiamo due parti di codice in un programma come questo:

 

parte1:

mov eax, seriale

        cmp eax, seriale_inserito
        jne registrazione_fallita
parte2:
        push offset caption
        push offset messaggio
        call MessageBoxA
 
con opportune istruzioni che spiegheremo tra poco potremmo far in modo che durante l'esecuzione il programma sovrascriva le ultime tre righe con le prime tre, il vantaggio sta nel fatto che noi potremmo mettere le prime tre righe di codice in una parte isolata che non verrebbe mai usata dal prg e le ultime tre nella parte dove dovrebbe stare la routine di comparazione del seriale, in questo modo proteggiamo il nostro programma almeno da un attacco col disassembler in quanto sembrerebbe solo una routine che genera un messaggio e non una routine di comparazione, risultato diverso lo otterremmo a run-time infatti sotto debugger vedremmo improvvisamente cambiare tre righe e non ne capiremo molto, oltre a questo possiamo dire che il programma sarà molto difficile da patchare infatti supponiamo che il jump da forzare si trovi secondo sice all'indirizzo 00401234, se andassimo nel W32Dasm per ricavarne l'offset troveremo la prima istruzione della seconda parte del programma, in pratica succederebbe questo:

 

in SoftIce:      xxxx:00401234         jne registrazione_fallita
nel W32dasm:     xxxx:00401234         push offset caption

 

ed in quel momento vi verrà da dire: "Ma mi stai prendendo per il culo????"
Bhè in effetti si, il programma si prende gioco di voi, ma visto che per antonomasia il reverser è più intelligente del programmatore shareware fa una bella cosa, ritorna in sice, scrive alla riga di comando: code on
e si segna una manciata di byte vicini all'isutruzione che ci interessa, poi va nell'hex-editor oppure nel w32dasm e li cerca, una volta trovati li patcha. Può anche non essere così semplice, mi capitò infatti un programma nel quale l'autore aveva inserito moltissime volte quella manciata di byte che cercavo, anche questo non è un problema perchè voi cercherete una stringa sempre più lunga di byte finchè non la troverete :)))
Ma andiamo sul pratico e vediamo com'è fatto un programma SMC, quelli che vi presento sono alcuni degli esempi fatti da +Mammon_ e tradotti dal Little John, uso quelli perchè sono molto chiari e ben spiegati:

 

mov       reg1,                         codice-da-sostituire
mov       [indir-su-cui-scrivere],      reg1

in pratica spostiamo l'offset del codice che ci interessa sostituire in un registro e poi spostiamo il contenuto di quel registro in un puntatore all'indirizzo che vogliamo cambiare, per farvi meglio capire vi mostro un altro esempio di mammon_:
 
call changer
mov dx, offset [string]          ; questo sarà eseguito ma ignorato
label:
mov ah, 09                       ; questo non sarà mai eseguito
int 21h                          ; questo chiuderà il programma
....
changer:
mov di, offset to_write          ; carica l'indirizzo del codice da scrivere in DI
mov byte ptr [label], [di]       ; scrivi il codice nella locazione 'label'
ret                              ; ritorna dalla chiamata
to_write:
mov ah, 4Ch                      ; codice di fine programma (int 21, ah=4c ndt)
adesso cercherò di spiegarvi cosa succede in questo programma, iniziamo col dire che parliamo di codice a 16bit e non a 32bit per cui dobbiamo avvalerci degli interrupt per poter fare qualcosa, supponiamo di aver disassemblato il file, come possiamo vedere abbiamo una prima chiamata, poi muoviamo in dx l'offset di una stringa e poi la stampiamo su schermo (sotto dos per stampare una stringa si muove l'offset della stessa in DX, poi si usa il servizio 09 dell'interrupt 21h, in pratica si muove in AL il numero 9 e poi si chiama l'interrupt che stamperà la stringa puntata da DX) ottimo questa routine non serve a nulla, anzi stampa solo una stringa, a questo punto potremmo chiudere tutto ed andare a giocare a Carmageddon però noi che siamo tipi seri apriamo il debugger e debuggiamo tutto. Ok siamo in Sice, passiamo con F10 sopra la chiamata changer e.........Mmmm strano l'istruzione "mov ah, 09" si è trasformata in "mov ah, 4Ch" che sotto dos è la procedura per chiudere il programma....Se se altro che stampare strighe quella chiamata fa chiudere il programma, bene bene, riapriamo il debugger e stavolta entriamo con F8 in quella chiamata: dunque, l'offset della sezione "to_write" viene spostato in DI, quindi DI punterà alla prima riga di byte che sono in quella sezione, poi quei byte vengono spostati guardate un po' dove??? Già nella prima riga della sezione label, heheh in pratica quindi al posto di "mov ah, 09" ci troviamo "mov ah, 4ch", ingegnoso eh? Tutto sto bordello si poteva risparmiare scrivendo semplicente:
 
mov ah, 4ch
int 21h
 
quindi due istruzioni contro le otto del primo esempio, come vediamo l'SM si paga quindi sia in termini di velocità di esecuzione che in termini di tempo di scrittura da parte del programmatore ed anche in livello di rincoglionimento sempre da parte del programmatore, heheh quante belle cose si inventano per confonderci le idee in capa??? :)  
Altro lampante esempio di SMC è l'Encrittazione, in pratica sono le stesse righe che vi riportato all'inizio solo che vengono usati altri operatori per alterare il contenuto dei regsitri, eccovi il solito esempio da mammon_:

 

mov reg1, indir-da-sovrascrivere
mov reg2, [reg1]
manipola reg2
mov [reg1], reg2
 
Cosa succede in questa situazione? Semplice, carichiamo in un registro l'offset della sezione che ci interessa sovrascrivere, spostiamo poi i byte puntati da questo registro in un altro registro e lo manipoliamo un po come ci pare (con shift, rotate xor) poi lo rispostiamo nuovamente nella locazione originale, ecco uno snippet del programma di mammon_ tradotto da Little John (stavolta è l'ultima che la ripeto):
 
secret_word      db      06Ch, 04Dh, 082h, 0D0h
magic_key        db      18h, 25h, 0EBh, 0A3h

 

----- snip snip-----

mov al, [magic_key]                 ; metti il byte1 della maschera XOR in al
mov bl, [secret_word]               ; metti il byte1 della password in bl
xor al, bl
mov byte ptr secret_word, al        ; cambia il byte1 della password
mov al, [magic_key+1]               ; metti il byte2 della maschera XOR in al
mov bl, [secret_word+1]             ; metti il byte2 della password in bl
xor al, bl
mov byte ptr secret_word[1], al
mov al, [magic_key+2]
mov bl, [secret_word+2]
xor al, bl
mov byte ptr secret_word[2], al
mov al, [magic_key+3]
mov bl, [secret_word+3]
xor al, bl
mov byte ptr secret_word[3], al
mov cx, 4
mov si, offset KbBuffer
mov di, offset secret_word
rep cmpsb
or cx, cx
jnz bad_guess
il programma intero non era altro che un exe per dos che una volta avviato chiedeva una pass, a noi il resto non interessa ma la routine si e pure tanto, vediamo cosa succede qui, per prima cosa abbiamo una secret_word ed una magic_key, dunque la secret_word sarebbe la nostra password ed è "this" il problema è che qui non è visibile in quanto xorata, in questo modo non è visibile ad un disassembler, poi c'è la magic_key che è la maschera che serve a decrittare la pass, infatti se aprite la calcolatrice di windows in modalità avanzata e settate su esadecimale e poi ancora fate 6c4d82d0 xor 1825EBA3 otterrete 74686973 che in ASCII è proprio: this.
Come vedete il programma mette il primo byte della maschera in AL ed il primo byte della password in BL, li xora e pone il risultato di nuovo nel primo byte della pass che da 6c è diventato 74, poi mette il secondo byte della maschera in AL, il secondo della pass in BL, li xora e mette il risultato ancora nel secondo byte della pass che cambia da 4d in 68 e così via per gli altri, cosa stà facendo? Semplice come vedete sta decrittando la password, infine con un semplice "rep cmpsb" controlla che la pass inserita (KbBuffer) sia uguale alla pass che è stata appena decrittata (secret_word), possiamo ottimizzare il tutto aggiungendo questo loop:

 

loop:
xor cl, cl
mov al, [magic_key+cl]                   
mov bl, [secret_word+cl]                 
xor al, bl
mov byte ptr secret_word[cl], al    
inc cl
cmp cl, 3
jl loop 

 

in tal modo prepariamo un contatore a partire da 0 con CL che si incrementa ad ogni loop, arrivato ed appena xorato il 4° carattere (0, 1, 2, 3) esce dal loop, ma usando codice a 32bit potevamo fare di meglio cioè:

 

 
secret_word      dd      6C4D82D0h
magic_key        dd      1825EBA3h
 
mov eax, dword ptr [magic_key]                   
mov ebx, dword ptr [secret_word]                 
xor eax, ebx
mov dword ptr secret_word, eax     

 

in pratica spostavamo l'intera doubleword (4 byte cioè 4 caratteri) della pass in EBX e l'intera doubleword della maschera in EAX, le xoravamo e ponevamo l'intero risultato in EAX :))).
OK ora abbiamo spiegato le basi dell'SMC adesso invece parleremo di una tecnica un poco più complicata che mammon_ illustra chiaramente in quest'altro esempio che ho editato leggermente aggiungendo il mio loop:
 
mov cx, offset EndPatch[1]         ;inizio dell'indiriz-da-sovrascriv + 1
sub cx, offset patch_pwd           ;fine dell'indiriz-da-sovrascriv
mov ax, cs                         ;salva il code segment
mov dx, ss                         ;salva lo stack segment -- importante!
mov ss, ax                         ;setta lo stack segment come code segment
mov bx, sp                         ;salva lo stack pointer
mov sp, offset EndPatch            ;inizio dell'indiriz-da-sovrascriv
mov si, offset Exit-1              ;inizio dell'indiriz della maschera XOR

XorLoop:
pop al                             ;prendi il byte-da-patchare in AL
xor al, [si]                       ;XORa al con la XorMask
push ax                            ;scrivi il byte-to-patch in memoria
dec sp                             ;carica il successivo byte-da-patchare
dec si                             ;carica il successivo byte della maschera XOR
cmp si, offset Start               ;fine della maschera XOR?
jae GoLoop                         ;Se No, continua
mov si, offset Exit-1              ;reinizializza la maschera XOR

GoLoop:
loop XorLoop                       ;XORa il byte successivo
mov sp, bx                         ;ripristina lo stack pointer
mov ss, dx                         ;ripristina lo stack segment
jmp patch_pwd

db 0CCh,0CCh                       ;Identifcation mark: START

patch_pwd:
xor cl, cl
mov al, [magic_key+cl]                   
mov bl, [secret_word+cl]                 
xor al, bl
mov byte ptr secret_word[cl], al    
inc cl
cmp cl, 3
jl patch_pwd

 

mov cx, 4
mov si, offset KbBuffer
mov di, offset secret_word
rep cmpsb
or cx, cx
jnz bad_guess
mov word ptr cs:PatchSpot[1], offset szString1

bad_guess:
call Reply
ret

Compare endp

EndPatch:

db 0CCh, 0CCh                      ;Identification Mark: END

Adesso state molto attenti perchè il discorso non è dei più semplici, ma cercherò comunque di farvi capire: per prima cosa dovete sapere che questo programma attua una crittazione dal basso, cioè dall'ultima alla prima istruzione e non vice versa, per prima cosa viene spostato in CX l'offset dell'inizio dell'indirizzo da sovrascrivere+1 e poi viene sottratto a questo stesso indirizzo l'offset della fine dell'indirizzo da sovrascrivere, e questo mi sembra elementare, poi vengono salvati il Code Segment e lo Stack Segment e viene quindi settato come Code Segment lo Stack Segment, ed infatti giocheremo proprio con lo stack, si salva poi lo Stack Pointer e viene mosso in quest'ultimo l'offset dell'indirizzo da sovrascrivere (che è EndPatch difatti parlavamo di crittazione dal basso), viene poi mosso nel Source Index (SI) l'offset della fine del FILE, in questo modo la fine del file sarà usata come maschera xor, è stata scelta la fine del file per camuffare il valore dello xor. Arrivati qui iniziamo la crittazione vera e propria, viene infatti prelevato con un pop da AL il byte da patchare, il byte viene poi xorato con il primo valore puntato da SI che come abbiamo detto conteneva l'offset della fine del file, successivamente viene riscritto in memoria (con un push) il byte xorato e vengono decrisati sia lo Stack Pointer che il Source Index, state capendo la dinamica del programma??? Noi normalmente avremmo incrisato sia l'uno che l'altro per far xorare il successivo byte con il successivo valore invece qui viene xorato il byte precedente con il precedente valore, si va all'indietro per capirci :). Viene poi confrontato lo Stack Index (SI) con l'offset Start cioè dell'inizio del file e se sono uguali significa che è stato xorato tutto, a questo punto si esce dal loop e si xora l'ultimo byte, dopodichè Stack Pointer e Stack Segment vengono rimessi dov'erano e quindi si può entrare nel loop di decrittazione della pass. Rileggete una paio di volte questo testo perchè non è poi troppo semplice. Sicuramente avrete notato quegli strani Identification Mark, bhè CCh significa "Int 3" e sono stati inseriti per far funzionare un eventuale patcher che dovrebbe cercare gli identification di inizio e fine programma, dovrebbe poi contare quanti byte ci sono tra tutti e due ed eseguire lo xor per decrittare il tutto. 
Ok adesso basta scopiazzare e vi presento un mio pupillo realizzato per funzionare a 32-bit:

 

.386
locals
jumps
.model flat,STDCALL

extrn ExitProcess:Proc
extrn MessageBoxA:Proc

MB_OK EQU           00000000h
MB_ICONHAND EQU     00000010h
MB_ICONQUESTION EQU 00000020h

.data
Msg       db 'Bravo sei proprio intelligente!!!',0
Cpt       db 'Complimenti che bel tipo che sei!!!',0
Message   db 'Ho incontrato tua madre ieri notte sul marciapiede',0
Caption   db 'Fai i complimenti a mamma, come fa le pompe lei....',0

.code

Start:

           call complimentati

chng1: push MB_OK OR MB_ICONQUESTION
           push offset Cpt
chng2: push offset Msg
           push 0
           Call MessageBoxA
           Call ExitProcess

   
;===========================================
complimentati proc
        mov edi, offset sovrascrivi1   
        mov eax, dword ptr[edi]
        mov dword ptr[chng1], eax
        mov edi, offset sovrascrivi2   
        mov eax, dword ptr[edi]
        mov dword ptr[chng2], eax
        ret
complimentati endp
;===========================================

sovrascrivi1:
    push MB_OK OR MB_ICONHAND
    push offset Caption

sovrascrivi2:
    push offset Message

End Start

 

Troverete sia il sorgente che il file compilato e funzionante nell'allegato, decompilate l'allegato e dategli uno sguardo, bhè non sembra nient'altro che un semplice programmino che ti fa tanti bei complimenti....Mmmm avvialo e dimmi se sei convinto...Bhè Non   più vero? Heheh come vedi offende tua madre, eppure il disassembler dice che dovrebbe dirti delle belle parole, come mai? Tutto ciò è molto semplice e per spiegarvelo nel migliore dei modi vi ripropongo la chiamata "Complimentati":

 

complimentati proc
        mov edi, offset sovrascrivi1   
        mov eax, dword ptr[edi]
        mov dword ptr[chng1], eax
        mov edi, offset sovrascrivi2   
        mov eax, dword ptr[edi]
        mov dword ptr[chng2], eax
        ret
complimentati endp

per prima cosa muoviamo in EDI l'offset della label "sovrascrivi1", quindi ora EDI punterà ai byte di quella label cioè: 

 

push MB_OK OR MB_ICONHAND
push offset Caption
nel successivo passaggio muoviamo i primi 4 byte puntati da EDI in EAX (mov eax, dword ptr[edi]) e riscriviamo questi 4 byte al posto dei primi 4 che si trovano dopo la label "chng1", visto dal debugger vediamo cambiare questi byte:

 

6A20        push MB_OK OR MB_ICONQUESTION
6822204000  push offset Cpt
 
in questi:
 
6A10                 push MB_OK OR MB_ICONHAND
6878204000       push offset Caption
ed il secondo passaggio della stessa chiamata fa la stessa cosa :)
Fate adesso una cosa, scaricate da questo sito il Tasm 5.0, aprite il sorgente del programma che si trova nell'allegato e secondo le istruzioni di compilazione.....Compilatelo :)) fatelo partire e.......Mha!!! Non funziona eh? Il compilatore non vi da errore eppure il programma termina in GPF giusto??? Hehehehe, dite la verità, non sapete che fare??? Bhè quando inizio l'era dei 32-bit qualcuno decise che le sezioni potevano essere Readable, Executable ma non Writeable, perchè? Bho chiedetelo a chi l'ha deciso, ad ogni modo come avete potuto constatre con i programmi a 16-bit non esistono di questi problemi, eh già perchè il codice a 16-bit può fare quello che cazzo vuole mentre a 32-bit ciò non succede, si ma se è così come mai il mio programma funziona??? Semplice, ho crackato il PE del file, ora vi starete sicuramente chiedendo come dal momento che senza saperlo non riuscireste mai a far girare neanche il più semplice prg con SMC!!! Bhè io non ve lo dico, anzi scopritelo da soli hahahahah....Vabbè scherzo non sono poi così sadico (Nooo??? :) OK scaricate il ProcDump da questo sito se non ce l'avete ed apritelo, clickate su "PE Editor" ed aprite il file che vi interessa, clickate sul pulsante "Sections" ed avrete davanti qualcosa come questo:
 
NAME  Virtual Size    Virtual Offs       Raw Size        Raw Offset       Characteristics    
CODE    00001000        00001000         00000200         00000600         60000020
DATA    00001000        00002000         00000200         00000800         C0000040
.idata  00001000        00003000         00000200         00000A00         C0000040
.reloc  00001000        00004000         00000200         00000C00         50000040

 

ok, come dicevamo non abbiamo sulle sezioni il permesso di scrittura e dato che il presupposto dell' SMC è proprio quello di sovrascrivere una sezione allora dovremmo intervenire sulle flag che ci danno i vari "permessi", come ci orientiamo? Semplice, sappiamo che la sezione che dobbiamo crackare è quella che contiene il codice scritto da noi, questa sezione di chiama CODE se l'eseguibile è stato compilato con compilatore Borland e .text se è stato usato un altro compilatore, quindi come vediamo la sezione che ci interessa crackare è la prima, a questo punto dobbiamo identificare il campo delle flag che ProcDump identifica come: Characteristics, quel numero: 60000020 significa qualcosa, più precisamente vuol dire che quella sezione è Readable, Executable e contiene codice, come faccio a saperlo? Semplice quei numeri significano qualcosa e quel "qualcosa" è scritto nella tabella in basso.
 

Caratteristica

Valore

IMAGE_SCN_CNT_CODE

EQU 000000020h

IMAGE_SCN_CNT_INITIALIZED_DATA

EQU 000000040h

IMAGE_SCN_CNT_UNINITIALIZED_DATA

EQU 000000080h

IMAGE_SCN_MEM_DISCARDABLE

EQU 002000000h

IMAGE_SCN_MEM_SHARED

EQU 010000000h

IMAGE_SCN_MEM_EXECUTE

EQU 020000000h

IMAGE_SCN_MEM_READ

EQU 040000000h

IMAGE_SCN_MEM_WRITE

EQU 080000000h

 

Come vedete abbiamo dei valori esadecimali e sono proprio questi che opportunamente combinati danno le caratteristiche delle varie sezioni, ma quello che ci interessa è aggiungere il flag di readable alla sezione CODE, come si fa? Con una semplice addizione:

 

60000020+800000000 = E0000020

 

con questo cambiamento  la nostra sezione è diventata: Readable+Executable+Writeable ed è quindi pronta per funzionare, in procdump clickare sulla sezione prima col sinistro e poi col destro, clickare sulla voce "Edit section" del menu di popup e nel campo "Section Characteristics" sostituite 60000020 con E0000020, ed il gioco è fatto, ma spieghiamo cosa significano tutti quei valori, tanto un po' di "sapere" in più non vi fa certo male:

IMAGE_SCN_CNT_CODE: Questa sezione contiene codice, in genere questa opzione è settata insieme al flag "executable"

 

IMAGE_SCN_CNT_INITIALIZED_DATA: Questa sezione contiene dati inizializzati, in genere tutte le sezioni tranne quella eseguibile e la .bss hanno questa flag settata

 

IMAGE_SCN_CNT_UNINITIALIZED_DATA: Questa sezione contiene dati non inizializzati come ad esempio la sezione .bss

 

IMAGE_SCN_MEM_DISCARDABLE: Questa sezione può essere scartata dal momento che non è necessaria al processo una volta che è stato caricato, in genere la sezione .reloc ha questa flag settata

 

IMAGE_SCN_MEM_SHARED: Questa sezione è divisibile, cioè quando è usata con una DLL tutti i dati in questa sezione sono divisi tra tutti i processi usati dalla DLL, in pratica una sezione divisibile dice al gestore della memoria di settare un page mapping per la sezione dimodo che tutti i processi che utilizzino la DLL usino anche la stessa pagine fisica di memoria 

 

IMAGE_SCN_MEM_EXECUTE: Questa sezione è eseguibile, questa flag in genere è settata insieme alla flag "Code" (000000020)

 

IMAGE_SCN_MEM_READ: Questa sezione è leggibile

 

IMAGE_SCN_MEM_WRITE: Questa sezione è scrivibile, in genere questa flag è settata solo sulle sezioni .data e .bss, ma noi la usiamo anche su altre :))

Ok i flag sono molti altri ma i principali sono questi, dopo tutto questo testo dovreste aver capito come funzionano i flag, se no ecco il solito breve esempio cioè cerchiamo di capire quali flag sono stati settati per la sezione DATA:

C0000040 = 80000000+40000000+000000040 cioè:  

IMAGE_SCN_MEM_WRITE+IMAGE_SCN_MEM_READ+IMAGE_SCN_CNT_INITIALIZED_DATA semplice no??? In fondo sono solo addizioni, fate quindi quel cambiamento al vostro file e vedrete che andrà bene :)))

Thnx to: +Mammon_ per il suo grande tutorial, Little-John per la sua traduzione e Neural_Noise per avermi aiutato all'inizio a crackare il PE :)))

Fuck to: Tripod che mi ha cancellato tutti e tre gli account!!!


OBIETTIVI:  
  1. Trovate il codice corrispondente al vostro nick

  2. Crackate il programma in modo da ottenere codici sempre validi e spiegatene MOLTO dettagliatamente in metodo utilizzato

  3. Spiegate come meglio potete l'algoritmo di generazione del serial

  4. **facoltativo** Scrivete un key-maker per il programma

Potete usare i tool che più vi garbano :)))

Quequero


[Back]