Subito ho pensato che il programma
in questione contenesse del codice automodificante (SMC) o avesse utilizzato
la funzione di encripting; questo perchè a prima vista il codice
modificato sembrava parecchio guardando il disassemblato.
Guardando con più attenzione invece, il codice realmente modificato
è di 2 byte, all'indirizzo 0158201; sono questi due byte che stravolgono
il disasm successivo.
Ho così deciso di guardare il codice originale, per capire da che
cosa potesse dipendere tale modifica.
Ecco come si presenta:
...
...
:200051F6 BFD0000000 mov
edi, 000000D0
:200051FB EB0A
jmp 20005207
:200051FD C7054C75012005000000 mov dword
ptr [2001754C], 00000005
:20005207
8BCF mov
ecx, edi
:20005209 8BBC2444010000 mov edi, dword
ptr [esp+00000144]
:20005210 8BD1 mov
edx, ecx
:20005212 8D742430 lea
esi, dword ptr [esp+30]
:20005216 C1E902 shr
ecx, 02
:20005219 F3 repz
:2000521A A5
movsd
:2000521B 8BCA mov
ecx, edx
:2000521D 83E103 and
ecx, 00000003
:20005220 F3 repz
:20005221 A4 movsb
:20005222 E839610000 call
2000B360
:20005227 E9DE000000 jmp
2000530A
...
...
Uhm uhm, un indirizzo fisso....
Azz, la rilocazione!!!!!!!
La rilocazione può avviene quando una DLL o un EXE devono essere
caricati (mappati) in memoria.
In realtà è molto difficile che avvenga la rilocazione per
un file exe ma è frequente invece nelle DLL.
Ogni file ha un indirizzo di partenza prefissato dove dovrebbe essere caricato,
chiamato BASE ADDRESS. Se però tale spazio in memoria è già
occupato, il codice viene caricato in un'altra locazione.
Questo non comporta alcun problema per il codice che utilizza riferimenti
relativi, ma genera invece errori per gli indirizzi assoluti. Mi spiego
meglio:
prendiamo come esempio il codice sopra all'indirizzo 20005227:
:20005227 E9DE000000 jmp 2000530A
In realtà questo salto non è un salto assoluto all'indirizzo
"2000530A" ma è un salto relativo del tipo "jmp +
0DE byte in avanti". Infatti i byte di tale istruzione indicano "E9"
il jump e "DE000000" di quanto deve avvenire il salto: 20005227
+ DE + "byte occupati dall'istruzione (5)" uguale appunto 200530A.
Se tutto il codice viene spostato non cambia nulla perchè il salto
avviene sempre n-byte più avanti a partire dal nuovo indirizzo.
L'istruzione all'indirizzo 200051FD invece non fa riferimento a indirizzi
n-byte più avanti o più indietro rispetto alla posizione corrente
ma indica in modo assoluto l'indirizzo di riferimento; all'interno del codice
troviamo infatti l'indirizzo scritto per esteso.
:200051FD C7054C75012005000000
mov dword ptr [2001754C], 00000005
Ipotizziamo che all'indirizzo 2001754C ci sia la stringa 'pippo'.
Se tutto il codice viene spostato ad un altro indirizzo (ad esempio 0158xxxx
invece che 2000xxxx), anche la stringa 'pippo' verrà spostata, più
precisamente al nuovo indirizzo 01581754C.
Cosa succede in questo caso? Questa istruzione fa ancora riferimento all'indirizzo
originale 2001754C, che contiene ora altri dati.
Per correggere queste istruzioni interviene appunto la rilocazione che sostituisce
a tutti questi indirizzi assoluti il nuovo indirizzo assoluto ricalcolato.
Nel nostro caso l'indirizzo a 2001754c dovrà diventare 0159754c quindi
la rilocazione fa questo tipo di calcolo:
BASE ADDRESS originale = 20000000
BASE ADDRESS corrente = 01580000
DIFFERENZA =
1EA80000
e sottrarrà a tutti gli indirizzi assoluti questa cifra.
Per questo motivo il codice che avevo scritto all'indirizzo 200051ff è
stato modificato:
:200051FB C70300000000
mov dword ptr [ebx], 00000000
:20005201 66C743040400 mov
[ebx+04], 0004
C7660000 - 1EA80000 = A8BE0000 che è appunto il nuovo valore
che ci troviamo a run-time:
:015851FB C70300000000
mov dword ptr [ebx], 00000000
:01585201 BEA8430404 mov
esi,040443A8
Cosa fare allora per essere sicuri che le modifiche al codice nelle DLL
non vengano "corrotte" dalla rilocazione?
Ci sono due soluzioni: la prima meno raffinata consiste nello scrivere il
codice solo dove la rilocazione non avviene e unire le varie parti di codice
con dei jump:
nostro
codice
nostro
codice
nostro
codice
jump prox
codice originale soggetto
a rilocazione
codice originale soggetto
a rilocazione
codice originale soggetto
a rilocazione
prox: nostro
codice
nostro codice
...
Questo comporta un'analisi del codice originario per vedere in quali
istruzioni ci sia un riferimento assoluto in modo da evitarle.
La seconda (la migliore a mio avviso) è quella di intervenire direttamente
sulla sezione di rilocazione (.reloc) in modo da evitare che la rilocazione
avvenga nel codice scritto da noi; vediamo quindi da vicino la sezione reloc.
La sezione reloc è composta da una serie di strutture dati di questo
tipo a lunghezza variabile:
struct IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress; RVA di partenza
per il blocco di dati corrente
DWORD SizeOfBlock; Grandezza
del blocco corrente
WORD TypeOffset(); Array
di indirizzi che subiscono la rilocazione
}
Ogni struttura descrive la rilocazione per ogni pagina di 4K (quindi 1000h
Byte) e ogni Virtual Address sarà 1000h byte maggiore del precedente.
Il SizeOfBlock dipende dal numero di rilocazioni nel blocco corrente; la
grandezza è data da Size(TypeOffset) + 8 quindi per calcolare il
numero di rilocazioni del blocco basta fare: (SizeOfBlock - 8) / 2
TypeOffset è composto da due parti: i 4 bit più alti corrispondono
al tipo di rilocazione da effettuare, gli altri 12 indicano invece l'offset
di rilocazione (che dev'essere sommato al VirtualAddress del blocco per
ottenere l'offset completo).
Il formato PE prevede solo due tipi di rilocazioni possibili:
* 0 (IMAGE_REL_BASED_ABSOLUTE):
Questo tipo di rilocazione è senza significato ed è usata
per arrotondare la struttura dell' IMAGE_BASE_RELOCATION ad una grandezza
multipla di DWORD.
* 3 (IMAGE_REL_BASED_HIGHLOW):
In questo caso la rilocazione avviene per tutti gli offset puntati dall'RVA
di partenza + i 16 bits inferiori di ogni TypeOffsetRelocation secondo la
formula prima descritta.
Tornando alla dll che ho modificato vediamo la sezione di rilocazione:
PEditor mi dice che:
l'image base del mio file è 20000000.
la sezione di rilocazione inizia a 20000.
.reloc
...
20000:00100000 D8000000 2B30 3430 9F30 A530 C130 D530 2531
...
...
La prima dword ci dice che l'RVA di partenza è 1000h; la seconda
dword che il blocco è grande D8h byte.
Tutte le altre WORD ci dicono invece il tipo e l'offset di rilocazione;
prendiamo come esempio la prima WORD:
2B30 ==> 302B. I primi 4 bit (3) ci indicano il tipo di rilocazione;
gli altri 12 (02B) l'offset.
Quindi all'indirizzo "Image Base + RVA + Offset" (20000000 + 1000
+ 2B=2000102B) avverrà la prima rilocazione.
Le nostre modifiche vanno dall'indirizzo 20005110
all' indirizzo 20005224. Il blocco che ci interessa è quindi quello
con RVA 5000.
...
20328:
00500000 94000000 0B30 5B30 8F30 9430 B330 CE30
8C31 FF31 4F32 8B32 9032 C132 F232 6A33
9633 D533 0934 0F34 2534 3A34 D534 E134 F434 EB35 1C36 2136 3D36 6536 D636
EA36 0D37 4137 4737 6937 8737 B937 2438 2E38 E238 1339 1A39 4A39 4F39 8139
9F39 C439 C839 CC39 D039 B73A 013B 3E3B 7F3B 893B 8F3B B53B BD3B 023C 443C
7F3C D23C 0C3D 663D AE3D 123E 753E F23E BD3F C33F 0000
00600000
Nel blocco che va da 5000 a 6000 vengono fatte (94h-8)/2 rilocazioni ovvero
70.
Le nostre modifiche però vanno da 5110 a 5224 quindi cerchiamo di
individuare gli offset che ci interessano; in questo caso due: (3)18C e
(3)1FF. Da notare gli 0000 finali che servono a rendere il blocco multiplo
di DWORD.
Andiamo a vedere nel codice originale gli indirizzi 2000518C e 200051FF.
...
...
:20005184 0F8480010000 je
2000530A
:2000518A F6054875012004 test
byte ptr [20017548], 04
:20005191 0F8495000000 je 2000522C
...
...
:200051FD C7054C75012005000000
mov dword ptr [2001754C], 00000005
:20005207
8BCF mov
ecx, edi
...
...
Come ci si aspettava, due indirizzi assoluti.
Siamo arrivati alla fine, manca solo correggere la sezione reloc. Come fare?
Beh, ci sono due possibilità.
1) Abbiamo scritto il codice fino a 20005224 dove facciamo il ret dalla
funzione, perciò il codice successivo non viene mai eseguito. Pertanto
anche se avviene la rilocazione dopo questo indirizzo non ci interessa.
Facendo diventare la WORD 8C31 ==>2A32 (2000522A) e la WORD FF31 ==>2A32
(2000522A) verrebbe rilocato tale indirizzo 2 volte ma inutilmente perchè
il codice a quell'offset non viene eseguito.
2) Facciamo uso del secondo tipo di rilocazione (0), che in realtà
non è una rilocazione vera e propria ma è paragonabile ai
nop nel codice ;) In questo modo gli faremo saltare gli offset utilizzati
da noi. Le due WORD possono essere scritte come 8C31 ==>8C01 e FF31 ==>FF01
o semplicemente 0000 e 0000, il risultato ottenuto è lo stesso.
Fate come più vi piace, l'importante è evitare la rilocazione.