Minima Anonyma Tabularia Ex Reti
nihil vacuum neque sine signo apud MATER
Questa pubblicazione contiene
informazioni e codice sorgente relativi alla sicurezza informatica. Lo
scopo di queste informazioni è di aiutare gli utenti ad accrescere la propria
capacità di programmazione. Questo materiale è a puro scopo didattico e non
include codice distruttivo né documenti attinenti ad alcuna attività illegale.
Si declina ogni responsabilità nel caso chiunque utilizzi le suddette
informazioni per creare, compilare e diffondere intenzionalmente programmi
diretti a danneggiare o interrompere un sistema informatico o telematico,
ovvero a provocarne l'interruzione, totale o parziale, o l'alterazione del suo
funzionamento Art. 615-quinquies (Legge n. 547/93 pubblicata in Gazzetta Ufficiale
n. 305 del 30.12.1993)
§ MATER
Liber Quartus §
indice
1.
API
Uno dei
cambiamenti più bruschi è tuttavia il passaggio dalle chiamate alle funzioni
del buon vecchio INT21 alle API (Application Program Interface). Cosa sono le API
? Sono delle funzioni che, come dice il loro nome, provvedono a fare un po’ di
tutto. Parlare di API è un po’ come stuzzicare un balena: se cominciassi a
farlo dovrei scrivere un’enciclopedia. Le API sono centinaia. Tanto per darvene
un’idea, ecco le API che trattano le operazioni sui files:
_hread
_hwrite
_lclose
_lcreat
_llseek
_lopen
_lread
_lwrite
AreFileApisANSI
CopyFile
CreateDirectory
CreateDirectoryEx
CreateFile
CreateIoCompletionPort
DefineDosDevice
DeleteFile
FileIOCompletionRoutine
FindClose
FindCloseChangeNotification
FindFirstChangeNotification
FindFirstFile
FindNextChangeNotification
FindNextFile
FlushFileBuffers
GetBinaryType
GetCurrentDirectory
GetDiskFreeSpace
GetDriveType
GetFileAttributes
GetFileInformationByHandle
GetFileSize
GetFileType
GetFullPathName
GetLogicalDrives
GetLogicalDriveStrings
GetQueuedCompletionStatus
GetShortPathName
GetTempDrive
GetTempFileName
GetTempPath
GetVolumeInformation
LockFile
LockFileEx
MoveFile
MoveFileEx
OpenFile
QueryDosDevice
ReadFile
ReadFileEx
RemoveDirectory
SearchPath
SetCurrentDirectory
SetEndOfFile
SetFileApisToANSI
SetFileApisToOEM
SetFileAttributes
SetFilePointer
SetHandleCount
SetVolumeLabel
UnlockFile
UnlockFileEx
WriteFile
WriteFileEx
Come potete vedere
siamo ad anni luce di distanza dalle operazioni che eravamo abituati a vedere
con il vecchio INT21. Con questo non voglio dire che il sistema degli interrupt
è sparito. Tutt’altro. Ne avremo modo di riparlarne. Vorrei solo farvi capire
che, se vogliamo studiare del codice che lavora in ambiente win32, dobbiamo
fare i conti con queste benedette API (consiglio vivamente di reperire l’help
ufficiale API, circa 8 Mbytes, ma ne vale la pena).
Ora un tipico
esempio, che rimane sempre un classico. Proviamo a scrivere il programma
"Hello world!" .
E’ molto
semplice. Useremo la funzione API "MessageBoxA" , definita con il
comando "extrn" command, inserire i parametri e chiamare la funzione
stessa Le stringhe del messaggio devono essere ASCIIZ (ASCII,0) e che i
parametri vanno inseriti in ordine inverso:
.386 ; Processore
(386+)
.model
flat ; Usiamo
registri a 32 bit
extrn ExitProcess:proc
; le API che usiamo
extrn MessageBoxA:proc
Con la direttiva "extrn" indichiamo
l’API che useremo nel programma. ExitProcess è una API usata per ritornare il
controllo al sistema operativo. MessageBoxA è usata per emettere un messaggio
sullo schermo.
.data
szMessage db "Hello
World!",0 ; Messaggio
per MsgBox
szTitle db "in
ambiente Win32",0 ; Titolo
del MsgBox
(Ricordiamo che
TASM non assembla se non inseriamo almeno un dato)
.code ; da qui il codice
HelloWorld:
push 00000000h ; Stile della MessageBox
push offset szTitle ; Titolo del MessageBox
push offset szMessage
; Il messaggio
push 00000000h
; Handle proprietario
call MessageBoxA
; API
I campi in
entrata delle funzioni sono:
int MessageBox(
HWND hWnd, // handle
of owner window
LPCTSTR lpText, //
address of text in message box
LPCTSTR lpCaption, // address
of title of message box
UINT uType //
style of message box
);
push 00000000h
call ExitProcess
e:
VOID ExitProcess(
UINT uExitCode // exit code
for all threads
);
end HelloWorld
3. Rings
Il processore ha
4 livelli di privilegi: Ring-0, Ring-1, Ring-2 and Ring-3, l’ultimo dei quali,
il livello utente, è il più restrittivo. Ring-0 è il paradiso del virus coder,
essendo quello che permette la massima libertà di azione. Il DOS, con i suoi
interrupt, era Ring-0.
Dunque Ring-3 è
lo"user level”. La maggior parte dei virus per Win32, per ora, utilizzano
Ring-3. Ring-0 è il livello che il kernel usa per il suo codice, il
"kernel level”. Qui il programmatore può accedere alle porte ed al altre
cosine. a Ring-0 si può accedere usando uno dei trucchi attualmente conosciuti,
come la IDT modification, la tecnica
"Call Gate" che SoPinKy/29A mostra
29A#3, o il VMM inserting, tecnica usata nel virus Padania o in Fuck
Harry. Qui i programmatori non hanno bisogno delle API, dato che utilizzano i servizi
VxD. Studieremo, più oltre, queste tecniche.
Selettori
Che cos’è un
selettore ? E’ un segmento molto grande, che forma la memoria di Win32,
chiamata anche flat memory. Qui
possiamo indirizzare 4 Gigabytes di memoria (4.294.967.295 bytes), solo usando
offsets di 32 bit. Vediamo come è organizzata questa memoria:
-------------------------------------------
OFFSET = 00000000h <-> 3FFFFFFFh
Application Code And Data
-------------------------------------------
OFFSET = 40000000h <-> 7FFFFFFFh
Shared Memory
-------------------------------------------
OFFSET = 80000000h <-> BFFFFFFFh
Kernel
------------------------------------------
OFFSET = C0000000h <-> FFFFFFFFh
Device Driverz
------------------------------------------ Risultato: abbiamo 4
Giga di memoria utilizzabile.
NOTA: WinNT tiene
le ultime due sezioni separate dalle prime due.
VA:
sta per indirizzo
virtuale (Virtual Address). E’ l’indirizzo
di un oggetto in memoria (ricordiamo che in Windows gli oggetti non sono
esattamente uguali in memoria e su disco).
RVA:
sta per indirizzo virtuale relativo (Relative
Virtual Address). E’ l’ offset di
qualcosa relativo a dove il file è mappato in memoria (is memory- mapped) (dal
sistema o dall’utente).
RAW Data:
è il nome che
useremo per i dati fisicamente presenti su disco e come esattamente si
presentano su disco (dati disco != dati ram).
Virtual Data:
è il nome che diamo
ai dati quando vengono caricati in memoria dal loader del sistema.
File Mapping:
Tecnica, implementata in tutti gli ambienti
Win32, che consiste nella manipolazione dei files più veloce ed economica.
Tutto quello che modifichiamo in memoria viene automaticamente anche modificato
su disco. Il File Mapping è anche il solo modo er scambiare informazioni tra
processi che lavorano in un ambiente Win32.
Quando eseguiamo
un’applicazione, il codice è chiamato da una parte del codice del KERNEL32 (per
la verità è come se il KERNEL facesse una CALL al nostro codice). Quando viene
effettuata una chiamata, l’indirizzo di ritorno è deposto nello stack (che in
memoria è ESP). Vediamo un esempio pratico:
.586p
.model flat
.data ; necessario per TASM32/TLINK32
db ?
.code
start:
mov eax,[esp] ; ora EAX dovrebbe essere
BFF8XXXXh (se w9X)
; da qualche parte l’API
CreateProcess
ret ; Return
end start
Noi abbiamo in
EAX un valore del tipo BFF8XXXX (XXXX non è importante. La piattaforma Win32
usualmente arrotonda superiormente tutto ad una pagina, così noi possiamo
cercare all’inizio di ogni pagina, così l’header KERNEL32 è all’inizio di una
pagina, noi possiamo facilmente verificarlo. Quando troviamo l’header PE, noi
conosciamo l’indirizzo di base del KERNEL32. Come limite dovremmo stabilire 50h
pagine
.586p
.model flat
extrn ExitProcess:PROC
.data
limit
equ 5
db 0
;------------------------------------
; dati
non utilizzati
;------------------------------------
.code
test:
call delta
delta:
pop ebp
sub ebp,offset delta
mov esi,[esp]
and
esi,0FFFF0000h
call GetK32
push 00000000h
call ExitProcess
Il primo blocco di
istruzioni è per ricavare il delta offset (già spiegato in T_4). Nel secondo blocco noi mettiamo in
ESI l’indirizzo dal quale la nostra applicazione è chiamata, che è l’indirizzo
mostrato da ESP (se non tocchiamo lo stack dopo aver caricato il programma). La
seconda istruzione AND è per ricavare l’inizio della pagina da cui il nostro
codice è stato chiamato. Noi chiamiamo la nostra routine, quindi terminiamo il
processo.
GetK32:
__1:
cmp byte ptr [ebp+K32_Limit],00h
; oltrepassiamo i limiti?
jz
WeFailed
cmp word ptr
[esi],"ZM" ; ZM
file ?
jz CheckPE ; Ok passa oltre
__2:
sub esi,10000h ; togliamo 10 pagine
dec byte
ptr [ebp+K32_Limit]
jmp __1
Per prima cosa
verifichiamo se oltrepassiamo il nostro limite (50 pagine). Poi controlliamo se all’inizio della pagina (come
dovrebbe essere) ci sono i caratteri MZ. Se ci sono andiamo a controllare se
siamo in un header PE. Altrimenti sottraiamo 10 pagine (10000h bytes),
decrementiamo la variabile del limite e cerchiamo di nuovo.
CheckPE:
mov edi,[esi+3Ch]
add edi,esi
cmp dword ptr [edi],"EP"
jz WeGotK32
jmp __2
WeFailed:
mov esi,0BFF70000h
WeGotK32:
xchg eax,esi
ret
K32_Limit dw
limit
Leggiamo il valore all’offset 3Ch dall’header MZ (prendendo l’indirizzo RVA da dove comincia
l’header PE), normalizziamo questo
valore con l’indirizzo della pagina e se a questo indirizzo troviamo i
caratteri PE, abbiamo trovato quello che cerchiamo.
end test
Quando indichiamo
una funzione API come extern il file import32.lib ci fornisce l’indirizzo della
funzione ed essa è assemblata nel codice. Tuttavia c’è un problema quando
scriviamo un programma che debba utilizzare tali API. Se noi
“hard-codifichiamo” (hardcode) – nome che diamo quando usiamo un indirizzo
assoluto per chiamare una API – probabilmente accadrà che tale indirizzo non
lavorerà nella prossima versione di Win32.
Cosa potremmo
fare ? Esiste una funzione, GetProcAddress, che ritorna l’offset dell’API
desiderata. Purtroppo GetProcAddress è un’API a sua volta, e quindi non
possiamo ricavare il suo indirizzo corretto.
Ci sono varie
possibilità. ne elenchiamo due:
1. Cercare GetProcAddress nella tabella di
Exports;
2. Quando infettiamo un file, cercare nelle
funzioni importate GetProcAddress.
Il metodo più
facile è la seconda, e sarà quello che illustreremo.
Se diamo
un’occhiata al formato dell’header PE, troviamo all’offset 78h (dell’header)
l’RVA della tabella di exports. Noi abbiamo bisogno di prendere l’indirizzo
della tabella exports del kernel. Per Windows 95/98, il kernel è all’offset
0BFF70000h, e in Windows NT il kernel sembra essere a 077F00000h.
In Win2k lo troviamo all’offset 077E00000h.
Così, prima di tutto dobbiamo caricare il suo indirizzo nel registro che utilizzeremo
come puntatore. Raccomandiamo di usare ESI, perché possiamo ottimizzare del
codice con l’istruzione LODSD. Quindi controlliamo se nell’indirizzo puntato abbiamo l’usuale “MZ” (ricordiamo che
Intel mantiene i dati rovesciati), dato
che kernel è una libreria DLL, e le librerie hanno l’header PE. Dopodiché
cerchiamo all’offset image_base+[3Ch] (= offset dove il the kernel
è locato + indirizzo contenuto dall’header PE del KERNEL' a 3Ch), ed
effettuiamo il controllo dei caratteri "PE\0\0", (PE signature).
Se tutto è a
posto, procediamo. Abbiamo bisogno dell’RVA della tabella degli export. Lo
troviamo all’offset 78h del PE header. Lo prendiamo ma, come sappiamo, l’RVA
(Relative Virtual Address), come indica il nome, è un indirizzo relativo ad un offset,
in questo caso l’image base del kernel.
Per “normalizzare” l’indirizzo RVA sommiamo l’offset del kernel al valore
trovato del RVA. In questa maniera siamo nella tabella degli export.
Diamogli
un’occhiata:
-------------------------------------------
+00000000h
Export Flags Size : 1 DWORD
-------------------------------------------
+00000004h
Time/Date stamp
Size : 1 WORD
-------------------------------------------
+00000006h
Major version Size : 1 WORD
-------------------------------------------
+00000008h
Minor version Size : 1 DWORD
-------------------------------------------
+0000000Ch
Name RVA Size : 1 DWORD
-------------------------------------------
+00000010h
Number Of Exported Functions
Size : 1 DWORD
-------------------------------------------
+00000014h
Number Of Exported Names
Size : 1 DWORD
-------------------------------------------+00000018h
Export Address Table RVA
Size : 1 DWORD
-------------------------------------------
+0000001Ch
Export Name Pointers Table RVA
Size : 1 DWORD
--------------------------------------------
+00000020h
Export Ordinals RVA
Size : 1 DWORD
--------------------------------------------
Totale 24h BYTES
Per noi sono importanti
gli ultimi sei campi. I valori di Address Table RVA, Name Pointers RVA e
Ordinals RVA sono relativi all’indirizzo della base del KERNEL32. Così, il
primo passo per ottenere un indirizzo API consiste nel conoscere la posizione
che quell’API occupa. La via più semplice per saperlo è di cercare
all’offset indicato da Name Pointers,
confrontare la stringa con il nome dell’API che cerchiamo, e se uguale ne
calcoliamo l’offset.
A questo punto
avremo un valore nel contatore, che dobbiamo incrementare ogni volta che
controlliamo il nome di una API. Questo contatore avrà il numero dell’API
richiesta. Il contatore dovrebbe essere una
word o una dword, ma non un
byte, perché le API sono più di 255 )
NOTA Si assume che si sia salvato nelle
variabili corrispondenti il VA (RVA+kernel image base) dell’indirizzo, del nome
e dell’Ordinal tables.
Immaginiamo di
avere il nome dell’API desiderata, così abbiamo nel contatore la posizione che
essa occupa nella Name Pointers table. Noi prendiamo il valore del contatore e
con esso andiamo a cercare nella Ordinal Table (un array di dwords) l’ordinale della API che vogliamo.
Una volta ottenuto il numero ‘dordine che la API occupa nell’array, dobbiamo
moltiplicare tale valore per 2 (si rammenta che l’array di ordinali è
costituito da words, così noi dobbiamo effettuare il calcolo per lavorare con
words...), e naturalmente aggiungere ad
esso l’offset dell’inizio della tabella degli ordinali (beginning offset).
Per riassumere
quanto è stato esposto, si consideri la seguente formula:
locazione API Ordinale: ( contatore * 2 ) +
Ordinal Table VA
Il passo
successivo consiste nel ricavare
l’indirizzo definitivo della API dalla Address Table. Noi abbiamo già
l’ordinale dell’API. Noi dobbiamo solo moltiplicare l’ordinale per 4 (date che
l’array di indirizzi è formato da dwords invece che words, e la misura di una
dword è 4), e sommare ad esso l’offset dell’inizio della Address Table, come abbiamo fatto prima con la
tabella degi ordinali. Ed infine abbiamo l’RVA dell’API cercata. L’ultima operazione è quella di
normalizzare l’indirizzo ottenuto aggiungendogli offset del kernel, ed è fatta.
indirizzo API: (
API's Ordinale * 4 ) + indirizzo Table VA + KERNEL32 imagebase
---------------------------------------
dato che ricaviamo la posizione che la stringa occupa nella Name Table,
EntryPoint Ordinal Name noi possiamo sapere il suo
ordinale (ogni nome di API ha un
ordinale chel è nella stessa posizione del nome della API), e
00005090
0001 AddAtomA conoscendo l’ordinale possiamo saere
il suo indirizzo e il suo RVA
di
entrypoint. Normalizzaiamo questo valore ed otteniamo quello
00005100
0002 AddAtomW di cui abbiamo bisogno, l’indirizzo
API cercato.
00025540
0003 AddConsoleAliasA
00025500
0004 AddConsoleAliasW
…….. …. …………….
Ed ecco la
routine che esegue tutto questo:
; GetAPI &
GetAPIs
;
; Queste
procedure sono divise in due blocchi: GetAPI ricava il nome di una API, mentre
GetAPIs
; ricava gli indirizzi
di tutte le API che vogliamo.
GetAPI proc
I parametri sono
I seguenti:
INPUT
- ESI : puntatore al nome dell’API
OUTPUT - EAX : indirizzo API
mov edx,esi ; Salvo il ptr al nome API
@_1:
cmp byte ptr [esi],0 ; carattere Null-terminated ?
jz @_2 ; Si, prendiamo
inc esi ; No, continuiamo a cercare
jmp
@_1 ;
@_2:
inc esi ;
sub esi,edx ; ESI = misura nome API
mov ecx,esi ; ECX = ESI
Abbiamo in ESI il
puntatore al nome dell’API. Immaginiamo di cercare
"FindFirstFileA":
FFFA db
"FindFirstFileA",0
^ il puntatore è
qui
Abbiamo bisogno di mantenere questo puntatore
a sapere la misura del nome dell’API, così preserviamo l’iniziale puntatore al nome dell’ API in un
registro come EDX (che non useremo). Quindi incrementiamo il puntatore in ESI
fino a che [ESI] = 0.
FFFA db
"FindFirstFileA",0
^ il puntatore ora è qui
Dopodichè
sottraiamo il vecchio puntatore al nuovo puntatore ottenendo la misura in caratteri
del nome della API (la misura ci servirà successivamente) e lo salviamo in ECX,
(altro registro che non useremo).
xor eax,eax ; EAX = 0
mov word ptr [ebp+Counter],ax ; contatore azzerato
mov
esi,[ebp+kernel] ;
ricavo kernel's PE head. offset
add esi,3Ch ; ‘PE’ offset
lodsw ; da ESI in AX
add eax,[ebp+kernel] ; Normalizziamo ‘PE’ offset
mov esi,[eax+78h] ; ricavo RVA della Export Table
add esi,[ebp+kernel] ; Ptr a indirizzo RVA Address Table
add esi,1Ch
; saltiamo i dati della
struttura
Ricordiamo che
l’istruzione lodsw trasferisce in AX la parola indirizzata da ESI, mentre lodsd
(più sotto) carica una doppia parola in EAX. Prima di tutto puliamo EAX, e
azzeriamo la variabile contatore per non incorrere in errori. Ricordate
l’offset 3Ch di un PE file (contando dall’image base, MZ mark) ?. Noi stiamo
lavorando sulla base dell’header del KERNEL32 PE; dato che esso è un RVA, lo normalizziamo ed così facendo
otteniamo l’offset dell’header PE. Ciò che dobbiamo fare a questo punto è
ricavare l’indirizzo deòòa Export Table address (in PE Header + 78h), e l’indirizzo RVA della Address Table.
lodsd ; EAX = RVA Address Table
add eax,[ebp+kernel] ; Normalizziamo
mov dword
ptr [ebp+AddressTableVA],eax ; Store it in VA form
lodsd
; EAX = RVA Name Ptrz Table
add
eax,[ebp+kernel] ; Normalizziamo
push
eax ;
mov [ebp+NameTableVA],eax
lodsd
; EAX = RVA Ordinal Table
add
eax,[ebp+kernel] ;
Normalizziamo
mov dword ptr [ebp+OrdinalTableVA],eax ; salvo in forma di VA
pop esi ; ESI = VA Name Ptrz Table
Se ricordate, noi
abbiamo in ESI il puntatore al RVA
dell’ Address Table RVA, così per ricavare tale indirizzo eseguiamo un
LODSD, che inserisce la DWORD locata da
ESI nell’accumulatore, che è EAX. dato
che questo è un RVA, dobbiamo normalizzarlo per ottenere il corrispettivo VA
Matt Pietrek dice riguardo a questo
campo: "This field is an RVA and points to an array of function addresses.
The function addresses are the entry points (RVA) for each exported function in
this module".
Naturalmente noi
salviamo tale valore in una variabile.
Dopodiché ricaviamo la Name Pointers Table, della quale Matt
Pietrek dice: "This field is an RVA and points to an array of string
pointers. The strings are the names of
the exported functions in this module".
Ma non lo salvo
in una variabile, bensì nello stack, dato che lo useremo presto.
Finalmente
Matt Pietrek descrive così:: "This
field is an RVA and points to an array of WORDs. The WORDs are the export ordinals of all the exported
functions in this module".
Ed è quello che abbiamo fatto.
@_3:
push esi ; Salvo ESI per
l8r restore
lodsd ; ricavo valore ptr ESI in EAX
add eax,[ebp+kernel] ; Normalizziamo
mov esi,eax ; ESI = VA del nome API
mov edi,edx ; EDI = ptr all’API
push ecx
; ECX = misura API
cld ; Clear direction flag
rep cmpsb ; Confronto i nomi API
pop ecx ; Restore ECX
jz @_4 ; Jump se I nomi sono uguali
pop esi ; Restore ESI
add esi,4 ; prendo il pross. valore dell’array
inc word ptr [ebp+Counter] ; Incremento contatore
jmp @_3 ; vai ancora
Prima di tutto
mettiamo ESI nello stack (dato che sarà
cambiato dall’istruzione CMPSB) per ripristinarlo più tardi. Poi prendiamo la
DWORD puntata da ESI (Name Pointerz Table) e la mettiamo nell’accumulatre
(EAX), attraverso l’istruzione LODSD. Normalizziamo aggiungendo il kernel base
address.
Ora noi abbiamo
in EAX un puntatore al nome di una API, ma no sappiamo ancora di quale API si
tratta. Per esempio EAX potrebbe puntare a qualcosa come
"CreateProcessA" ma tale PAI non ci interessa. Allora confrontiamo
quella stringa con quella che noi cerchiamo (puntata ora da EDX) con CMPSB.
Così prepariamo i suoi parametri: in ESI mettiamo il puntatore all’inizio
dell’API nella Name Pointers Table, e in EDI mettiamo il puntatore all’API
cercata. In ECX mettiamo la misura della stringa, quindi confrontiamo le due
stringhe byte per byte. Se esse sono uguali viene settato il flag zero, ed
effettuiamo un jump alla routine che ci fornirà l’indirizzo di quella API. Se
falliamo ripristiniamo ESI, e aggiungiamo ad esso la misura di una DWORD per
andare a puntare al prossimo valore nella Name Pointers Table array.
Incrementiamo il contatore e continuiamo a cercare.
@_4:
pop esi ; evitando
mondezza nello stack
movzx eax,word ptr [ebp+Counter]
; mettiamo il contatore in AX
shl eax,1 ; EAX = AX * 2
add eax,dword ptr [ebp+OrdinalTableVA] ; Normalizziamo
xor esi,esi ; pulisce ESI
xchg eax,esi
; EAX = 0, ESI = ptr to Ord
lodsw ; metto Ordinale in AX
shl eax,2 ; EAX = AX * 4
add eax,dword ptr [ebp+AddressTableVA] ; Normalizziamo
mov esi,eax ; ESI = ptr all’indirizzo RVA
lodsd ; EAX = indirizzo RVA
add eax,[ebp+kernel] ; Normaliziamo
ret
Il pop è
semplicemente per pulire lo stack, così non sporchiamo il nome dell’API. MOVZX
(move with aero extend: sposta un operando a 8 o a 16 bit in un registro a 16 o
32 bit, inserendo una serie di 0 nella metà superiore della destinazione) Ha
l’effetto di muovere nella porzione bassa di EAX il valore del contatore (che è
una WORD) e azzerare la parte alta del registro. Noi lo moltiplichiamo per 2,
ottenendo il numero che occupa in tabella (l’array dove cerchiamo è un array di
WORD).
Ora aggiungiamo
ad esso l’inizio dell’inizio dell’array da cui vogliamo iniziare la ricerca, e
in EAX abbiamo il puntatore
all’ordinale dell’API che vogliamo. Così mettiamo EAX in ESI per usare quel
puntatore paer ricavare il valore puntato che è l’Ordinale in EAX, con un
semplice LODSW. In questo modo abbiamo
l’Ordinal, ma noi vogliamo l’ EntryPoint del codice dell’API, cos’
moltiplichiamo l’ordinale (che contiene la posizione dell’EntryPoint che l’API
cercata occupa nella Address Table) per 4, dato che è una DWORD, e così facendo
otteniamo un valore RVA, relativo cioè al RVA della AddressTable.
Normalizziamo, così abbiamo in EAX il puntatore al valore della EntryPoint
della API nella Address Table.Mettiamo EAX in ESI, e ricaviamo il valore
puntato in EAX. Così avremo in EAX l’EntryPoint (RVA) dell’API cercata.
L’ultima cosa da fare ora è normalizzare tale indirizzo con l’image base di
KERNEL32.
Ed ecco
l’indirizzo dell’API cercata in EAX
GetAPI
endp
------------------------------------------------------------------------------------------------------------------
GetAPIs proc
Questo è il
codice per ricavare TUTTE le APIs utilizzando la procedura descritta più sopra. i parametri sono:
INPUT
- ESI : Puntatore alla prima API cercata (ASCIIz) – è l’indirizzo
dell’array di stringhe
contenente i nomi
delle API cercate;
EDI : Puntatore alla
variabile che conterrà la prima API – indirizzo dell’array in cui salveremo
gli indirizzi delle
API
OUTPUT – nulla.
La struttura per
ricavare i valori è del tipo:
ESI
punta a db "FindFirstFileA",0
db
"FindNextFileA",0
db
"CloseHandle",0
[...] [...]
db 0BBh ; Marcatore della fine di questo array
EDI
punta a dd 00000000h ; Futuro indirizzo di FindFirstFileA
dd
00000000h ; “ “
FindNextFileA
dd 00000000h ; “
“ CloseHandle
[...] [...]
-------------------------------------------------------------------------------------
@@1: push
esi
push edi
call GetAPI
pop edi
pop esi
stosd ; trasferisce la word da EAX (riornante da GetAPI
all’elemento
; della stringa
indirizzato da EDI
Salviamo nello
stack i valori prima di chiamare la procedura, dopodiché effettuiamo una call
alla GetAPI. Assumiamo che ESI sia un puntatore al nome della API cercata e EDI
il puntatore alla variabile che conterrà
il nome della API
@@2:
cmp byte ptr [esi],0
jz @@3
inc esi
jmp @@2
@@3: cmp byte ptr [esi+1],0BBh
jz @@4
inc esi
jmp @@1
@@4: ret
GetAPIs endp
In questa maniera
otteniamo gli indirizzi di tutte le API che vogliamo, a prescindere dalla
versione di Windows.