Home | Chi sono | Contattami
 

Progr. lineare

Delphi
 
Componenti
  Database
 
Miei articoli

Windows

Miei articoli 

 

Eseguire un .exe direttamente dalla memoria: download di un .exe dal web e sua esecuzione senza prima salvarlo su disco.


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

 

 

 

 

 

 

 

 

 

 

 

 

 

 
 
Your Ad Here