In questo tutorial vedremo come crackare l'ultima
versione del safedisc (ovvero la 2), come target useremo Soul Reaver
2
Crackare Soul Reaver
2
Manual Unpacking Written by
Quake2
Introduzione
Soul Reaver 2... e chi non conosce le gesta del
divino Raziel? La sua continua ricerca della verità su chi sia, cosa sia e a
cosa sia destinato? Se non conoscete la saga di The Legacy Of Kain è bene che
iniziate a procurarvi il primo Blood Omen (e capirete anche il significato
della frase "Kain refused to sacrifice" :)), e metterete le vostre mani su una
delle storie più belle mai scritte, ve lo assicuro :)
Soul Reaver 2 usa l'ultima versione disponibile
per safedisc, ovvero safedisc 2, che rispetto alle altre è stata abbastanza
"imbastardita". Per crackare soul reaver 2, useremo un metodo un po' anomalo
ma che funziona alla perfezione (graaaaaaaaaazie Yado :)), prima di tutto per
decrittare il codice e i dati useremo il solito metodo del dump, mentre per la
IT, scriveremo un programmino che beh... lo vedrete in seguito
:)
Essay
Bene, iniziamo, allora per prima cosa dobbiamo
individuare la protezione, cioè noi gia lo sappiamo, ma facciamo finta di non
saperlo, quindi apriamo la directory del gioco e... sorpresa! niente file
.icd, niente clcd32.dll, niente dplayerx.dll, niente di niente! ma che
protezione è? Ok, non lasciamoci scoraggiare, il precedente Soul Reaver era
protetto sempre con safedisc, quindi diamo un'occhiata al file sr2.exe,
apriamolo con hiew, e noteremo subito una cosa strana, ovvero la presenza di
due sezioni molto sospette, una chiamata stxt774 e una stxt371, diamo
un'occhiata all'entry point... punta dentro stxt371, quindi sappiamo che qui
risiede il loader, ma ancora non abbiamo individuato la protezione, non ha
niente di classico di safedisc, ma non lasciamoci scoraggiare, quindi
chiudiamo hiew, e carichiamo filemon, e attiviamo il logging, facciamo partire
soul reaver 2, giocate un po' se volete, uscite e poi date un'occhiata al log
prodotto, tombola! tra i vari nomi c'è pure clcd32.dll e clcd16.dll, è
safedisc!!! ma voi direte, ste dll do cacchio stanno? Semplice, allora il
safedisc quello che fa quando parte è estrarre quelle due dll in
C:\windows\temp\cartella\, dove cartella prende un nome che può variare, i
dati di queste due dll li prende dal file exe, dato che sono appesi alla fine
(attenzione, non fanno parte del file, ma sono solo appesi, il file quando
viene mappato in memoria non comprende i dati delle dll), e sono crittati, poi
estrae altre due dll, di cui una è l'ex-dplayerx, l'altra non lo so e
sinceramente non me ne frega niente :) (ovviamente anche queste ultime due dll
sono crittate). Ok, sappiamo che è safedisc, sappiamo che è la versione 2,
insomma, sappiamo abbastanza per partire :). Allora, prima di tutto
dobbiamo trovare l'oep del programma, per far ciò, faremo un po' di backtrace,
quindi mettete un bel bpx su GetVersion, fate partire SR2, e premete F5 finche
il softice smette di apparire e compare la schermata col logo della eidos,
dopo questa schermata riapparirà il softice, premete F12, e vi trovere alla
prima call del programma, qualche riga più sopra, ci sarà l'entry point (push
ebp) e sarà al VA 004D810E, quindi mettiamo un bel break, e togliamo pure il
break su GetVersion, fate partire il gioco, uscite e fatelo ripartire, quando
il softice brekka, fate a eip e poi jmp eip, così il processo entra in loop,
ora lanciate ProcDump, selezionate il task sr2.exe, e dumpatelo (Full dump), e
chiamatelo (con molta fantasia) Crack.exe, killate il processo (oppure
riassemblate il push ebp, insomma basta che uscite da soul reaver 2 :)), e la
prima parte è fatta :). Ora abbiamo il codice bello in chiaro, iniziamo a dare
un'occhiata al file dumpato, per prima cosa controllate la dimensione, è quasi
1mb più piccolo, questo perché nel dump ovviamente non sono presenti le dll
del safedisc, ora diamo un'occhiata all'IAT (inizio della sezione .rdata),
come vedrete conterrà qualcosa del genere:
Dunque, gli indirizzi tipo 01ECxxxx o 01EBxxxx
puntano a codice del safedisc che spiegherò più tardi, mentre come potete
vedere per le prime entry, c'è un'entry che punta a BFE81644, che guarda caso
è una funzione di advapi32, quindi questo ci suggerisce che non tutte le api
sono risolte, ma solo alcune, la scelta di quali api risolvere è fatta al
momento del wrapping, quindi è del tutto arbitraria, ma da quello che sono
riuscito a capire, serve ALMENO un'api per dll importata (perché il safedisc
per prendere gli entry point dell'api non usa GetProcAddress come nelle
versioni precedenti, ma si scanna la export table, e per sapere il base
address della dll, tramite una sua IT carica le dll necessarie al gioco e poi
prende il loro base address, che metterà in una tabella che verrà usata in
seguito per trovare l'entry point dell'api), comunque questi sono dettagli per
il momento, ora sappiamo dov'è la IAT, dobbiamo trovare la IT originale
(quella che c'è adesso è quella usata dal safedisc, e a noi non ci serve :)),
e in questo caso il metodo è del tutto "casuale" ovvero a partire dalla IAT
scorrete il file alla ricerca di una zona che possa sembrare una IT, e questa
zona si trova all'RVA 000E5840, ed eccola qui:
dove OriginalFirstThunk è
un puntatore ad una tabella (terminata con una dword vuota) che contiene dei
puntatori ai nomi delle funzioni importate, o nel caso che la funzione venga
importata con l'ordinal, contengono un'array di ordinal, TimeDateStamp e
ForwarderChain non ci interessano, NameRVA punta al nome della dll, e
FirstThunk punta ad un'array di dword che a runtime verrano sostuituite con
gli entry point delle funzioni importate da quella dll. Quindi, come possiamo
vedere, abbiamo 5 entry senza OriginalFirstThunk, e queste sono le entry che
dovremo sistemare, ma per prima cosa vediamo a cosa puntano, per vederlo,
basta vedere a che nome punta il campo a +0Ch dell'entry, ovvero NameRVA,
quindi abbiamo in ordine: Kernel32.dll, User32.dll, Gdi32.dll, Advapi32.dll e
ole32.dll, ricordatevi questo ordine che in seguito ci servirà :). Ok adesso
che sappiamo dov'è l'import table, apriamo ProcDump e usiamo il suo pe editor
per sistemare l'import table e l'entry point e salviamo il file. Ok, siamo
quasi al 10% del lavoro :). Adesso a questo punto, non ci rimane che fixare la
IT e abbiamo finito, semplice no? Se magari :) Adesso prima di continuare,
vorrei aprire una piccola parentesi su come safedisc risolve le api,
principalmente ci sono due casi, nel primo caso, è quando abbiamo o un call
dword ptr [xxx], o un jmp dword ptr [xxx], o un call edi, o call esi o call
ebx o call ebp, in questi casi il return value viene preso dallo stack (perché
come tutti sapete quando viene chiamata una funzione il return valure viene
pushato nello stack), mentre nel secondo caso, abbiamo una cosa del
genere:
Questo pezzo di codice, non fa
altro che generare un'indirizzo che corrisponde ad una locazione di memoria
allocata dal safedisc, in cui è presente del codice che pusha nello stack il
return address vero, e poi chiama la routine di risoluzione, ora analizzeremo
nel dettaglio i vari casi, quindi assicuratevi che il bpx sull'entry point sia
ancora attivo, e fate partire SR2, steppate finche non arrivate alla prima
call, e entrateci con F8, ed arriverete a del codice che fa da ponte, è da
notare che c'è una routine ponte per ogni api chiamata, nel caso di
GetVersion, la routine di ponte sarà questa:
push BFEA1236 pushfd pushad push
esp push 01EB3EFD call 10043C90 add esp, 08 push 00 pop
eax popad popfd ret
Allora,
il push 01EB3EFD pusha nello stack l'indirizzo di una tabella fatta da due
DWORD, di cui la prima identifica la dll, mentre la seconda identifica la
funzione da chiamare, ad esempio, se la dll è Kernel32, la prima dword sarà 0,
1 per user32, 2 per gdi32 e così via, mentre la numerazione per le funzioni è
un po' diversa, ad esempio, mettiamo che l'api 3 (contando a partire da 1) non
viene risolta dal fixer (ma c'è direttamente l'indirizzo nella IAT), quindi la
numerazione sarà 0, 1, 3, 4, 5, n, come vedete, non viene contata l'api gia
risolta. Mentre la call 10043C90 chiama la funzione di risoluzione vera e
propria, ora siccome il codice di risoluzione è un po' lunghetto, riporterò
solo il pezzo di codice iniziale, mentre per le funzioni chiamate, di quelle
meno importanti darò solo una descrizione, di quelle più importanti (in questo
caso solo la funzione che prende l'entry point dell'api) riporterò i pezzi di
codice più importanti, allora dopo la call ci troveremo
qua:
10043CD8: push
eax 10043CD9: mov eax, ebp 10043CDB: add eax, 38 10043CE6: mov eax,
[eax] ; in eax abbiamo il return
address 10043CE8: sub eax, 06 ; ora invece in
eax abbiamo il punto in cui è stata chiamata la funzione (si sottrae 6, perché
la dimensione di una call dword ptr [xxx] è di 6 byte, stesso discorso per jmp
dword ptr [xxx], mentre per le call edi, esi, ecc..., non ci ritroveremo al
punto di chiamata) 10043CEB: mov [ebp-2C], eax 10043CEE: pop
eax 10043D01: mov ecx, [ebp+08] ; in ebp+08 abbiamo la
tabella dll:funzione 10043D04: mov edx, [ecx] ;
in edx adesso abbiamo il numero della dll (0 in questo
caso) 10043D06: mov [ebp-18], edx 10043D09: mov eax,
[ebp+08] 10043D0C: mov ecx, [eax+04] 10043D0F: mov [ebp-14],
ecx 10043D12: cmp [ebp-18], -01 ; controlla che il
numero della funzione non sia -1, se lo è, allora vuol dire che la funzione è
stata chiamata con quel jmp 00762xxx, quindi prende il numero di dll e di
funzione in un'altro modo che descriverò in seguito 10043D16: jnz
10043DCF
ora seguendo quel jmp vi
ritroverete in un punto di codice in cui inizia la risoluzione, se non viene
eseguito, appunto come dicevo prima, verrà calcolato il numero della dll e
della funzione in un'altro modo, non riporto il codice perché tanto per
risolvere i jmp 00762xxx basta mettere l'eip all'RVA dove sono presenti e
farglielo eseguire, tanto il return address lo pusha lui, comunque questo lo
vedremo in seguito, ora concentriamoci sulla risoluzione della funzione,
quindi stavamo a 10043DCF:
10043DCF: mov eax, [ebp-18] 10043DD2: imul eax, eax, 8D ; moltiplica il numero della dll per 8d, 8d è una specie di
numeretto magico che serve per calcolare l'indirizzo di alcuni valori
fondamentali e alcune tabelle 10043DD8: mov ecx, [10065218] ; se dal softice fate d 10065218, vedrete l'indirizzo 01FDF00C,
questo è un'indirizzo molto importante, perché è usato come base address
per calcolare l'indirizzo di praticamente tutte le tabelle e i valori del
safedisc 10043DDE: mov edx, [eax+ecx+C3] ; in
eax abbiamo l'indirizzo di una tabella (una per ogni dll), di cui tra un po'
di codice ne spiegherò il significato 10043DE5: mov [ebp-1C],
edx 10043E01: mov eax, [ebp-2C] ; in ebp-2c come potete
vedere qualche riga più sopra abbiamo il caller address (solo nel caso di
jmp dword ptr e call dword ptr ) 10043E04: mov [ebp-04], eax 10043E2C: mov ecx,
[ebp-2C] 10043E2F: push ecx 10043E30: mov edx, [ebp-14] 10043E33:
push edx 10043E34: mov eax, [ebp-1C] ; riecco la
tabella di prima 10043E3A: push eax 10043E38: call 10041F30 ; questa call basandosi sulla tabella di prima, controlla se
quella funzione è stata gia risolta, in questo caso salta tutto il resto, così
non perde tempo a risolvere 2 volte la stessa api 10043E3D: add esp,
0C 10043E40: mov [ebp-0C], eax 10043E43: cmp dword ptr [ebp-0C],
00 10043E47: jz 10043FC8 ; se non è stato gia risolto,
allora procedi alla risoluzione
10043FC8: un po' di morfismo
inutile che ci porta a 10043FD4 10043FD4: mov eax, [ebp-14] 10043FD7:
mov [ebp-20], eax 10043FF3: lea ecx, [ebp-28] 10043FF6: push
ecx 10043FF7: lea edx, [ebp-24] 10043FFA: push edx 10043FFB: lea eax,
[ebp-08] 10043FFE: push eax 10043FFF: mov ecx, [ebp-2C] 10044002:
push ecx 10044003: call 10044880 ; questa call non fa
altro che controllare che il caller address sia dentro la sezione .text o
.stxt 10044008: add esp, 10 1004400B: and eax,
0000FFFF 10044010: cmp eax, 01 10044013: jnz 10044149 10044029: mov
edx, [ebp-08] 1004402C: add edx, [ebp-28] ; ora in eax
abbiamo il VA della sezione .text, ovvero 00401000 1004402F; mov
eax, [ebp-2C] ; in eax abbiamo il caller
address 10044032: sub eax, edx ; ora in eax
abbiamo caller address - 00401000 10044034: mov [ebp-10],
eax 10044050: mov ecx, [ebp-10] 10044053: push ecx 10044054: call
10044CE0 ; questa funzione fa i soliti calcoli
fisico-nucleari sul caller address - 00401000 e poi divide il risultato per 4,
se il resto è maggiore di 2 torna 1, altrimenti 0, se viene ritornato 1,
allora dal numero della funzione ricava un'altro numero, altrimenti usa
quello, poi spiegherò per cosa viene usato 10044059: add esp,
04 1004405C: and eax, 0000FFFF 10044061: cmp eax, 01 10044064: jnz
10044149 1004408E: mov edx, [ebp-14] 10044091: imul edx, edx,
34B 10044097: mov eax, [ebp-04] ; caller
address 1004409A: mov ecx, [ebp-1C] ; ecco di
nuovo la tabella 1004409D: mov eax, [eax+02] ;
prende l'indirizzo dell'entry nella IAT (siccome il call dword ptr occupa 2
byte, a eax+2 abbiamo l'indirizzo) 100440A0: cmp eax, [edx+ecx+332]
; controlla l'indirizzo nella IAT con quello memorizzato
nella tabella per questa funzione 100440A7: jnz
10044149 100440AD: mov ecx, [ebp-04] 100440B0: xor edx, edx 100440B2:
mov dl, [ecx] 100440B4: cmp edx, FF 100440BA: jnz 10044149 100440C0:
mov eax, [ebp-04] 100440C3: xor ecx, ecx 100440C5: mov cl,
[eax+1] 100440C8: cmp ecx, 15 ; a questo punto
controlla che l'opcode sia FF (controllato prima) 15, ovvero call dword
ptr 100440CB: jnz 10044149 100440DD: mov edx,
[ebp-14] 100440E0: mov [ebp-20], edx 100440E3: mov eax, [10065218] ; riecco il base address :) 100440E8: mov ecx,
[eax+28] ; ora in ecx abbiamo un'altro valore magico,
ovvero 9EE3E155 100440EB: add ecx, [ebp-10] ;
aggiunge al valore magico il caller address - 00401000 100440EE:
push ecx 100440EF: mov edx, [ebp+20] 100440F2: push edx 100440F3: mov
eax, [ebp-18] 100440F6: imul eax, eax, 8D 100440FC: mov ecx,
[10065218] 10044102: mov edx, [eax+ecx+58] ; ora in edx
abbiamo un'altro valore che rimane costante, per kernel32 è 54, che,
casualmente :), rappresenta il numero delle funzioni importate, se volete
sapere quante funzioni ci sono per ogni api, basta che moltiplicate il numero
della dll per 8D, e poi in softice fate d risultato+ecx+58, così avrete il
numero di funzioni 10044106: push edx 10044107: call 100445E0
; questa funzione a partire dai parametri passati,
attraverso 2 divisioni calcola un valore che viene ritornato in
eax 1004410C: add esp, 0C 1004410F: mov [ebp-20],
eax 1004411F; mov eax, [ebp-20] 10044122: shr eax, 03 10044125: mov
ecx, [ebp-18] 10044128: mov edx, [10065214] ; ecco
un'altro base address, ma questo è meno importante del precedente, perché è
usato solo qui 1004412E: mov ecx, [ecx*4+edx] 10044131: xor edx,
edx 10044133: mov dl, [eax+ecx] 10044136: mov ecx, [ebp-20] 10044130:
and ecx, 07 1004413C: mov eax, 01 10044141: mov eax, cl 10044143: and
edx, eax 10044145: test edx, edx 10044147: jz 100440E3 ; ripete questo ciclo finche il risultato è diverso da 0, in
questo caso significa che abbiamo trovato il vero numero dell'api da chiamare,
per GetVersion, da 12 diventa 30 1004415D: mov ecx,
[ebp-18] 10044160: imul ecx, ecx, 8D 10044166: mov edx,
[10065218] 1004416C: mov eax, [ecx+edx+4C] ; ora in eax
abbiamo il base address di una tabella di numeri, dove il valore ottenuto dal
codice precedente verrà utilizzato come indice in questa tabella, in questo
modo abbiamo un valore che poi verrà usato come indice nella tabella di nomi
delle funzioni e della lunghezza, ma questo lo spiegherò più
avanti 10044170: mov ecx, [ebp-20] 10044173: mov edx,
[ecx*4+eax] 10044176: mov [ebp-20], edx ; ora in ebp-20
abbiamo l'indice di cui parlavo prima 10044185: mov eax,
[ebp-20] 10044188: imul eax, eax, 8D 1004418E: mov ecx,
[ebp-12] 10044191: mov edx, [eax+ecx+2FA] 10044198: mov [ebp-0C],
eax 1004419B: cmp dword ptr [ebp-0C], 00 1004419E: jnz
100441CA 100441A1: mov eax, [ebp-20] ; l'indice trovato
in precedenza 100441A4: push eax 100441A5: mov ecx,
[ebp-18] 100441A8: push ecx 100441A9: call 10043630 ; chiama la funzione che decritta il nome e ritorna l'entry
point della funzione, di questa funzione ripoterò gran parte del
codice 10044265: mov esp, [ebp+0C] ; ho saltato
tutto il codice che viene dopo la call, perché sostanzialmente quello che fa è
questo: inserisce nella tabella che dicevamo prima, la funzione chiamata con
relativo caller address (quindi avremo una cosa del genere: caller
address:api_entry_point) e recritta il nome 10044268:
popad 10044269: popfd 1004426A: ret ; quando verrà eseguito il ret, vi ritroverete
all'entry point dell'api chiamata
Ok, il codice tolta la
robaccia di morfismo è abbastanza chiaro, comunque quello che fa
sostanzialmente è questo: 1) Controlla che il call provenga dalla sezione
.text o .stxt 2) Controlla che la funzione non sia stata gia risolta, se è
stata gia risolta allora non risolve 3) Se non è stata risolta, procede
alla risoluzione 4) Prende il numero dell'api, se non è una call dword ptr,
allora si basa su quello, altrimenti procede a ricavare un'altro numero 5)
Il numero dell'api o quello ricavato dal numero dell'api, è usato come indice
in una tabella, il valore preso da questa tabella è poi usato per risolvere
l'api 6) Una volta risolto tutto, recritta il nome, aggiunge l'api risolta
alla tabella usata per controllare che l'api non sia stata gia risolta 7)
Attraverso un ret salta all'entry point dell'api
Ok, ora sappiamo come
funziona safedisc, ma con il metodo che ho deciso di usare, tutta sta roba non
è necessaria, ho voluto descrivere il funzionamento del safedisc solo per
chiarire meglio le cose, e poi è sempre meglio saperlo :). La cosa che ci
interessa a noi è la funzione di decrypt del nome che andremo ad analizzare
adesso:
10043639: mov dword ptr
[ebp-0C], 00 100436A4: mov eax, [10065218] 100436A9: mov ecx,
[ebp+08] 100436AC: cmp ecx, [eax+0F] ; controlla che il
numero della dll non sia superiore del numero massimo di dll, ovvero
5 100436AF: jbe 10043B2D 100436B5: mov edx, [ebp+08] 100436B8:
imul edx, edx, 8D 100436BE: mov eax, [10065218] 100436C3: mov ecx,
[ebp+0C] ; ecx contiene il numero della funzione, 50h per
GetVersion 100436C6: cmp ecx, [edx+eax+58] ;
controlla che non sia superiore al numero massimo di funzioni importate dalla
dll, nel caso di kernel32 sono 54h (84) 100436CA: jae
10043B2D 100436E8: mov edx, [ebp+08] 100436EB: imul edx, edx,
8D 100436F1: mov eax, [10065218] 100436F6: mov ecx, [edx+eax+B5] ; mette in ecx il base address di una tabella di puntatori ai
nomi delle funzioni 100436FD: mov edx, [ebp+0C] 10043700: mov
eax, [edx*4+ecx] ; il numero dell'api viene appunto usato
come indice all'interno della tabella, e in eax abbiamo il puntatore al nome
(crittato della funzione) 10043703: mov [ebp-0C], eax 10043706:
mov ecx, [ebp+08] 10043709: imul ecx, ecx, 8D 1004370F: mov edx,
[10065218] 10043715: mov eax, [ecx+edx+BF] ; in eax
abbiamo il base address di un'altra tabella, stavolta contiene un'array di
puntatori ad una dword, che contiene la lunghezza della
funzione 1004371C: mov ecx, [ebp+0C] 1004371F: mov edx,
[ecx*4+eax] ; adesso in edx abbiamo il puntatore alla
lunghezza 10043722: mov eax, [edx] ; in eax
abbiamo la lunghezza (per GetVersion sarà 0B) 10043724: mov
[ebp-18], eax 10043741: mov ecx, [10065218] 10043747: add ecx, 26 ; ecx punta ad un'array di 4 dword (9EE3E155, 76AD7FF7,
2A2A7F59, DACE2F29) che sono usate per il decrypt del nome (solo per la prima
funzione di decrypt, poi lo spiegherò meglio) 1004374A: push
ecx 1004374B: mov edx, [ebp-18] ; in edx abbiamo il
puntatore alla lunghezza del nome 1004374E: push edx ; e lo pushiamo :) 1004374F: mov eax, [ebp-0C] ; in eax abbiamo il puntatore al nome 10043752: push
eax ; e pushiamo pure quello :) 10043753: call 10029CD0 ; chiama la funzione di decrypt, che sostanzialmente è divisa in
3 parti fondamentali, che poi spiegherò in dettaglio, adesso vediamo il codice
della funzione di decrypt:
10029DCA: mov eax,
[ebp+0C] 10029DDD: shr eax, 03 ; divide eax per
8 10029DD0: mov [ebp-10], eax 10029DD3: cmp dword ptr [ebp-10],
00 10029DD7: jnz 10029EA4 ; se il risultato è 0, allora
la lunghezza del nome è < di 8 caratteri, e chiama un'altra funzione di
decrypt 10029ECC: mov eax, [ebp+0C] 10029ECF: xor edx,
edx 10029ED1: mov ecx, 8 10029ED6: div ecx 10029ED8: mov [ebp-0C],
edx ; salva il resto della divisione, che verrà usato in
seguito 10029EE8: cmp dword ptr [ebp-0C], 00 ;
controlla se il resto è 0 (ovvero il nome è lungo esattamente 8
caratteri 10029EEC: jz 10029FA9 ; se si salta la
prima parte di decrypt 10029F27: mov edx, [ebp+0C] 10029F29: mov
eax, [ebp+08] 10029F2D: lea ecx, [edx+eax-08] ; ecx
punta alla posizione len-8 del nome (dove len è la
lunghezza) 10029F31: mov [ebp-14], ecx 10029F58: mov edx,
[ebp+10] ; mette in edx un puntatore alla tabella di 4
dword 10029F5B: push edx 10029F5C: mov eax, [ebp-14] ; in eax abbiamo il puntatore a len-8 10029F5F: push
eax 10029F60: call Decrypt ; chiama la prima funzione
di decrypt di cui non riporto il codice perché è troppo lungo, comunque è una
serie di xor con i valori della tabella di 4 dword di
prima 10029F65: add esp, 08 10029FA9: mov ecx,
[ebp+08] 10029FAC: mov [ebp-14], ecx 10029FF0: mov dword ptr [ebp-04],
1437A1E9 ; primo valore di xor 1002A01F: mov
dword ptr [ebp-08], 00 1002A026: jmp 1002A031 1002A028: mov edx,
[ebp-08] 1002A02B: add edx, 01 1002A02E: mov [ebp-08], edx 1002A031:
mov eax, [ebp-08] 1002A034: cmp eax, [ebp-10] 1002A037: jae
10024196 1002A03B: jbe 1002A04A 1002A04A: mov ecx, [ebp-14] 1002A04D:
mov edx, [ecx] 1002A04F: xor edx, [ebp-04] 1002A052: mov eax,
[ebp-14] 1002A055: mov [eax], edx 1002A07C: mov ecx,
[ebp-04] 1002A07F: imul ecx, ecx, E32A79DC ; al primo
valore di xor moltiplica un secondo valore 1002A085: add ecx,
274B0EFA ; e poi ne aggiunge un terzo 1002A08B:
mov [ebp-04], ecx 1002A0A2: mov edx, [ebp-14] 1002A0A5: mov eax,
[edx+04] 1002A0A8: xor eax, [ebp-04] 1002A0AB: mov ecx,
[ebp-14] 1002A0AE: mov [ecx+04], eax 1002A0BE: mov edx,
[ebp-04] 1002A0C1: imul edx, edx, E32A79DC 1002A0C7: add edx,
292881ED 1002A0CD: mov [ebp-04], edx 1002A120: mov eax,
[ebp+10] 1002A123: push eax 1002A124: mov ecx, [ebp-14] 1002A127:
push ecx 1002A128: call Decrypt 1002A12D: add esp, 08 1002A154: mov
edx, [ebp-14] 1002A157: add edx, 08 1002A15A: mov [ebp-14],
edx 1002A15D: jmp 10027A28
Allora
quello che fa questa funzione, è praticamente decrittare a 8 a 8 il nome, se
il nome è minore di 8 caratteri allora viene usata un'altra funzione molto
semplice, altrimenti viene usata questa appunto, che chiama a sua volta
un'altra funzione che non fa altro che decrittare il nome, come potete vedere
il decrypt è cambiato molto dalle precedenti versioni, ma questo non è un
problema, noi sappiamo come agisce, quindi adesso ci basta scriverci un
decrypter che farà esattamente questo, quindi, riassumiamo brevemente cosa
dovrà fare il nostro decrypter:
1) Prendere il nome crittato 2)
Controllare se la lunghezza è minore di 8 3) se si, chiamare una routine
particolare che eseguirà il decrypt tutto in un colpo 4) se no
continuare 5) restituire il nome decrittato 6) passare al prossimo
nome
Ok, adesso ci resta da analizzare la routine di decrittazione nel
caso che il nome sia inferiore agli 8 caratteri, questa routine è
semplicissima, riporto direttamente il codice che ho convertito in
C:
void SimpleDecrypt(char
*Name, int len) { int mv =
0x56; for(int i = 0; i < len;
i++) {
Name[i] ^= mv; mv *=
0x32; mv +=
0x34; mv &=
0x000000FF; } }
Come vedete è talmente semplice che è quasi redicola :), quello
che fa è xorare il primo byte con un numero fisso, ovvero 56h, e
successivamente xorare gli altri con il risultato ottenuto dalla
moltiplicazione la somma e l'and.
Ora rimane un'ultima cosa, dove
stanno i nomi da decrittare? Allora diamo un'occhiata al campo NameRVA
dell'entry per kernel32 nella import table, punta ad una zona di memoria, poco
più sopra ci sono i byte da decrittare, attenzione, le funzioni di kernel32
stanno in due zone, una all'inizio e una alla fine, comunque ecco la lista
delle zone di memoria, con relativo indirizzo e lunghezza:
Ok, adesso sappiamo dove stanno le funzioni, però
attenzione, il modo in cui sono scritte è un po' particolare, vi faccio
un'esempio, aprite ultraedit, andate su Search, Find, e mettete i byte
crittati di GetVersion (li ho presi io per voi :)), che sono: C1 58 86 71 BA
42 9F F9 69 D2 E3, e fate Find Next, ovviamente ve li trova, e stanno
all'indirizzo E64EE, però date un'occhiata 2 byte prima, troverete 0B 7A, ora
0B è la lunghezza e lo sappiamo, ma quel 7A non centra assolutamente niente
col nome, quindi nel codice del decrypter, dovremmo anche tener conto di
questo, ovvero si legge la lunghezza, si salta un byte, si legge il nome e si
decritta, e così via, sperò di essere stato chiaro :). Ora sorge un'altro
problema, io me ne sono accorto durante l'esecuzione del decrypter, ovvero
dopo aver fatto l'ultima api, la lunghezza che viene letta non è esatta (per
kernel32 viene letto 4B) e quindi si sballa il processo di decrypt, questo è
dovuto al fatto, che per ogni serie di nomi di funzioni, c'è un byte
terminatore dopo l'ultima funzione, per far capire al safedisc che deve
smettere di prendere le funzioni, per kernel32 questo byte è 4B, mentre Gdi32
non è presente (per GDI32 ci baseremo sull'inizio del nome della DLL), ecco i
vari terminatori:
Ok, questo è il programma, lo
so che il codice fa schifo, ma l'ho copiato direttamente dal safedisc, non
avevo voglia di stare a riscriverlo in C, comunque dovrebbe essere chiaro
quello che fa, l'unica cosa che dovete fare è far partire il programma, quando
ha finito cambiate gli indirizzi delle due fseek, con quelli di User32 e
mettete il carattere terminatore nell'if (if nlen == 0x4B), e fate ripartire,
fate così per tutte le api, alla fine otterrete il file crack2.exe con i nomi
decrittati (attenzione, il file crack2.exe deve gia esistere all'interno della
cartella), ok adesso dobbiamo azzerare un po' di byte del file, che erano
quelli dopo il carattere terminatore, quindi aprite il file crack2.exe con
hiew, e azzerate tutti i byte dopo l'ultimo nome di ciascuna funzione della
dll importata (azzerate fino al nome della dll), per Kernel32_1 iniziate ad
azzerare da E5E66, per Kernel32_2 da E66BE, per User32 da E615F, per Gdi32 non
dovete azzerare niente, per Advapi32 da E620C e infine per Ole32 da
E624F. Ok, adesso finalmente abbiamo un file con tutti i nomi decrittati,
manca solo una cosa, ovvero ricostruire l'array di OriginalFirstThunk, però
noi non ricostruiremo quello, ma invece ricostruiremo l'array di FirstThunk
(il pe loader risolve sia dall'OriginalFirstThunk che dal FirstThunk) per far
ciò faremo una cosa un po' particolare, ovvero, attraverso un programma che
inietteremo direttamente nel processo, ci loggeremo il caller address, la
funzione chiamata, e in che indirizzo della iat c'è quella funzione, poi con
un programma in C, ci creeremo due tabelle, una per User32 e una per Kernel32,
che conterranno informazioni sulle api chiamate e da dove sono chiamate, poi
ricostruiremo la iat in modo che ogni api chiamata punti ad un solo indirizzo
nella iat, e non a due (può capitare perché il safedisc può risolvere più di
un'api con lo stesso indirizzo), fatto ciò, useremo il PEditor per ricostruire
l'array di FirstThunk, che conterrà dei puntatori al nome della funzione, in
modo che il pe loader di windows ci risolva correttamente le funzioni, infine
aggiungeremo a mano le funzioni che non sono state loggate (saranno pochissime
non preoccupatevi). Vediamo cosà dovrà fare il nostro logger:
1)
Scannare la sezione .text alla ricerca degli opcode di call dword ptr, jmp
dword ptr, call edi, call esi, call ebx e call ebp 2) Se trova l'opcode di
mov esi, dword ptr o mov edi, dword ptr o mov ebx, dword ptr o ebp, dword ptr,
scannare il codice in avanti alla ricerca dei relativi call (call esi, call
edi, call ebx, call ebp)
3) Prendere il return address (che si ottiene
sommando 6 nel caso di call dword e jmp dword, e sommando 2 nel caso di call
edi, call esi, ecc..), e pusharlo nello stack (in modo che il safedisc pensi
che la funzione è veramente chiamata da li)
4) Controllare che l'api
non sia stata gia risolta (quindi dentro c'è ad esempio BFF8xxxx per qualche
funzione di Kernel32)
5) Se è stata risolta allora aggiungerla alla
tabella e proseguire
6) Se non è stata risolta, allora saltare
direttamente al codice di risoluzione della funzione (preso dal call dword o
jmp dword o call esi, ecc...), tramite in jmp dword ptr [ebx], in modo che lo
stack rimanga inalterato
7) Sostituire il ret finale della funzione di
risoluzione con un jmp alla funzione di logging che abbiamo fatto
noi
8) La funzione di logging riempie la tabella (mettendo caller
address, api chiamata, e indirizzo della iat), incrementa il puntatore alla
tabella di 16 byte, e procede alla prossima istruzione
9) ripetere il
ciclo finche non si è scannata tutta la sezione .text :)
(il logger
scannerà solo per le chiamate a Kernel32 e User32, dato che per le altre ci
ricostruiremo l'iat a mano (è una cavolata non preoccupatevi)) Ok, vediamo
il codice:
; logger.asm
.486P .Model Flat,
stdcall
.code start: mov esi, 401000h ; mettiamo in esi il VA dell'inizio della sezione
.text mov edi, 4E2000h-401000h ; in edi la grandezza della sezione .text (che sarà inizio
.rdata - inizio .text) mov ecx, 20001000h ; indirizzo del punto dove creeremo la nostra bella tabella che
poi dumperemo
search_loop: ; questo loop
scanna tutti gli opcode alla ricerca di quell che ci
interessano cmp word ptr [esi], 015ffh ; è un call dword ptr ? jne
try_jmp cmp dword ptr [esi+2],04E2038h ; l'indirizzo della iat è compreso tra gli indirizzi di kernel32
e user32? jl try_jmp cmp
dword ptr [esi+2],04E2224h jg try_jmp ; se no salta al prossimo controllo
lea eax, [esi+6] ; mettiamo in eax il return address
(sommiamo 6 ad esi perché il call dword ptr occupa 6
byte) pushad push eax ; pushiamo il return address, così la funzione di risoluzione
penserà che stiamo chiamando al funzione dal punto
vero mov ebx,[esi+2] ; mette
in ebx l'indirizzo della iat mov edx,[ebx] ; mettiamo in edx la funzione
chiamata and edx,0f0000000h ;
controlliamo che non sia una di quelle gia risolve (perché nel caso che non è
risolta, sarà 01ECxxxx, se è gia risolta sarà BFF8xxxx per kernel32 ad
esempio) test edx,edx jnz
imm ; se è stata risolta, allora aggiungila direttamente
alla tabella jmp dword ptr [ebx] ; altrimenti salta alla funzione di
risoluzione
OkApi: ; questa è la funzione
che loggerà la chiamata (a cui salteremo col jmp nella funzione di
risoluzione mov [ecx],esi ;
mette nel primo elemento della tabella l'indirizzo da dove è stata chiamata la
funzione mov eax,[esp] ;
mette in eax l'entry point dell'api da chiamare
mov [ecx+4],eax ; e mette l'indirizzo nel secondo
elemento mov eax,ebx ; in eax
abbiamo l'indirizzo nella iat mov [ecx+8],eax
; e lo mettiamo nel terzo
elemento mov dword ptr [ecx+12],0h ; azzeriamo la quarta dword inc esi
; incrementa il puntatore alle
istruzioni dec edi ;
decrementa la lunghezza del codice add ecx,16
; ci spostiamo di 16 byte nella tabella, così siamo alla
prossima entry, perché ogni entry occupa 16 byte (4
dword) test edi,edi ; sono
finite le istruzioni? jnz search_loop ; se ci sono altre istruzioni continua, altrimenti lancia un int
1 che verrà catturato dal softice int
1
imm: ; questa funzione invece non fa altro che
aggiungere alla tabella un'api gia risolta ; esi
= caller address ; ebx = iat address ; edx = api entry
point
mov [ecx], esi ; al
solito mettiamo nel primo elemento il caller
address mov eax, [ebx] mov
[ecx+4], eax ; mettiamo nel secondo l'api
chiamata mov [ecx+8], ebx ;
nel terzo l'indirizzo nella iat mov dword ptr
[ecx+12], 0h ; e azzeriamo l'ultimo
valore inc esi dec
edi add ecx, 16 test edi,
edi jnz search_loop int
1
try_jmp: ; cerca i
jmp cmp word ptr [esi], 025FFh ; controlla che l'opcode corrisponda a quell del jmp, se si
procede come nel caso del call, quindi non commento, se no passa al prossimo
controllo jne try_esi cmp
dword ptr [esi+2],04E2038h jl
try_esi cmp dword ptr
[esi+2],04E2224h jg try_esi mov
eax, dword ptr [esi+2] cmp dword ptr [eax],
00789C50h je try_again lea eax,
[esi+6] pushad push
eax mov ebx,[esi+2] mov
edx,[ebx] and edx,0f0000000h test
edx,edx jnz imm jmp dword ptr
[ebx]
try_esi: ; cerca le call
esi cmp word ptr [esi], 358Bh ; prima di cercare una call esi però, dobbiamo trovare il mov
esi, dword ptr [xxx], 358B è il suo opcode jne
try_edi cmp dword ptr [esi+2],
04E2038h jl try_edi cmp dword ptr
[esi+2], 04E2224h jg try_edi mov
ebx, [esi+2] ; ora che abbiamo trovato la mov esi,
mettiamo in ebx l'indirizzo nella iat search_for_call_esi: ; questa funzione cerca la call esi a partire dal punto del mov
esi, dword ptr inc esi cmp
word ptr [esi], 0D6FFh ; cerca per l'opcode di call esi,
se non lo trova continua a cercare jnz
search_for_call_esi lea eax, [esi+2] ; abbiamo l'opcode, quindi come al solito mettiamo in eax il
return address pushad push
eax mov edx, [ebx] and edx,
0F0000000h ; facciamo il solito test per vedere se non è
gia risolta test edx, edx
jnz imm jmp dword ptr [ebx] ; se non
è gia risolta allora risolviamola :)
try_edi: ; cerca per il mov edi, dword ptr, e successivamente il call
edi, non commento perché tanto accade sempre la stessa
cosa cmp word ptr [esi],
3D8Bh jne try_ebx cmp dword ptr
[esi+2], 04E2038h jl try_ebx cmp
dword ptr [esi+2], 04E2224h jg
try_ebx mov ebx,
[esi+2] search_for_call_edi: inc
esi cmp word ptr [esi], 0D7FFh jnz
search_for_call_edi lea eax,
[esi+2] pushad push
eax mov edx, [ebx] and edx,
0F0000000h test edx, edx jnz
imm jmp dword ptr [ebx]
try_ebx: ; cerca per il mov ebx, dword ptr/call
ebx cmp word ptr [esi],
1D8Bh jne try_ebp cmp dword ptr
[esi+2], 04E2038h jl try_ebp cmp
dword ptr [esi+2], 04E2224h jg
try_ebp mov ebx,
[esi+2] search_for_call_ebx: inc
esi cmp word ptr [esi], 0D3FFh jnz
search_for_call_ebx lea eax,
[esi+2] pushad push
eax mov edx, [ebx] and edx,
0F0000000h test edx, edx jnz
imm jmp dword ptr [ebx]
try_ebp: ; cerca per il mov ebp, dword ptr/call
ebp cmp word ptr [esi],
2D8Bh jne try_again cmp dword ptr
[esi+2], 04E2038h jl try_again cmp
dword ptr [esi+2], 04E2224h jg
try_again mov ebx,
[esi+2] search_for_call_ebp: inc
esi cmp word ptr [esi], 0D5FFh jne
search_for_call_ebp lea eax,
[esi+2] pushad push
eax mov edx, [ebx] and edx,
0F0000000h test edx, edx jnz
imm jmp dword ptr [ebx]
try_again: ;
arriveremo qui se l'opcode non è uno dei casi precedenti, quindi passiamo al
prossimo byte di codice inc
esi dec edi jne
search_loop int 1 ; qui arriveremo
al termine del fixing e ci brekkerà il softice
end start
Ok, adesso invece vediamo come funziona (il
codice mi sembra commentato abbastanza bene :)). Allora prima di tutto
dobbiamo compilarlo, quindi col masm, fate ml /c /coff /Cp logger.asm, e poi
link /SUBSYSTEM:WINDOWS logger.obj, a questo punto abbiamo un file exe con cui
non possiamo farci assolutamente niente :), quindi apriamolo con ultraedit, e
tagliamo l'header (praticamente tagliate finche non inizia il primo byte di
codice, e successivamente tagliate dall'ultimo in poi), quindi, tagliate da 0
a 1ff, e da 3B0 a 400, adesso abbiamo qualcosa gia più utile :), segnatevi la
dimensione di questo file, che se avete fatto le cose per bene dovrebbe essere
di 1B0 byte. Ok, ora accertatevi che icedump sia caricato (dovrebbe esserlo
considerando il fatto che avete fatto partire SR2), fate partire SR2, e
brekkate sull'entry point, a questo punto iniettermo il codice direttamente
dentro il processo, ora, invece di stare a cercare dello spazio vuoto, io ho
scelto una soluzione meno pulita ma più rapida, ovvero mi sono allocato 4
pagine e ho lavorato con quelle, ed è quello che farete pure voi :), quindi
sempre nel softice, allocate 4 pagine con /alloc 20000000 4000 (ho usato
20000000 come base address perché sto tranquillo che qui non c'è niente,
almeno nel mio caso :)), adesso tocca a inserire le pagine in memoria, quindi
fate pagein 20000000, pagein 20001000, pagein 20002000 e pagein 20003000. Ok
adesso abbiamo le nostre belle 4 pagine, ora nella prima pagina caricheremo il
logger, la seconda invece la useremo per salvare la nostra bella tabella (e fa
pure rima! :)), dunque, per caricare il logger, fate /load 20000000 1B0
f:\games\sr2\logger.exe (ovviamente sostuituite la directory del gioco con
quella che avete voi), a questo punto il nostro bel logger è in memoria,
adesso dobbiamo modificare il ret della routine di risoluzione in modo che una
volta risolta l'api salti al codice del logger invece che all'api da chiamare,
ora il pezzo di codice a cui bisogna saltare è quello che nel file asm era
nella label OkApi, l'indirizzo dovrebbe essere 2000003E, comunque voi per
sicurezza controllate, fatto questo, fate A 1004426A (l'indirizzo è quello del
ret, se non vi corrisponde, allora cercatelo (basta seguire qualsiasi call
dword ptr [xxx] e poi vedere qual'è l'indirizzo del ret)) e mettete
l'istruzione jmp 2000003E, in questo modo invece di chiamare l'api salterà al
codice del logger. Bene, adesso dobbiamo far eseguire il logger :), quindi
facciamo un brutale r eip 20000000, fate u eip, e vi ritroverete nel codice
del logger, ora basta fare i1here on e poi F5, in questo modo il softice
brekkerà ad ogni int 1, e il primo int 1 viene eseguito quando il logger ha
finito il suo lavoro, appena brekkerà il softice, significa che abbiamo
terminato lo scan e la tabella è stata creata, per sapere quanto è grande,
basta vedere il valore di ecx, che sarà 20002B50, quindi per trovare la
grandezza basta fare 20002B50 - 20001000, ovvero 1B50 byte, ora quello che ci
resta da fare è dumparla, quindi facciamo un bel /dump 20001000 1B50
f:\games\sr2\iat.dmp, a questo punto avremo la nostra tabella su disco, che
dovrebbe assomigliare a qualcosa del genere:
Come vedete, il
primo elemento della tabella è il caller address, il secondo è l'api chiamata,
mentre il terzo è il punto nella iat in cui si trova quell'api, ma a noi di
questo terzo argomento non ce ne frega niente :), ok adesso abbiamo risolto la
maggior parte delle api, ma per adesso mettiamo da parte questo file, ci
servirà in seguito, quello che faremo adesso (come anticipato qualche
paragrafo fa) sarà ricostruire le entry per Gdi32, Advapi32 e
Ole32. Allora, prima di tutto occupiamoci di Gdi32 (terza entry nella IT),
quello che faremo sarà mettere nella sua IAT (che si trova all'indirizzo
E202C), i puntatori all'HINT-NAME della funzione importata (perché come
sapete, il nome della funzione è preceduto da 2 byte che sono l'hint della
funzione, se presente, il pe loader risolve con quello altrimenti col nome, e
il puntatore deve puntare all'hint), l'unico problema è che questi puntatori
non possiamo scriverli nell'ordine in cui stanno nella zona che contiene i
nomi, ma dobbiamo scriverli nell'ordine originale, per saperlo, ci basta
vedere che funzioni chiama il file originale per le due entry della IAT di
Gdi32, ovvero 01ECA337 per il primo e 01ECA682 per il secondo, questi sono gli
indirizzi delle funzioni di ponte di safedisc, quindi quello che faremo sarà
mettere un break su quegli indirizzi, e vedere a che api portano, e scopriremo
che la prima api è GetStockObject, la seconda è GetDeviceCaps, quindi nella
IAT dovremo scrivere il puntatore a GetStockObject e GetDeviceCaps, ma come
facciamo a sapere a quale rva si trovano questi nomi? Semplice, con UltraEdit
basta che facciamo Search->Find e attiviamo l'opzione find ascii, poi
scriviamo il nome della funzione e vediamo in che punto sta, e troveremo che
GetStockObject sta all'offset E618E (che corrisponde anche all'RVA, perché in
questo caso abbiamo section offset = section rva), e GetDeviceCaps all'offset
E617E, quindi nella IAT di Gdi32 scriviamo questi due valori, quindi alla fine
avremo una cosa del genere:
Come vedete nelle
altre entry abbiamo ancora gli indirizzi del safedisc, ok, ora dobbiamo fare
la stessa cosa per Advapi32 e Ole32, state attenti che con Advapi32 avete gia
una funzione risolta, ovvero RegCloseKey (BFE81644), quindi non dovrete
neanche perdere tempo a cercare questa funzione, mentre per le altre dovete
fare la stessa cosa che avete fatto per Gdi32, probabilmente non riuscirete a
risolvere un'api per Advapi32, che è RegOpenKeyExA, non preoccupatevi, tanto è
l'unica api che non è stata risolta quindi basta andare per esclusione :),
comunque siccome non voglio farvi perdere tempo a cercarli, ecco gli altri
indirizzi (le funzioni stanno nell'ordine in cui vanno messe nella
IAT):
Se qualche valore (che non è
della iat di advapi32 o gdi32) non vi corrisponde, è normale, perché questo
era l'unico eseguibile, che avevo con la iat fixata, e avevo sistemato anche
le altre, quindi ci saranno alcuni valori diversi, ma non è un
problema. Ok, adesso se disassemblate con W32Dasm (fatelo :)), vedrete che
tra le funzioni importate ci saranno anche quelle di Advapi32, Gdi32 e Ole32
(mentre per kernel32 e user32 ci sarà qualcosa del genere. kernel32.kernel32,
user32.user32). Adesso, dobbiamo fixare Kernel32 e User32, per far ciò
useremo un programmino che ho fatto in C, che scannerà la tabella che ci siamo
dumpati, e salverà in un'array di strutture il caller address e l'api
chiamata, a questo punto si posizionerà all'offset del caller address, e
sostituirà all'argomento del call dword (o jmp o mov esi, dword ptr, ecc...)
il primo elemento della iat, in cui metterà l'indirizzo della funzione
chiamata, e così via, e in più, ogni volta che trova 2 entry nella iat che
chiamano la stessa funzione, sostituirà l'argomento con quello del caller
address che punta alla stessa funzione, così non avremo doppioni, uhm... mi sa
che mi sono spiegato male, comunque riassumiamo brevemente i passi
fondamentali che dovrà compiere il fixer:
1) Scannare il file iat.dmp,
e per ogni entry (composta da 16byte), riempire una struttura fatta in questo
modo:
dove caller è l'indirizzo
da dove viene chiamata l'api, api_called contiene l'entry point dell'api
chiamata, iat_addr contiene l'indirizzo della iat originale, e padding è usato
per leggere gli ultimi 4 byte 00, fatto ciò, riempire una struttura di questo
tipo:
e aggiungerla ad un'array di strutture di quel tipo, i
campi api_called e caller verranno copiati dalla struttura fixs, mentre
iat_addr partirà dall'inizio della iat (per kernel32 E2038), e ad ogni nuova
api chiamata, verrà incrementato di 4, se invece l'api è gia presente
nell'array, allora non verrà incrementato, ma si userà quello gia presente, in
questo modo avremo un iat address univoco.
2) Finito lo scanning,
tramite un MMF (memory mapped file) aprire il file exe, leggere il primo
elemento, posizionarsi all'indirizzo della iat indicato, e mettere l'indirizzo
dell'entry point della funzione chiamata
3) Spostarsi all'indirizzo
indicato come caller address, e vedere l'opcode, se è un call dword o un jmp
dword, allora sostituire direttamente l'argomento con quello nuovo
4)
se è un call edi, call esi, call ebx o call ebp, scannare all'indietro il file
alla ricerca del mov esi, dword ptr [xxx], mov esi, dword ptr [xxx] ecc...,
una volta trovato cambiare l'argomento del mov.
5) ripetere il ciclo
sia per kernel32 che per user32
Ok come al solito per prima cosa
vediamo il codice (avverto, il codice fa letteralmente schifo, ma che volete,
è stato codato in 10 minuti e l'importante è che funziona :))
:
#include
<stdio.h> #include <stdlib.h> #include
<windows.h> #include <vector> // per
l'array dinamico useremo la classe vector della stl
using
namespace std;
//queste due strutture
definiscono rispettivamente un'etry del file iat.dmp,
//e le informazioni sull'api
chiamata con relativo caller address e iat address struct fixs { DWORD
caller; DWORD api_called; DWORD
iat_addr; DWORD pad; };
f = fopen("iat.dmp",
"r+b"); // apriamo il file iat.dmp che ci scanneremo in
seguito
while(!feof(f))
{ bool isEqual =
false; static k32iat = 0xE2038;
//inizio della iat di
kernel32 static u32iat =
0xE218C; // inizio della iat di
user32 iatinfo
info;
fread(&fix,
sizeof(fixs), 1, f);
if((fix.api_called >= 0xBFF51000) && (fix.api_called <=
0xBFF5D000)) //controlla che la funzione sia di user32 (se
avete user32 mappato in un indirizzo diverso, SOSTITUITE questi valori!
altrimenti non funzionerà niente, per sapere dov'è mappato user32 da softice
fate map32 user32)
{
for(int i = 0; i < u32napi; i++) // questo ciclo for
controlla se l'api è gia stata aggiunta
all'array
{
if(fix.api_called ==
u32apis[i].api_called)
{
info.api_called = fix.api_called;
info.iat_addr = u32apis[i].iat_addr; // se si allora
usiamo il suo indirizzo nella
iat
info.caller =
fix.caller;
u32apis.push_back(info); //aggiungiamo la struttura
all'array
isEqual =
true;
u32napi++;
break;
}
}
if(!isEqual)
{
info.api_called =
fix.api_called;
info.iat_addr = u32iat; //se invece non è stata gia
aggiunta, allora mettiamo come iat address quello
corrente
info.caller =
fix.caller;
u32iat += 4; //incrementa di 4 l'iat address
corrente
u32apis.push_back(info); //aggiunge alla
struttura
u32napi++;
}
} else //stesso discorso di prima solo che per
kernel32
{
for(int i = 0; i < k32napi;
i++)
{
if(fix.api_called ==
k32apis[i].api_called)
{
info.api_called =
fix.api_called;
info.iat_addr =
k32apis[i].iat_addr;
info.caller =
fix.caller;
k32apis.push_back(info);
isEqual =
true;
k32napi++;
break;
}
}
//questo ciclo for scanna la
tabella appena creata e fixa tutte le chiamate alle api che sono state
loggate for(int i
= 0; i < k32napi; i++)
{ WORD
istr; DWORD
operand;
pTmpPtr2 +=
k32apis[i].iat_addr; //pTmpPtr2 punta all'indirizzo nella
iat per la struttura
corrente DWORD addr =
k32apis[i].api_called; //addr è l'entry point dell'api
chiamata
//mette l'entry point
dell'api chiamata nella iat __asm
{ mov
esi, dword ptr
[pTmpPtr2]
mov eax,
addr mov
dword ptr [esi], eax
}
//prende
l'istruzione __asm
{ mov
esi, dword ptr
[pTmpPtr]
mov ax, word ptr
[esi]
mov istr, ax } //e controlla che istruzione
è
switch(istr) {
//nel caso di call dword ptr
e jmp dword ptr cambiamo solo l'argomento del call o jmp
case
0x15FF:
case
0x25FF:
{
pTmpPtr +=
2;
operand = k32apis[i].iat_addr +
0x00400000;
__asm
{
mov esi, dword ptr
[pTmpPtr]
mov eax,
operand
mov dword ptr [esi],
eax
}
}
break; //nel caso delle altre istruzioni (call esi,
edi, ec...) scanniamo il file all'indietro alla ricerca del relativo mov, una
volta trovato cambiamo
l'argomento
case
0xD6FF:
{
while(istr !=
0x358B)
{
pTmpPtr--;
Quindi il fixer richiede la presenza del file
crack.exe e iat.dmp nella sua directory, assicuratevi che ci siano quei file,
e lanciatelo (ovviamente il file crack.exe sarà l'ultima versione
dell'eseguibile su cui state lavorando), a questo punto abbiamo quasi finito
(insomma :)). Date un'occhiata al file, e vedrete che nella iat di kernel32 e
user32 ci saranno un sacco di indirizzi di entry point delle api, solo che
noterete che le ultime 3 entry di kernel32 (a partire da E217C) e le ultime 4
di user32 (a partire da E2214) non sono state fixate, questo perché alcune
funzioni non venivano chiamate in nessuno dei modi descritti ma tramite il jmp
00762xxx (che il logger non esamina), e quindi non siamo stati in grado di
fixarle, ma lo faremo a mano senza problemi. Però adesso possiamo fixare il
file col PEditor, perché un bel po' di api corrette le abbiamo, quindi
azzerate le entry che non sono state fixate, aprite il PEditor, fate browse, e
selezionate il file crack.exe, selezionate il rebuilder, e fate Rebuild Import
Table (assicuratevi che l'opzione selezionata sia Rebuild Import Table e NON
Rebuild New Import Table), fate ok, e aspettate, ci vorrà un po' di tempo, ma
alla fine avrete un file quasi a posto. Quando il PEditor ha terminato il suo
lavoro, chiudete il file, apritelo con Hiew ad esempio, e se avete la versione
6.70, fate F8 e poi F7 (nella modalità hex), e vedrete la lista delle imports,
selezionate Kernel32 e vedrete che ci sono quasi tutte le api, stesso discorso
per User32, a questo punto non ci resta che sistemare le altre api, far
partire il file e risolvere gli eventuali crash (ce ne saranno pochissimi non
preoccupatevi). Allora, prima di tutto risolviamo i vari jmp 00762xxx, cosa
abbastanza semplice da fare. E' giunto il momento di usare il W32Dasm, quindi
apritelo e fategli disassemblare il file crack.exe (sempre l'ultima versione
ovviamente), quando avrà finito, usate la sua funzione di search per cercare
il testo "jmp 00762", e segnatevi l'indirizzo dei punti in cui trova quel jmp,
alla fine troverete questi indirizzi:
Bene, ma che ci facciamo con questi indirizzi? Semplice, ci
risolveremo queste api, poi cambieremo il jmp 00762 con call dword ptr o call
xxx, ok, adesso vediamo come risolvere le api, è abbastanza semplice. Per
prima cosa brekkate sull'entry point, a questo punto fate bpx 1004426A (break
sul ret della funzione di risoluzione), e fate r eip 004DB7FA, fate u eip e
vedrete il jmp, se volete eseguitelo per vedere cosa succede a runtime, ma
l'importante è che arrivate al ret (o con F5 (perché abbiamo settato il break)
o se siete masochisti vi fate tutto il codice di risoluzione), e che lo
eseguite, a questo punto vi ritroverete nell'api chiamata, segnatevela e
continuate così per tutti gli altri indirizzi. Siccome sono buono (insomma
:)), ecco la lista delle funzioni chiamate:
Ok, quello che faremo adesso è cercarci gli indirizzi
nella iat di quelle funzioni, nel caso che la funzione non sia presente nella
iat sistemata da PEditor, allora la aggiungeremo a mano, quindi iniziate a
cercarvi gli indirizzi per le call dword ptr, per farlo, sempre usando il
W32Dasm, cercate un punto in cui venga chiamata quella funzione, ad esempio
per GetStartupInfoA, andate nel dialogo delle import, e fate doppio click su
KERNEL32.GetStartupInfoA, e segnatevi che indirizzo viene chiamato (che sarà
004E2108). Come noterete, le chiamate a GetCurrentThreadId e GetTickCount,
sono del tipo:
call
xxxx
xxxx: jmp dword ptr [yyy]
Quindi voi dovete segnarvi l'indirizzo del jmp dword ptr,
perché noi nel caso di GetCurrentThreadId e GetTickCount non metteremo un call
dword ptr, ma con hiew assembleremo un call 0046FE40 per GetCurrentThreadId e
un call 0045AAC0 per GetTickCount (ricordatevi che quando assemblate con hiew
gli indirizzi sono dei RVA, quindi ad esempio dovete assemblare un call 6FE40
per 004EFE40). Dopo esservi segnati tutti gli indirizzi, noterete che non
avete trovato niente per GetCommandLineA, SetEvent, EndPatin e
DialogBoxParamA, nessun problema, aggiungeremo queste funzioni a mano,
iniziamo con Kernel32, per trovare a che indirizzo si trova il nome, con
UltraEdit fate un search in ascii, e cercate la stringa del nome, segnatevi
l'indirizzo e aggiungetelo nella iat dopo l'ultima entry, fatelo sia per
Kernel32 che per User32, comunque gli indirizzi da aggiungere (e relative
posizioni risultanti nella iat) sono questi:
Ok, adesso che avete tutti gli indirizzi, fixate
il file (andate all'indirizzo del jmp e mettete un call dword ptr (vi conviene
mettere direttamente l'opcode, ad esempio per GetStartupInfoA metterete
FF1508214E00) o un call xxx (solo per GetCurrentThreadId e
GetTickCount)). Bene, adesso che avete finito di fixare avete un file quasi
funzionante, prepariamoci alla prima esecuzione :). Quindi fate partire, e...
crash! :) Beh ce lo aspettavamo, quindi andiamo a vedere perché crasha, uhm...
l'ip del crash risulta essere 00478DDD, è un ret, quindi significa che c'è
qualche api fixata male, infatti andando un po' sopra (molto sopra) troveremo
dei mov edi e mov esi e relativi call, quindi facciamo partire il file
originale, e vediamo cosa succede nelle varie call edi, call esi e call ebx,
uhm... call edi punta a RegOpenKeyExA, call esi a RegQueryValueExA e call ebx
a RegCloseKey (vedete i mov), apriamo il crack e controlliamo, corrispondono
tutte, tranne che la call edi, che punta a RegOpenKeyA, quindi sostuituiamo
l'argomento del mov edi, con mov edi, dword ptr [004E2014] (per farlo usate il
solito hiew). Facciamo ripartire e noteremo che non crasha più la, ma ad
un'altro address :), stavolta è 00478882, stesso discorso di prima,
controllate il file originale, e scoprirete che all'indirizzo 00478842 viene
chiamata RegOpenKeyExA, mentre nel crack abbiamo RegCreateKeyExA, ok come
prima sostituite con il il giusto indirizzo della iat salvate e fate
ripartire... altro crash :) ok, non perdete la pazienza, vedete a che eip
crasha, e sarà 00450883, al solito fate partire il programma originale e
vedete che api viene chiamata, GetDriveTypeA, uhm... se non sbaglio manca
ancora un'entry nella iat di kernel32, e casualmente :), GetDriveTypeA non è
presente tra le funzioni importate di Kernel32, esatto! aggiungete anche
quella (l'indirizzo del nome è 000E5D8D), e modificate l'argomento del call
dword ptr, con call dword ptr [004E2184]. Ok, fate ripartire per l'ennesima
volta e otterrete un'altro bel crash :)) (forza ne mancano solo 3), stavolta
l'eip indicato è 0044FE7D, prendete la funzione esatta col file originale, e
vedrete che è Sleep (IAT: 004E2088), sostituite, salvate e fate ripartire, e
ora sono guai. Il crash avviente dentro il codice di user.exe e non abbiamo
alcuna informazione su cosa succede, dato che l'ip è sballato, che facciamo? E
qui sono cazzi :), allora l'unica cosa da fare è tracciare dalla chiamata a
GetCommandLineA in poi, e seguire le varie call, insomma andando per tentativi
e con molta fortuna (non preoccupatevi non è semplice ma manco difficile),
scoprirete che i problemi si trovano agli indirizzi 00478625 e 0047862B, nel
file originale vengono chiamate TranslateMessage e DispatchMessage, mentre nel
crack GetClientRect e FillRect, uhm... c'è qualcosa che non va :), e
scoprirete che queste funzioni non sono importate da User32 e che mancano solo
2 entry in User32, quindi aggiungete anche queste due funzioni (gli indirizzi
sono 000E6112 e 000E60FF), e fixate i mov ebp e mov edi che diventeranno
rispettivamente mov ebp, dword ptr [004E221C] e mov edi, dword ptr [004E2220].
Forza abbiamo quasi finito, fate partire e....
PARTEEEEEEEEEEEEEEEEEEEEEEEE!!!!!!!!!!!!!! Provate però a cominciare una nuova
partita :), crash! Ok, non spazientiamoci, osserviamo i dati del crash
00478E94, è un ret, quindi è un'api sballata, controllate il file originale e
scoprirete che a 00478E0C viene chiamata RegCreateKeyEx, mentre nel crack
viene chiamata RegOpenKeyA, sostituite e salvate. Fate partire, ok sappiamo
che parte, fate nuovo gioco... WOWWWWWWW!!!!!! Ecco comparire il filmato
iniziale ed ecco raziel che fa la sua comparsa nella stanza del
tempo!!!!!!!!!!!!!! FUNZIONAAAAAAAAAAAAAAAAAAAAAAAAAA!!!!!!!!!!!!!!!!!!!!
Premente ESC e inizierà a parlare moebius, mandatelo a quel paese e fate
ALT+F4, d'oh! crash! Controllate il crash e vedrete che è a 0045017C, un call
edi, controllate che viene messo in edi, e vedrete che è GetVersionEx, ok
nessun problema, controllate il file originale e vedrete che è Sleep la
funzione da mettere, fixate, salvate, avviate e... GIOCATE!!!!!! Adesso il
gioco funziona alla perfezione, niente più safedisc e niente più crash!!!!!!!!
Ok giocate finche volete ma poi uscite che c'è ancora un'ultima cosa da fare,
ovvero rimuovere il cd check. Procediamo.
Chi di voi ricorda i bei
tempi in cui quando eravate alle prime armi per cracckare cercavate la stringa
del messaggio di errore e cambiavate il jne poco prima? Be è ora di tornare al
passato :), quindi togliete il cd, e fate partire il gioco, vi appare il
messaggio "Please insert ecc...", ok apriamo il file con w32dasm, e cerchiamo
quella stringa, troviamo un'unica reference a 004509E0, andiamo un po' più
sopra e a 004509D1 troviamo un jne, cambiamolo in un jmp incondizionato,
avviamo soul reaver 2, e... parte! Senza cd! Ma che controllo del cacchio!
Bene a questo punto se volete fate un rebuild con il PEditor (ovviamente fare
un realign) così otterette un file più piccolo, comunque come volete, la
differenza è di 32kb.
Ok, abbiamo FINITO!!!!!!!!!!!!!!! Ora divertitevi
quanto vi pare ad ammazzare vampiri, a succhiare le anime, e... be non vi
svelo la trama :).
(un ultima nota, alcuni crash sono dovuti al
fatto che il logger per qualche arcano motivo, non ha loggato bene alcune
chiamate, ma infondo non sono poi tante, quindi chi se ne frega! :), se volete
potete pure aggiungere il logging per advapi32 dato che da un po' di problemi,
io ho preferito di no perché volevo vedere come si comportava sistemando
l'entry per advapi32 a mano) Ciao!!
Quake2
Note finali
Be che dire, il tutorial è venuto più lungo del
previsto, ma credo che sia abbastanza chiaro e soprattutto ho cercato di
descrivere al meglio ciò che c'è da fare, ma purtroppo non sono molto bravo a
spiegarmi :), e adesso è ora dei ringraziamenti e dei saluti:
cominciamo
con azzurra :) :
un saluto, un ringraziamento, un milione di
ringraziamenti, un milardo di ringraziamenti, vabbe basta :), a Yado per il suo
aiuto e per i tanti consigli che mi ha dato su safedisc e per il codice del
logger :) (anche se poi alla fine l'ho cambiato quasi tutto :)) ringrazio
anche AndreaGeddon perché anche lui mi ha dato un sacco di consigli vorrei
salutare anche tuttti quelli di #crack-it e #asm (tra cui albe, deimos, pbdz,
true-love, blackdeath, ecc... :)) un saluto particolare a ^Spider^ perché mi
sta simpatico (e perché tra un po' tocca pure alla sua tarantula :)) un
saluto a tutto #gameprog-ita (casa dolce casa :))
e finiamo con ircnet :)
:
vorrei salutare tutti quelli di
#kill'em-all (hail!) vorrei salutare anche tutti quelli di #programmazione, e
vorrei ringraziare recidjvo per avermi fatto da guida a milano altrimenti mi
sarei perso :)
come ultima cosa, forse non importante ma per me lo è,
vorrei ringraziare gli Iced Earth per la splendida serata che mi hanno fatto
passare il 10 febbraio, grazie del concerto!!!!!!!!!!!...Si
si...concerto...NdQue :P
Disclaimer
Vorrei ricordare che il software va comprato
e non rubato, dovete registrare il vostro prodotto dopo il periodo di
valutazione. Non mi ritengo responsabile per eventuali danni causati al vostro
computer determinati dall'uso improprio di questo tutorial. Questo documento è
stato scritto per invogliare il consumatore a registrare legalmente i propri
programmi, e non a fargli fare uso dei tantissimi file crack presenti in rete,
infatti tale documento aiuta a comprendere lo sforzo immane che ogni singolo
programmatore ha dovuto portare avanti per fornire ai rispettivi consumatori i
migliori prodotti possibili.
Noi reversiamo al solo scopo informativo e di
miglioramento del linguaggio Assembly.