- 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!!!
|