Crackare Soul Reaver 2 (Manual Unpacking) |
Data |
by
"Quake2" |
|
27/02/2002 |
UIC's Home
Page |
Published
by Quequero |
In a time when everyone follows
Ignorance can kill
|
Coooooooomplimenti
hai fatto un tute dettagliatissimo con tando di logger bravo bravo
|
In a world of
spoon-fed emotion
Intelligence can save |
.... |
- E-mail: [email protected]
- Quake2,
51184823, IRCNet: #programmazione, Azzurra: #gameprog-ita/#crack-it/#asm
|
.... |
Difficoltà |
( )NewBies (X)Intermedio ( )Avanzato
( )Master
|
|
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
- 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 :)
- SoftIce
IceDump
ProcDump 1.6
Hiew
FileMon
UltraEdit32 7.0
W32Dasm
MASM 6.11 o superiore
PEditor 1.7
- Qualsiasi negozio di giochi per pc :)
- 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
:)
- 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:
000E2000 D3 A9 EC 01 44 16 E8 BF 69 B0 EC 01 B4 B3 EC 01 ....D...i.......
000E2010 FF B6 EC 01 4A BA EC 01 00 00 00 00 72 A4 B7 BF ....J.......r...
000E2020 00 00 00 00 60 B0 00 70 00 00 00 00 37 A3 EC 01 ....`..p....7...
000E2030 82 A6 EC 01 00 00 00 00 77 03 EB 01 C2 06 EB 01 ........w.......
000E2040 0D 0A EB 01 58 0D EB 01 A3 10 EB 01 EE 13 EB 01 ....X...........
000E2050 39 17 EB 01 84 1A EB 01 CF 1D EB 01 1A 21 EB 01 9............!..
000E2060 65 24 EB 01 B0 27 EB 01 FB 2A EB 01 46 2E EB 01 e$...'...*..F...
000E2070 91 31 EB 01 DC 34 EB 01 27 38 EB 01 72 3B EB 01 .1...4..'8..r;..
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:
000E5840 00 00 00 00 FF FF FF FF FF FF FF FF FC 5E 0E 00 .............^..
000E5850 38 20 0E 00 00 00 00 00 FF FF FF FF FF FF FF FF 8 ..............
000E5860 72 61 0E 00 8C 21 0E 00 00 00 00 00 FF FF FF FF ra...!..........
000E5870 FF FF FF FF A0 61 0E 00 2C 20 0E 00 00 00 00 00 .....a.., ......
000E5880 FF FF FF FF FF FF FF FF 0E 62 0E 00 00 20 0E 00 .........b... ..
000E5890 00 00 00 00 FF FF FF FF FF FF FF FF 52 62 0E 00 ............Rb..
000E58A0 84 22 0E 00 38 59 0E 00 FF FF FF FF FF FF FF FF ."..8Y..........
000E58B0 5C 62 0E 00 1C 20 0E 00 98 5B 0E 00 FF FF FF FF \b... ...[......
000E58C0 FF FF FF FF 7C 62 0E 00 7C 22 0E 00 40 59 0E 00 ....|b..|"..@Y..
000E58D0 FF FF FF FF FF FF FF FF 9C 62 0E 00 24 20 0E 00 .........b..$ ..
000E58E0 44 5B 0E 00 FF FF FF FF FF FF FF FF 66 63 0E 00 D[..........fc..
000E58F0 28 22 0E 00 70 5B 0E 00 FF FF FF FF FF FF FF FF ("..p[..........
000E5900 1C 64 0E 00 54 22 0E 00 00 00 00 00 00 00 00 00 .d..T"..........
000E5910 00 00 00 00 00 00 00 00 00 00 00 00 ............
Allora, per prima cosa vi ricordo la struttura di una entry dell'IT:
+00h OriginalFirstThunk
+04h TimeDateStamp
+08h ForwarderChain
+0Ch NameRVA
+10h FirstThunk
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:
jmp 000762099
000762099:
:00762099 53 push ebx
:0076209A E800000000 call 0076209F
CALL at Address 0076209A:
:0076209F xchg dword ptr [esp], eax
:007620A2 pushfd
:007620A3 add eax, FFFFFF61
:007620A8 mov ebx, dword ptr [eax]
:007620AA imul ebx, 00000005
:007620AD add ebx, dword ptr [eax+04]
:007620B0 popfd
:007620B1 pop eax
:007620B2 xchg dword ptr [esp], ebx
:007620B5 ret
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:
Kernel32_1: E5BB0 - E5EFC, lunghezza: 34C
Kernel32_2: E6428 - E66D2, lunghezza: 2AA
User32: E5F0A - E6172, lunghezza: 268
Gdi32: E617E - E61A0, lunghezza: 22
Advapi32: E61AA - E620E, lunghezza: 64
Ole32: E621C - E6252, lunghezza: 36
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:
Kernel32_1: 4B
Kernel32_2: 47
User32: 55
Gdi32: 47
Advapi32: 41
Ole32: 6F
Ok, adesso vediamo il codice del decrypter:
__declspec(naked) void Decr(char *Ptr, DWORD *XorValue)
{
__asm {
push ebp
mov ebp, esp
sub esp, 00000010h
mov eax, dword ptr [ebp+08h]
mov ecx, dword ptr [eax]
mov dword ptr [ebp-08h], ecx
mov edx, dword ptr [ebp+08h]
mov eax, dword ptr [edx+04h]
mov dword ptr [ebp-0Ch], eax
mov ecx, 20h
mov dword ptr [ebp-10h], ecx
mov edx, 9E3779B9h
shl edx, 05h
mov dword ptr [ebp-04h], edx
jmp1:
mov eax, dword ptr [ebp-10h]
mov ecx, dword ptr [ebp-10h]
sub ecx, 00000001
mov dword ptr [ebp-10h], ecx
test eax, eax
jbe jmp2
mov edx, dword ptr [ebp-08h]
shl edx, 04h
mov eax, dword ptr [ebp+0Ch]
add edx, dword ptr [eax+08h]
mov ecx, dword ptr [ebp-08h]
add ecx, dword ptr [ebp-04h]
xor edx, ecx
mov eax, dword ptr [ebp-08h]
shr eax, 05h
mov ecx, dword ptr [ebp+0Ch]
add eax, dword ptr [ecx+0Ch]
xor edx, eax
mov eax, dword ptr [ebp-0Ch]
sub eax, edx
mov dword ptr [ebp-0Ch], eax
mov ecx, dword ptr [ebp-0Ch]
shl ecx, 04h
mov edx, dword ptr [ebp+0Ch]
add ecx, dword ptr [edx]
mov eax, dword ptr [ebp-0Ch]
add eax, dword ptr [ebp-04h]
xor ecx, eax
mov edx, dword ptr [ebp-0Ch]
shr edx, 05h
mov eax, dword ptr [ebp+0Ch]
add edx, dword ptr [eax+04h]
xor ecx, edx
mov edx, dword ptr [ebp-08h]
sub edx, ecx
mov dword ptr [ebp-08h], edx
mov eax, dword ptr [ebp-04h]
sub eax, 9E3779B9h
mov dword ptr [ebp-04h], eax
jmp jmp1
jmp2:
mov ecx, dword ptr [ebp+08h]
mov edx, dword ptr [ebp-08h]
mov dword ptr [ecx], edx
mov eax, dword ptr [ebp+08h]
mov ecx, dword ptr [ebp-0Ch]
mov dword ptr [eax+04h], ecx
mov esp, ebp
pop ebp
ret- }
}
__declspec(naked) void Decrypt(char *Name, int NameLen, DWORD *XorValue)
{
__asm {
push ebp
mov ebp, esp
sub esp, 14h
push ebx
push esi
push edi
mov eax, [ebp+0Ch]
shr eax, 03h
mov [ebp-10h], eax
mov eax, [ebp+0Ch]
xor edx, edx
mov ecx, 08h
div ecx
mov [ebp-0Ch], edx
cmp dword ptr [ebp-0Ch], 0h
jz jmp1
mov edx, [ebp+0Ch]
mov eax, [ebp+08h]
lea ecx, [edx+eax-08h]
mov [ebp-14h], ecx
mov edx, [ebp+10h]
push edx
mov eax, [ebp-14h]
push eax
call Decr
add esp, 08h
jmp5:
mov ecx, [ebp+08h]
mov [ebp-14h], ecx
mov dword ptr [ebp-04h], 1437A1E9h
mov dword ptr [ebp-08h], 0h
jmp jmp2 //1002A031
jmp4:
mov edx, [ebp-08h]
add edx, 01h
mov [ebp-08h], edx
jmp2:
mov eax, [ebp-08h]
cmp eax, [ebp-10h]
jae jmp3 //1002A196-
mov ecx, [ebp-14h]
mov edx, [ecx]
xor edx, [ebp-04h]
mov eax, [ebp-14h]
mov [eax], edx
mov ecx, [ebp-04h]
imul ecx, ecx, 0E32A79DCh
add ecx, 274B0EFAh
mov [ebp-04], ecx
mov edx, [ebp-14h]
mov eax, [edx+04h]
xor eax, [ebp-04h]
mov ecx, [ebp-14h]
mov [ecx+04h], eax
mov edx, [ebp-04h]
imul edx, edx, 0E32A79DCh
add edx, 292881EDh
mov [ebp-04h], edx
mov eax, [ebp+10h]
push eax
mov ecx, [ebp-14h]
push ecx
call Decr
add esp, 08h
mov edx, [ebp-14h]
add edx, 08h
mov [ebp-14h], edx
jmp jmp4
jmp1:
jmp jmp5
jmp3:
mov ax, 01h
pop edi
pop esi
pop ebx
add esp, 14h
mov esp, ebp
pop ebp
ret
}
}
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;
}
}
int main()
{
DWORD XorValue[] = {0x9EE3E155, 0x76AD7FF7, 0x2A2A7F59, 0xDACE2F29};
FILE *f = fopen("crack.exe", "r+b");
FILE *f2 = fopen("crack2.exe", "r+b");
fseek(f, 0xE5BB0, SEEK_SET);
fseek(f2, 0xE5BB0, SEEK_SET);
char Buffer[150];
memset(Buffer, 0, 150);
while(1)
{
int nlen = 0;
while(1)
{
nlen = fgetc(f);
if(nlen == 0)
continue;
if(nlen == 0x4B)
{
fclose(f);
fclose(f2);
return 0;
}
break;
}
fgetc(f);
fread(Buffer, nlen, 1, f);
if(nlen < 8)
SimpleDecrypt(Buffer, nlen);
else
Decrypt(Buffer, nlen, XorValue);
fputc('\0', f2);
fputc('\0', f2);
fwrite(Buffer, nlen, 1, f2);
memset(Buffer, 0, 150);
}
fclose(f);
fclose(f2);
printf("All functions decrypted... enjoy :)\n");
return 0;
}
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:
00000000 3A F5 44 00 B4 48 F7 BF EC 20 4E 00 00 00 00 00 :.D..H... N.....
00000010 55 F5 44 00 3D 6E F7 BF E0 20 4E 00 00 00 00 00 U.D.=n... N.....
00000020 80 F5 44 00 DB 7A F7 BF 40 20 4E 00 00 00 00 00 ..D..z..@ N.....
00000030 A5 F5 44 00 DB 7A F7 BF D4 20 4E 00 00 00 00 00 ..D..z... N.....
00000040 F8 F5 44 00 F0 FF F7 BF CC 20 4E 00 00 00 00 00 ..D...... N.....
00000050 2B F6 44 00 39 70 F7 BF D0 20 4E 00 00 00 00 00 +.D.9p... N.....
00000060 3E F6 44 00 83 0B FA BF D4 20 4E 00 00 00 00 00 >.D...... N.....
00000070 75 F6 44 00 CB 41 F8 BF 70 21 4E 00 00 00 00 00 u.D..A..p!N.....
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:
000E2000 D3 A9 EC 01 44 16 E8 BF 69 B0 EC 01 B4 B3 EC 01 ....D...i.......
000E2010 FF B6 EC 01 4A BA EC 01 00 00 00 00 72 A4 B7 BF ....J.......r...
000E2020 00 00 00 00 60 B0 00 70 00 00 00 00 8E 61 0E 00 ....`..p....7...
000E2030 7E 61 0E 00 00 00 00 00 77 03 EB 01 C2 06 EB 01 ........w.......
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):
Advapi32: IAT: 000E2000
RegCreateKeyExA: 000E61EC
RegCloseKey: 000E61AA
RegQueryValueEx: 000E61B8
RegOpenKeyA: 000E61FE
RegSetValueExA: 000E610B
RegOpenKeyExA: 000E61CB
Ole32: IAT: 000E2284
CoCreateInstance: 000E623C
CoUnInitialize: 000E622B
CoInitialize: 000E621C
Ok, a questo punto abbiamo sistemato quasi tutto, comunque per evitare
equivoci, ecco come apparirà la IAT dopo il fixing:
IAT per Advapi32 e Gdi32:
000E2000 EC 61 0E 00 AA 61 0E 00 B8 61 0E 00 FE 61 0E 00 .a...a...a...a..
000E2010 DB 61 0E 00 CB 61 0E 00 00 00 00 00 38 59 0E 00 .a...a......8Y..
000E2020 00 00 00 00 86 62 0E 00 00 00 00 00 8E 61 0E 00 .....b.......a..
000E2030 7E 61 0E 00 00 00 00 00 ~a......
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:
struct fixs
{
DWORD caller;
DWORD api_called;
DWORD iat_addr;
DWORD pad;
};
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:
struct iatinfo
{
DWORD api_called;
DWORD iat_addr;
DWORD caller;
};
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;
};
struct iatinfo
{
DWORD api_called;
DWORD iat_addr;
DWORD caller;
};
bool fix(char *FileName)
{
HANDLE hFile = NULL;
HANDLE hMappedFile = NULL;
unsigned char *lpFileBase = NULL;
unsigned char *pTmpPtr = NULL;
unsigned char *pTmpPtr2 = NULL;
WORD istr = 0;
DWORD arg = 0;
DWORD iat_addr = 0;
int k32napi = 0;
int u32napi = 0;
FILE *f = NULL;
fixs fix;
vector<iatinfo> k32apis; // array che
contiene le funzioni per kernel32
vector<iatinfo> u32apis; // array che
contiene le funzioni per user32
hFile = CreateFile(FileName, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,-
NULL);
if(hFile == INVALID_HANDLE_VALUE)
return false;
hMappedFile = CreateFileMapping(hFile, NULL, PAGE_READWRITE,
0, 0, NULL); // come vedete ho deciso di usare un mmf
if(hMappedFile == INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
return false;
}
lpFileBase = (unsigned char *)MapViewOfFile(hMappedFile,
FILE_MAP_WRITE, 0, 0, 0);
if(!lpFileBase)
{
CloseHandle(hMappedFile);
CloseHandle(hFile);
return false;
}
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;
}
}
if(!isEqual)
{
info.api_called = fix.api_called;
info.iat_addr = k32iat;
info.caller = fix.caller;
k32iat += 4;
k32apis.push_back(info);
k32napi++;-
}
}
}
fclose(f);
- //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;
pTmpPtr = lpFileBase;
pTmpPtr2 = lpFileBase;
pTmpPtr += (k32apis[i].caller -
0x00400000); //pTmpPtr punterà all'offset del caller
address
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--;
__asm {
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = k32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
case 0xD7FF:
{
while(istr != 0x3D8B)
{
pTmpPtr--;
__asm {
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = k32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
case 0xD3FF:
{
while(istr != 0x1D8B)
{
pTmpPtr--;
__asm {-
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = k32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
case 0xD5FF:
{
while(istr != 0x2D8B)
{
pTmpPtr--;
__asm {
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = k32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
}
}
//ripetiamo il tutto per user32
for(i = 0; i < u32napi; i++)
{
WORD istr;
DWORD operand;
pTmpPtr = lpFileBase;
pTmpPtr2 = lpFileBase;
pTmpPtr += (u32apis[i].caller -
0x00400000);
pTmpPtr2 += u32apis[i].iat_addr;
DWORD addr = u32apis[i].api_called;
__asm {
mov esi,
dword ptr [pTmpPtr2]
mov eax, addr
mov dword ptr
[esi], eax
}
__asm {
mov esi,
dword ptr [pTmpPtr]
mov ax, word
ptr [esi]
mov istr, ax
}
switch(istr)
{
case 0x15FF:
case 0x25FF:
{
pTmpPtr += 2;
operand = u32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
case 0xD6FF:
{
while(istr != 0x358B)
{
pTmpPtr--;
__asm {
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = u32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
case 0xD7FF:
{
while(istr != 0x3D8B)
{
pTmpPtr--;
__asm {
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = u32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
case 0xD3FF:
{
while(istr != 0x1D8B)
{
pTmpPtr--;
__asm {
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = u32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
case 0xD5FF:
{
while(istr != 0x2D8B)
{-
pTmpPtr--;
__asm {
mov esi, dword ptr [pTmpPtr]
mov ax, word ptr [esi]
mov istr, ax
}
}
pTmpPtr += 2;
operand = u32apis[i].iat_addr + 0x00400000;
__asm {
mov esi, dword ptr [pTmpPtr]
mov eax, operand
mov dword ptr [esi], eax
}
}
break;
}
}
UnmapViewOfFile((LPVOID)lpFileBase);
CloseHandle(hMappedFile);
CloseHandle(hFile);
return true;
}
int main()
{
fix("crack.exe");
printf("File fixed... enjoy! :)\n");
return 0;
}-
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:
004DB7FA, 004DC494, 004743B0, 0048790E, 004D8182, 0044FC5D, 0046C602,
00475632, 0048A6F3,
004DDC83, 00473900, 004736F0, 00474520, 00450207, 0047844A, 00450981,
004DA141, 004DF0CF,
0045A5BD, 0047633B, 00473B30, 0046FFB8, 00474860, 0046FE8F, 0046BB84,
0046F4A0, 0045099E,
00472A90, 004757F0, 00473743, 004722D2, 004746D0, 0046FE9C
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:
004DB7FA --> GetStartupInfoA
004DC494 --> MultiByteToWideChar
004743B0 --> GetCurrentThreadId
0048790E --> InitializeCriticalSection
004D8182 --> GetCommandLineA
0044FC5D --> SetEvent
0046C602 --> DeleteCriticalSection
00475632 --> GetCurrentThreadId
0048A6F3 --> GetTickCount
004DDC83 --> GetModuleFileNameA
00473900 --> GetCurrentThreadId
004736F0 --> GetCurrentThreadId
00474520 --> GetCurrentThreadId
00450207 --> CreateFileA
0047844A --> EndPaint
00450981 --> GetActiveWindow
004DA141 --> VirtualFree
004DF0CF --> GetStringTypeA
0045A5BD --> GetTickCount
0047633B --> GlobalAlloc
00473B30 --> GetCurrentThreadId
0046FFB8 --> QueryPerformanceCounter
00474860 --> GetCurrentThreadId
0046FE8F --> SetThreadPriority
0046BB84 --> FreeLibrary
0046F4A0 --> GetCurrentThreadId
0045099E --> DialogBoxParamA
00472A90 --> GetCurrentThreadId
004757F0 --> GetCurrentThreadId
00473743 --> GetCurrentThreadId
004722D2 --> GetCurrentThreadId
004746D0 --> GetCurrentThreadId
0046FE9C --> SetThreadPriority
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:
GetCommandLineA --> Indirizzo: 000E64D8, IAT: 000E217C
SetEvent --> Indirizzo: 000E5C86, IAT: 000E2180
EndPaint --> Indirizzo: 000E60DC, IAT: 000E2214
DialogBoxParamA --> Indirizzo: 000E5F4F, IAT: 000E2218
Ecco tutti gli indirizzi delle varie api:
GetStartupInfoA: 004E2108
MultiByteToWideChar: 004E2134
GetCurrentThreadId: 0046FE40
InitializeCriticalSection: 004E206C
GetCommandLineA: 004E217C
SetEvent: 004E218D
DeleteCriticalSection: 004E2090
GetTickCount: 0045AAC0
GetModuleFileNameA: 004E2110
CreateFileA: 004E2040
EndPaint: 004E2214
GetActiveWindow: 004E219C
VirtualFree: 004E2050
GetStringType: 004E2158
GlobalAlloc: 004E2038
QueryPerformanceCounter: 004E20D4
SetThreadPriority: 004E2080
FreeLibrary: 004E20E0
DialogBoxParamA: 004E2218
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
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
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.
Capitoooooooo????? Bhè credo di si ;))))