Home | Chi sono | Contattami
 

Progr. lineare

Delphi
 
Componenti
  Database
 
Miei articoli

Windows

Miei articoli 

 

Dll Injection


Questo articolo affronta la tecnica di "Dll Injection" ossia il caricamento di una Dll nello spazio di memoria di un altro processo. Ovviamente viene esaminata anche la problematica opposta ossia l' eliminazione della Dll medesima. Verranno esaminate le varie api win32 che consentono la realizzazione di tale tecnica e ovviamente verrà fornito un esempio (la cosidetta "proof of concept") di implementazione. Il tutto in Delphi.

1. Introduzione

La tecnica di "Dll injection" viene associata spesso a virus o malware (se ne sente spesso parlare nella descrizione di alcuni virus nei bollettini online dei vari produttori di antivirus), tuttavia non rappresenta in se qualcosa di pericoloso (dipende tutto ovviamente dal codice che verrà scritto nella dll stessa). Se si vuole caricare una dll nello spazio di memoria associato al processo chiamante, il sistema operativo fornisce l'API LoadLibrary implementata nella dll kernel32.dll. Per eseguire l'operazione opposta ossia lo scaricamento di una dll, viene invece fornita l'API FreeLibrary implementata sempre in kernel32.dll. Nei paragrafi successivi si andrà ad estendere le 2 API in questione con delle versioni che prendano in input anche l'identificativo del processo consentendo quindi il caricamento e lo scaricamento di una dll relativamente allo spazio di memoria di qualsiasi processo.

2. LoadLibrary , GetModuleHandle , FreeLibrary e GetProcAddress  

Come prima cosa è opportuno definire lo scheletro di una dll nell'ambiente di sviluppo Delphi. Di seguito abbiamo un esempio di una semplice dll con tutto l'occorrente

library TestDll; uses Windows; procedure Somma(i1: Cardinal; i2: Cardinal; var i3: Cardinal); stdcall; begin i3 := i1 + i2; end; procedure EntryPointProc(reason: integer); begin case reason of DLL_PROCESS_ATTACH: //1 begin end; DLL_THREAD_ATTACH: //2 begin end; DLL_PROCESS_DETACH: //3 begin end; DLL_THREAD_DETACH: //0 begin end; end; end; exports Somma name 'Addizione'; begin //codice di inizializzazione della dll DllProc := @EntryPointProc; DllProc(DLL_PROCESS_ATTACH); end.

2.1 LoadLibrary

La funzione LoadLibrary mappa una dll nello spazio di memoria del processo chiamante. Prende in input il nome della dll da mappare e restituisce in output un Handle (valore intero positivo di 4 byte) al modulo caricato. il valore di questo handle ha un significato ben preciso: si tratta dell'indirizzo di base della dll nello spazio di memoria del processo in cui è caricata. La funzione è' implementata in kernel32.dll in 2 versioni, una per il sistema di caratteri ANSI ed una per il sistema di caratteri UNICODE. Di seguito le 2 dichiarazioni

function LoadLibraryA(lpLibFileName: PAnsiChar): cardinal; stdcall; function LoadLibraryW(lpLibFileName: PWideChar): cardinal; stdcall;

Se il modulo è già stato mappato allora viene incrementato il "Reference Count" della dll: il "Reference Count" è un valore di 4 byte, definito per ogni modulo caricato, e memorizzato nello spazio di memoria del processo chiamante. Se il nome della Dll (parametro lbLibFileName) non è valido allora la funzione restituisce 0. Entrando più nel dettaglio, se la dll è già mappata allora viene incrementato il suo "Reference Count", altrimenti la dll viene mappata e viene eseguito il blocco di inizializzazione (begin - end) ossia il main della dll. Nel main viene assegnata una funzione a DllProc (la procedura EntryPointProc) poi viene chiamata DllProc passandogli come parametro il valore 1 (DLL_PROCESS_ATTACH). A questo punto assume un ruolo importante la variabile ExitCode : dichiarata nella unit System e con valore di default pari a 0, tale variabile rappresenta il codice di uscita del main della dll. Se nel main della dll vado a settare ExitCode ad un valore diverso da 0, la dll viene scaricata dalla memoria e la LoadLibrary restituisce 0. La DllProc viene comunemente chiamata "Entry Point procedure".

2.1 GetModuleHandle

La funzione GetModuleHandle restituisce l'handle di una dll caricata nello spazio di memoria del processo chiamante (stesse caratteristiche dell'handle resituito dalla LoadLibrary). Se la dll in questione non è mappata allora la funzione restituisce 0. La funzione è implementata in kernel32.dll in 2 versioni, una per il sistema di caratteri ANSI ed una per il sistema di caratteri UNICODE. Di seguito le 2 dichiarazioni:

function GetModuleHandleA(lpModuleName: PAnsiChar): Cardinal; stdcall; function GetModuleHandleW(lpModuleName: PWideChar): Cardinal; stdcall;

2.2 FreeLibrary

La funzione FreeLibrary ha il comportamento esattamente opposto a quello della LoadLibrary: prende in input un handle ad un modulo dll caricato (l'handle ottenuto ad esempio da LoadLibrary o da GetModuleHandle) e decrementa di 1 il "Reference Count" del modulo in questione; se il nuovo valore di "Reference Count" è 0 allora scarica la dll dallo spazio di memoria del processo chiamante. Entrando più nel dettaglio, prima di scaricare la dll, chiama la DllProc passandogli come parametro il valore 0 (DLL_PROCESS_DETACH). Se il modulo da scaricare non è mappato allora la funzione restituisce 0. La funzione è implementata in kernel32.dll in un unica versione (non in versione doppia ASCII UNICODE come le precedenti). Di seguito la dichiarazione

function FreeLibrary(hLibModule: Cardinal): Boolean; stdcall;

2.3 GetProcAddress

La funzione GetProcAddress restituisce l'indirizzo di una funzione esportata da una dll caricata nello spazio di memoria del processo chiamante

function GetProcAddress(hModule: cardinal; lpProcName: pAnsiChar): Pointer; stdcall;

hModule è l'handle alla dll (determinato ad esempio tramite chiamata a LoadLibrary o GetModuleHandle) e lpProcName è il nome della procedura.

3. Alcuni esempi

Vediamo ora alcuni esempi di utilizzo delle 3 funzioni sopra descritte. Prima di tutto però bisogna osservare che tutte le volte che viene chiamata un API win32 e tale funzione restituisce un valore che ne indica il fallimento, è doveroso approfondire la conoscenza dell'errore in questione tramite l'API GetLastError. E' opportuno quindi aprire una breve parentesi sulla gestione degli errori nel contesto delle API win32.

3.1 Gestione degli errori

Quando una API win32 viene eseguita, se si verifica un errore durante l'esecuzione (ad esempio LoadLibrary con un nome di dll inesistente), l'API in questione interrompe l'esecuzione e restituisce un valore che ne indica il fallimento (in generale 0). Prima di uscire però, l'API va a salvare il codice numerico associato all'errore incontrato in una zona di memoria associata al thread che ha chiamato l'API in questione (tramite l'API SetLastError). Tale codice numerico è un valore intero positivo di 4 byte e può essere recuperato dal thread che ha effettuato la chiamata tramite l'api GetLastError. Tale codice numerico può poi essere tradotto in una stringa che ne da una descrizione tramite l'API FormatMessage. Nella unit SysUtils è definita la funzione SysErrorMessage che fa da involucro alla FormatMessage (gestisce allocazione e deallocazione del buffer di memoria etc...). E' opportuno sottolineare più di una volta che il codice errore viene salvato in un area di memoria associata al thread chiamante (il thread che ha eseguito la chiamata all'api che ha dato esito fallimentare) quindi non esiste il pericolo che il valore letto da GetLastError sia stato nel frattempo sovrascritto da un altro errore generato nel contesto di un altro thread. Chiaramente la GetLastError va chiamata immediatamente dopo aver accertato l'esito fallimentare dell'api chiamata altrimenti il valore potrebbe essere effettivamente sovrascritto da codici di errore generati da altre api successivamente chiamate dal medesimo thread. Di seguito andiamo a elencare alcuni esempi di chiamata alle api LoadLibrary, GetModuleHandle e FreeLibrary con la gestione degli errori.

3.2 Esempi con gestione degli errori

Di seguito eseguiamo il caricamento di una dll

var error_code: Cardinal; error_description: string; ... ... if LoadLibraryA(pAnsiChar('C:\TestDl.dll')) = 0 then begin error_code := GetLastError; error_description := SysErrorMessage(error_code); Exit; end;

Esempi analoghi possono essere fatti con le altre API.

3.3 Valori di input della "Entry Point procedure"

Nella definizione di dll fatta nel paragrafo 2 viene evidenziato il fatto che la "Entry Point procedure" può avere 4 valori distinti per il parametro in input: andiamo a descrivere quali sono le situazioni che portano a chiamare la "Entry Point procedure" con ognuno dei suddetti valori

1) DLL_PROCESS_ATTACH: la dll ancora non è mappata in memoria e viene effettuato il caricamento. Come già definito nel paragrafo 2.1, se assegno alla variabile ExitCode un valore diverso da zero, allora il caricamento della dll fallisce e la funzione di caricamento (ad esempio la LoadLibraryA) restituisce errore (ad esempio la LoadLibraryA restituisce zero e tramite la GetLastError si può avere codice di errore e descrizione)  

2) DLL_PROCESS_DETACH: sono 2 le situazioni:

  • la dll è mappata in memoria e viene chiamata la FreeLibrary per scaricarla
  • il processo, nel cui spazio di memoria è stata caricata la dll, termina; N.B. quando si parla di terminazione del processo si parla di terminazione "corretta", ad esempio facendo click sulla crocetta in alto a destra della finestra principale oppure chiamando direttamente l'API ExitProcess. La terminazione "forzata" ossia quella invocata ad esempio tramite l'API TerminateProcess, elimina il processo senza andare a chiamare la "Entry Point procedure" di ogni singola dll caricata: questo è quello che succede ad esempio nel taskmanager di Windows quando si esegue un "kill" su un processo.  

3) DLL_THREAD_ATTACH: si verifica quando viene creato un nuovo thread nel processo che ha mappata la dll: tutte le volte che un processo crea un nuovo thread viene chiamata la "Entry Point procedure" per ogni dll caricata

4) DLL_PROCESS_DETACH: si verifica quando un thread del processo termina (anche in questo caso si deve avere una terminazione "corretta").

3.4 DisableThreadLibraryCalls

Come si è già visto nel paragrafo precedente, ad ogni creazione o terminazione di un thread, viene chiamato l'entrypoint di ogni dll caricata passandogli come parametro DLL_THREAD_ATTACH (creazione di un thread) o DLL_THREAD_DETACH (terminazione di un thread). Per evitare ciò (e quindi ottimizzare anche le performance) si può ricorrere all'API win32 DisableThreadLibraryCalls. Di seguito la dichiarazione della funzione

function DisableThreadLibraryCalls(hLibModule: Cardinal): Boolean; stdcall;

  • hLibModule: Handle del modulo dll le cui notifiche DLL_THREAD_ATTACH e DLL_THREAD_DETACH devono essere disabilitate. Solitamente la funzione DisableThreadLibraryCalls viene chiamata in corrispondenza della notifica DLL_PROCESS_ATTACH e viene usato come parametro il valore HInstance (variabile definita nella unit SysInit) che rappresenta l'handle al modulo dll corrente.

a seguire, lo scheletro della dll modificato con la chiamata alla DisableThreadLibraryCalls.

library TestDll; uses Windows, SysInit; procedure Somma(i1: Cardinal; i2: Cardinal; var i3: Cardinal); stdcall; begin i3 := i1 + i2; end; procedure EntryPointProc(reason: integer); begin case reason of DLL_PROCESS_ATTACH: //1 begin DisableThreaLibraryCalls(HInstance); end; DLL_THREAD_ATTACH: //2 begin end; DLL_PROCESS_DETACH: //3 begin end; DLL_THREAD_DETACH: //0 begin end; end; end; exports Somma name 'Addizione'; begin //codice di inizializzazione della dll DllProc := @EntryPointProc; DllProc(DLL_PROCESS_ATTACH); end.

4. Come mappare una dll nello spazio di memoria di un processo remoto

Dopo avere analizzato le caratteristiche delle dll è arrivato il momento di affrontare con cognizione di causa l'obiettivo prefissato ossia mapping e unmapping di una dll nello spazio di memoria di qualsiasi processo.

4.1 L'API CreateRemoteThread

Il segreto della realizzazione della tecnica di dll injection sta tutto nell'API win32 CreateRemoteThread. Questa funzione è una estensione della funzione CreateThread che viene usata per creare e mandare in esecuzione un thread nello spazio di memoria del processo chiamante; con la CreateRemoteThread è possibile creare un thread (e quindi mandare in esecuzione del codice) nello spazio di memoria di un qualsiasi processo diverso da quello chiamante. Entrambe le API sono implementate in kernel32.dll. Di seguito la dichiarazione di entrambe

function CreateThread(lpThreadAttributes: Pointer; dwStackSize: Cardinal; lpStartAddress: TFNThreadStartRoutine; lpParameter: Pointer; dwCreationFlags: Cardinal; var lpThreadId: Cardinal): Cardinal; stdcall; function CreateRemoteThread(hProcess: Cardinal; lpThreadAttributes: Pointer; dwStackSize: Cardinal; lpStartAddress: TFNThreadStartRoutine; lpParameter: Pointer; dwCreationFlags: Cardinal; var lpThreadId: Cardinal): Cardinal; stdcall;

Come si può vedere dalle dichiarazioni sopra, l'unica differenza tra le 2 API consiste nell'handle al processo remoto che è presente come primo parametro nella CreateRemoteThread. Andiamo ora ad analizzare i singoli parametri:

  • hProcess:[input] è l'handle del processo remoto in cui si vuole creare e mandare in esecuzione il thread (ottenuto ad esempio tramite l'API win32 OpenProcess); l'handle deve avere almeno il seguente livello di accesso allo spazio di memoria del processo in questione: PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION +  PROCESS_VM_OPERATION +  PROCESS_VM_WRITE + PROCESS_VM_READ
  • lpThreadAttributes:[input] puntatore ad una struttura SECURITY_DESCRIPTOR (vedere la documentazione sul PlatformSDK per maggiori approfondimenti): non ci interessa (almeno nel contesto di questo articolo) e può essere settato a nil
  • dwStackSize:[input] dimensione iniziale in bytes dello stack associato al nuovo thread (consultare sempre il PlatformSDK per maggiori approfondimenti); lo impostiamo a 0 (in questo modo viene usata la dimensione di default)
  • lpStartAddress:[input] è l'indirizzo della funzione che rappresenta l'esecuzione del thread
  • lpParameter: [input] è l'indirizzo del parametro da passare alla funzione puntata da lpStartAddress
  • lpThreadId: [output] è il "Thread Id" (TID) ossia l'identificativo del nuovo thread

L funzione restituisce un handle al Thread appena creato in remoto. In caso di errore la funzione restituisce 0: occorre in questo chiamare l'API GetLastError per avere maggiori informazioni riguardo al tipologia di errore.

Focalizziamo ora l'attenzione sui parametri lpStartAddress e lpParameter: sono rispettivamente l'indirizzo della funzione che verrà eseguita dal thread che andiamo a creare (lpStartAddress) e l'indirizzo del parametro che passiamo in input a tale funzione (lpParameter)

TFNThreadStartRoutine=function(lpParameter: pointer): cardinal;

il fatto che la funzione in questione preveda un unico parametro in input potrebbe sembrare riduttivo ma in realtà non lo è in quanto possiamo mettere tutti i parametri che vogliamo in un record e passare l'indirizzo di tale record come parametro. Si è già detto che lpStartAddress e lpParameter sono entrambi indirizzi (funzione e parametro): nel caso della CreateRemoteThread dovranno essere indirizzi validi nello spazio di memoria del processo remoto. Detto questo vediamo come affrontare il problema del caricamento di una dll nello spazio di memoria di un processo remoto: la soluzione è semplice ed efficace e consiste nell'andare ad eseguire la funzione LoadLibrary passandogli come parametro il nome della dll che vogliamo mappare. Il caso vuole che la LoadLibrary sia proprio una funzione di tipo TFNThreadStartRountine e quindi calza a pennello con la CreateRemoteThread. Quelli della funzione e del suo parametro devono essere indirizzi validi nello spazio di memoria del processo remoto: nel seguito analiziamo nel dettaglio la questione prendendo in considerazione la versione per ASCII della LoadLibrary ossia LoadLibraryA.

4.2 LoadLibraryA

Come determinare l'indirizzo a partire dal quale è implementata la LoadLibraryA? Se prendiamo in considerazione il processo chiamante basta usare le seguenti righe di codice

GetProcAddress(GetModuleHandle('kernel32.dll'), 'LoadLibraryA');

Come fare per un processo remoto? Apriamo una breve parentesi inerente la locazione di caricamento di una dll nello spazio di memoria di un processo. Una dll è fondamentalmente un PE binary ossia un file che ha una struttura interna che risponde alle caratteristiche del formato PE (Portable Executable); i .exe sono anch'essi dei PE binary. Il formato PE prevede che all'interno del file, in una zona specifica, vi siano 4 byte che definiscono l'indirizzo di base preferito per il caricamento del file nello spazio di memoria di un processo: il cosidetto BaseAddress. In Delphi si può definire tale valore tramite la  direttiva {$IMAGEBASE <number>}.

library TestDll; {$IMAGEBASE $10000000} interface ... ...

Nell'esempio si impone come BaseAddress per la dll, l'indirizzo $10000000: la dll verrà preferibilmente caricata all'indirizzo $10000000 nel contesto dello spazio di memoria del processo che ha eseguito la LoadLibrary. Il valore di default è $00400000. E' importante sottolineare il fatto che è un indirizzo preferito, non obbligato: se lo spazio a cui fa riferimento il BaseAddress è inutilizzabile (poichè già occupato o per altri motivi), la dll viene "rilocata" ossia caricata ad un indirizzo valido differente. Esistono però delle eccezioni: le seguenti dll vengono obbligatoriamente caricate sempre allo stesso indirizzo

  • ntdll.dll: $7C910000
  • kernel32.dll: $7C800000
  • user32.dll: $77D10000

Inoltre:

  • Tutti i processi caricano obbligatoriamente ntdll.dll
  • Tutti i processi, tranne smss.exe, caricano obbligatoriamente kernel32.dll
  • ntdll.dll e kernel32.dll sono sempre rispettivamente la prima e la seconda dll caricata nello spazio di memoria di un processo
  • Se un processo viene creato in modalità SUSPENDED (vedi l'API win32 CreateProcess), dopo la sua creazione, nel suo spazio di memoria saranno caricate solo ntdll.dll e kernel32.dll

In conclusione, fatta eccezione per smss.exe, la kernel32.dll è caricata sicuramente in tutti i processi e sempre allo stesso indirizzo e quindi anche le funzioni implementate al suo interno saranno sempre allo stesso indirizzo nello spazio di memoria di qualsiasi processo. Quindi, l'indirizzo ottenuto con le righe di codice descritte sopra, è valido per ogni processo. Di conseguenza tale indirizzo può essere usato come valore del parametro lpStartAddress della CreateRemoteThread.

4.3 Nome della dll

A questo punto bisogna copiare il nome della dll, nello spazio di memoria del processo remoto. L'obiettivo viene raggiunto con i seguenti 2 passaggi:

1) Allocazione, nello spazio di memoria del processo remoto, di un area di memoria di dimensione tale da contenere il nome della dll: API win32 VirtualAllocEx

2) Copio il nome della dll nella zona di memoria appena allocata: API win32 WriteProcessMemory

4.3.1 VirtualAllocEx

L' API VirtualAllocEx va ad allocare (o alternativamente a riservare) una regione di memoria nello spazio di memoria di un determinato processo.

function VirtualAllocEx(hProcess: Cardinal; lpAddress: Pointer; dwSize: Cardinal; flAllocationType: Cardinal; flProtect: Cardinal): Pointer; stdcall;

Analizziamo i singoli parametri:

  • hProcess:[input] rappresenta l'handle del processo destinazione (ottenuto ad esempio tramite l'API win32 OpenProcess); l'handle deve avere almeno il seguente livello di accesso allo spazio di memoria del processo in questione: PROCESS_VM_OPERATION (verrà approfondito in seguito l'argomento)
  • lpAddress:[input] rappresenta l'indirizzo desiderato, nello spazio di memoria del processo remoto, a partire dal quale verrà allocata la memoria: nel nostro caso non ci interessa un indirizzo particolare e quindi lasciamo che sia il sistema ad allocare la memoria dove meglio crede; settiamo quindi a nil il valore del parametro lpAddress
  • dwSize:[input] esprime la dimensione della regione di memoria che sia vuole allocare: nel nostro caso sarà la lunghezza della stringa che mi indica il nome della dll (con eventuale percorso completo; quindi Length(nome_dll)
  • fAllocationType:[input] indica il tipo di operazione da effettuare (in quanto la VirtualAllocEx, oltre ad effettuare allocazioni di memoria, si occupa anche di riservare l'area specificata dai parametri lpAddress e dwSize): nel nostro caso, ossia allocazioni di memoria, dobbiamo settare il valore a MEM_COMMIT (allocazione di memoria di dimensione pari a dwSize con inizializzazione a 0)
  • flProtect:[input] indica il tipo di accesso alla memoria allocata: nel nostro caso abbiamo bisogno di accesso in lettura e scrittura e quindi gli diamo il valore PAGE_READWRITE (in altri contesti, come ad esempio la copia di un intero eseguibile nell'area allocata, ci sarebbe stato bisogno ad esempio anche del valore PAGE_EXECUTE).

La funzione restituisce l'indirizzo dell'area di memoria allocata. In caso di errore, la funzione restituisce 0. Anche in questo caso è doveroso gestire la situazione di errore, usufruendo dell'API GetLastError

4.3.2 WriteProcessMemory

L' API WriteProcessMemory scrive nello spazio di memoria di un determinato processo.

function WriteProcessMemory(hProcess: Cardinal; const lpBaseAddress: Pointer; lpBuffer: Pointer; nSize: Cardinal; var lpNumberOfBytesWritten: Cardinal): Boolean; stdcall;

Analizziamo i singoli parametri:

  • hProcess:[input] rappresenta l'handle del processo destinazione (ottenuto ad esempio tramite l'API win32 OpenProcess); l'handle deve avere almeno il seguente livello di accesso allo spazio di memoria del processo in questione: PROCESS_VM_WRITE + PROCESS_VM_OPERATION.
  • lpBaseAddress:[input] indica l'indirizzo (nello spazio di memoria del processo remoto) a partire dal quale inizia la scrittura: può essere ad esempio il puntatore restituito dall'API VirtualAllocEx
  • lpBuffer:[input] indirizzo della sequenza di byte che devo copiare: è un indirizzo nello spazio di memoria del processo locale (processo chiamante): in pratica si copiano i dato da lpBuffer in locale a lpBaseAddress in remoto
  • nSize:[input] numero di byte che devo copiare
  • lpNumberOfBytesWritten:[output] numero di byte effettivamente copiati 

In caso di errore, la funzione restituisce 0 (False). Ricorrere in tal caso alla GetLastError per maggiori informazioni sul tipo di errore.

5. Procedura per Dll injection

Si hanno tutti gli elementi per definire una procedura base per effettuare la Dll injection. La procedura che andremo a creare prenderà in input il PID (Process Identifier) del processo remoto ed il nome della Dll che vogliamo mappare. Come prima cosa però è opportuno soffermarsi sull' API win32 OpenProcess che consente di ottenere un handle ad un processo a partire dal suo PID

5.1 OpenProcess

L' API OpenProcess consente di ottenere un handle ad un processo (con un determinato livello di accesso allo spazio di memoria del medesimo) a partire dal PID (il PID di un processo può essere ottenuto facilmente tramite il taskmanager di Windows)

function OpenProcess(dwDesiredAccess: Cardinal; bInheritHandle: Boolean; dwProcessId: Cardinal): Cardinal; stdcall;

Analizziamo i singoli parametri:

  • dwDesiredAcces:[input] definisce il livello di accesso al processo remoto; in relazione alle esigenze minimali delle API che andremo a chiamare (evidenziate nella descrizione di ognuna), si ha che il livello minimo richiesto per le nostre esigenze è il seguente: PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION +  PROCESS_VM_OPERATION +  PROCESS_VM_WRITE + PROCESS_VM_READ
  • bInheritHandle:[input] specifica se l'handle è ereditabile o no
  • dwProcessId:[input] PID del processo remoto

5.2 Procedura

procedure InjectDll(PID: dword; DLL: pChar); var BytesWritten, hProcess, hThread, TID: Cardinal; Parameters: pointer; pThreadStartRoutine: Pointer; begin hProcess := OpenProcess(PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION + PROCESS_VM_OPERATION + PROCESS_VM_WRITE + PROCESS_VM_READ, False, PID ); Parameters := VirtualAllocEx( hProcess, nil, Length(DLL), MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProcess, Parameters, Pointer(DLL), Length(DLL), BytesWritten); pThreadStartRoutine := GetProcAddress(GetModuleHandle('KERNEL32.DLL'), 'LoadLibraryA'); hThread := CreateRemoteThread(Process, nil, 0, pThreadStartRoutine, Parameters, 0, TID); CloseHandle(hProcess); end;

La procedura InjectDll ci consente di mappare una dll nello spazio di memoria di un processo remoto (ho volutamente omesso la gestione degli errori per rendere il tutto più chiaro ed accessibile).

5.3 Sincronizzazione col thread remoto

Ora che abbiamo una procedura che realizza l'obiettivo prefissato, analizziamo quelle che possono essere le valide aggiunte funzionali; la prima cosa che può essere fatta è quella di mettersi in attesa fino alla notifica dell'avvenuta terminazione del thread creato in remoto. L'attesa dell'evento può essere realizzata tramite l'API win32 WaitForSingleObject che andiamo di seguito ad esaminare

5.3.1 WaitForSingleObject

L'API WaitForSingleObject realizza uno stato di attesa che termina nel momento in cui l'oggetto a cui si fa riferimendo passa allo stato "Signaled"

function WaitForSingleObject(hHandle: Cardinal; dwMilliseconds: Cardinal): Cardinal; stdcall;

Analizziamo i singoli parametri:

  • hHandle:[input] handle dell'oggetto sul cui stato si vuole rimanere in attesa (e quindi sincronizzarsi);
  • dwMilliseconds:[input] è il timeout in millisecondi ossia il tempo dopo il quale la funzione esce dallo stato di attesa e ritorna a prescindere dallo stato dell'oggetto.

La funzione restituisce i seguenti valori

  • WAIT_ABANDONED ($00000080): si verifica quando l'oggetto in questione è un oggetto Mutex che non è stato rilasciato dal thread di appartenenza prima che quest'ultimo terminasse
  • WAIT_TIMEOUT ($00000102): il timeout (espresso dal parametro dwMilliseconds) è scaduto è l'oggetto non è nello stato Signaled; se il timeout è 0 allora la funzione controlla semplicemente lo stato dell'oggetto ed esce immediatamente; se il timeout è INFINITE ($FFFFFFFF) allora non esiste timeout
  • WAIT_OBJECT_0 ($00000000): l'oggetto è passato allo stato Signaled
  • WAIT_FAILED ($FFFFFFFF): c'è stato un errore e la funzione è fallita; utilizzare GetLastError

5.3.2 GetExitCodeThread

Visto che ci mettiamo in attesa della terminazione del thread, possiamo anche rilevare l'output del thread ossia il valore restituito dalla funzione eseguita dal thread; nel nostro caso la funzione eseguita dal thread è la LoadLibraryA il cui output è l'indirizzo di base della dll che andiamo a caricare. Per fare questo useremo l'API win32 GetExitCodeThread 

function GetExitCodeThread(hThread: Cardinal; var lpExitCode: Cardinal): Boolean; stdcall;

  • hThread:[input] handle del thread
  • lpExitCode:[output] output della funzione eseguita dal thread (nel nostro caso la funzione eseguita dal thread è la LoadLibraryA il cui output è l'indirizzo di base della dll caricata, nello spazio di memoria del processo remoto)

5.3.3 ExitThread

Strettamente correlata alla funzione GetExitCodeThread vista al paragrafo precedente, c'è la funzione ExitThread. Di seguito la dichiarazione

procedure ExitThread(dwExitCode: Cardinal); stdcall;

  • dwExitCode: codice di uscita del thread; è il valore che verrà successivamente prelevato dall'API win32 GetExitCodeThread.

Chiamata nel contesto della funzione eseguita da un thread, causa l'immediata terminazione del thread medesimo assegnando come codice di uscita il valore dwExitCode. La chiamata all'API win32 GetExitCodeThread consente di prelevare il valore del codice di uscita. Nel nostro caso la funzione eseguita dal thread è l'API win32 LoadLibraryA e quindi il valore prelevato dalla GetExitCodeThread è l'output della LoadLibraryA (ossia l'indirizzo di base in memoria del modulo dll caricato o eventualmente 0 in caso di fallimento).  E' opportuno osservare che, se la funzione eseguita dal thread non chiama in maniera esplicita la funzione ExitThread, la ExitThread viene chiamata implicitamente passandogli come parametro l'output della funzione medesima.

5.3.4 TerminateThread

L'API win32 TerminateThread provoca la terminazione di un generico thread (non solo del thread chiamante come nel caso della ExitThread vista al paragrafo precedente). Di seguito la dichiarazione

function TerminateThread(hThread: Cardinal; dwExitCode: Cardinal): Boolean; stdcall;

  • hThread: handle al thread che si vuole terminare: l'handle deve avere necessariamente il diritto di accesso THREAD_TERMINATE.
  • dwExitCode: codice di uscita (analogo alla funzione ExitThread)

Per ulteriori informazioni inerenti la TerminateThread, rimando alla documentazione sul Platform SDK.

5.4 Deallocazione di memoria

Un altro miglioramento che può essere apportato alla procedura di dll injection è la gestione della memoria allocata in remoto quindi principalmente la sua deallocazione; per fare questo utilizziamo l' API win32  VirtualFreeEx.

5.4.1 VirtualFreeEx

L' API VirtualFreeEx va a deallocare una regione di memoria nello spazio di memoria di un qualsiasi processo remoto

function VirtualFreeEx(hProcess: Cardinal; lpAddress: Pointer; dwSize: Cardinal; dwFreeType: Cardinal): Pointer; stdcall;

Analizziamo i singoli parametri:

  • hProcess:[input] l'handle del processo remoto; handle con livello di accesso minimo pari a PROCESS_VM_OPERATION
  • lpAddress:[input] indirizzo di base della regione di memoria da deallocare; deve essere il puntatore restituito da una precedente chiamata a VirtualAllocEx
  • dwSize:[input] va settato a 0
  • dwFreeType:[input] tipo di operazione; va settato a MEM_RELEASE

In caso di errore la funzione restituisce 0; approfondire con GetLastError

5.5 Nuova versione della procedura di injection con possibilità di sincronizazione, gestione della memoria e gestione degli errori

function ErrStr(nomeFunc: string): Boolean; //funzione che crea ua stringa composta da codice errore e descrizione //relativamente all'ultimo errore che si è verificato nel thread chiamante; //il parametro nomeFunc è il nome dell' API win32 che è fallita ed ha quindi //restituito l'errore var error_code: Cardinal; error_description: string; error_string: string; begin result := False; error_code := GetLastError; error_description := SysErrorMessage(error_code); error_string := nomeFunc + ': ' + 'CODErr=' + IntToStr(error_code) + ' Descr=' + error_description; //qui può essere inserito del codice per scrivere la stringa su un file //oppure visualizzarla a video, etc... result := True; end; function InjectDll(PID: dword; DLL: pChar; synch: Boolean; var BaseAddr: Cardinal): Boolean; var BytesWritten, hProcess, hThread, TID: Cardinal; Parameters: pointer; pThreadStartRoutine: Pointer; lpExitCode: Cardinal; begin Result := False; hProcess := 0; hThread := 0; parameters := nil; pThreadStartRoutine := nil; try hProcess := OpenProcess(PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION + PROCESS_VM_OPERATION + PROCESS_VM_WRITE + PROCESS_VM_READ, False, PID ); if hProcess = 0 then begin ErrStr('OpenProcess'); Exit; end; Parameters := VirtualAllocEx( hProcess, nil, Length(DLL), MEM_COMMIT, PAGE_READWRITE); if Parameters = nil then begin ErrStr('VirtualAllocEx'); Exit; end; if not WriteProcessMemory(hProcess, Parameters, Pointer(DLL), Length(DLL), BytesWritten) then begin ErrStr('WriteProcessMemory'); Exit; end; pThreadStartRoutine := GetProcAddress(GetModuleHandle('KERNEL32.DLL'), 'LoadLibraryA'); if pThreadStartRoutine = nil then begin ErrStr('GetProcAddress'); Exit; end; hThread := CreateRemoteThread(hProcess, nil, 0, pThreadStartRoutine, Parameters, 0, TID); if hThread = 0 then begin ErrStr('CreateRemoteThread'); Exit; end; //mi metto in attesa della terminazione del thread if synch then begin case WaitForSingleObject(hThread, INFINITE) of WAIT_FAILED: begin ErrStr('WaitForSingleObject'); Exit; end; end; if not GetExitCodeThread(hThread, lpExitCode) then begin ErrStr('GetExitCodeThread'); Exit; end; BaseAddr := lpExitCode; end; Result := True; finally if hThread <> 0 then begin if not CloseHandle(hThread) then begin ErrStr('CloseHandle'); end; end; if Parameters <> nil then begin if VirtualFreeEx(hProcess, Parameters, 0 , MEM_RELEASE) = nil then begin ErrStr('VirtualFreeEx'); end; end; if hProcess <> 0 then begin if not CloseHandle(hProcess) then begin ErrStr('CloseHandle'); end; end; end; end; end;

Il modo migliore per verificar che la dll è stata mappata nello spazio di memoria del processo remoto è utilizzare ProcessExplorer e verificare che tra le dll caricate dal processo remoto c'è anche la dll in questione. Si consiglia di testare la procedura su applicativi tipo wordpad o notepad in modo da evitare crash del sistema in caso di problemi.

6. Privilegi utente

Se si fanno dei test di injection su dei processi tipo ad esempio winlongon.exe si nota che la OpenProcess restituisce errore; in sostanza non si riesce ad ottenere un handle al processo con i requisiti minimi richiesti per effettuare l'injection. Per potere raggiungere l'obiettivo occorre che il processo che esegue l'injection venga eseguito con una utenza che abbia assegnato il PRIVILEGIO di DEBUG e tale PRIVILEGIO sia abilitato. Vediamo nel seguito una breve descrizione del concetto di PRIVILEGIO UTENTE

6.1 Descrizione di PRIVILEGIO

Per Privilegio si intende il diritto di un account, sia esso un utente od un gruppo, di effettuare varie operazioni a livello di sistema sul computer locale, come ad esempio spegnere il sistema, caricare device drivers o cambiare la data e ora di sistema.  Di seguito sono elencate le differenze principali tra Privilegi e Diritti di accesso:

1) I Privilegi controllano l' accesso a risorse di sistema e processi relativi al sistema mentre i Diritti di Accesso controllano l' accesso ai cosidetti Securable Objects (oggetti ai quali possa essere assegnato un Security Descriptor: ad es. File, Cartelle, Chiavi di registro, etc..)

2) Un amministratore di sistema può assegnare Privilegi ad un utente od un gruppo, mentre il sistema consente o nega l' accesso ad un Securable Object in relazione ai Diritti di accesso specificati nella ACL dell' oggetto

Windows XP/Windows 2003/Windows 2000/Windows NT dispongono di un database che raccoglie i Privilegi dei vari utenti e gruppi: si può avere un elenco da "Pannello di controllo" -> "Strumenti di amministrazione" -> "Criteri di protezione locali" -> "impostazioni protezione" -> "Criteri locali" -> "Assegnazione diritti utente". Quando un utente esegue un log in, il sistema produce un token di accesso che contiene una lista dei Privilegi dell' utente, inclusi naturalmente quelli garantiti ai gruppi a cui l' utente appartiene . E' da notare che i Privilegi si applicano solo al computer locale e quindi un account di dominio può avere Privilegi differenti su computer differenti.

Quando un utente tenta di eseguire una operazione che richiede determinati Privilegi, il sistema controlla il Token di accesso dell' utente per verificare che l' utente in questione disponga dei Privilegi necessari e, in caso affermativo, che tali Privilegi siano abilitati. In caso contrario l' utente non può eseguire l' operazione.

Per determinare i Privilegi inclusi in un Token di Accesso si usa la funzione GetTokenIformation che indica anche quali Privilegi sono abilitati e quali no. La maggiorparte sono disabilitati di default.

Per far riferimento ai vari Privilegi nel contesto delle Api di Windows, è previsto un insieme di stringhe ognuna delle quali identifica uno dei Privilegi. Per un elenco delle stringhe andare all' url

Nomi Privilegi

In breve sono le seguenti stringhe:

'SeCreateTokenPrivilege'; 'SeAssignPrimaryTokenPrivilege'; 'SeLockMemoryPrivilege'; 'SeIncreaseQuotaPrivilege'; 'SeUnsolicitedInputPrivilege'; 'SeMachineAccountPrivilege'; 'SeTcbPrivilege'; 'SeSecurityPrivilege'; 'SeTakeOwnershipPrivilege'; 'SeLoadDriverPrivilege'; 'SeSystemProfilePrivilege'; 'SeSystemtimePrivilege'; 'SeProfileSingleProcessPrivilege'; 'SeIncreaseBasePriorityPrivilege'; 'SeCreatePagefilePrivilege'; 'SeCreatePermanentPrivilege'; 'SeBackupPrivilege'; 'SeRestorePrivilege'; 'SeShutdownPrivilege'; 'SeDebugPrivilege'; 'SeAuditPrivilege'; 'SeSystemEnvironmentPrivilege'; 'SeChangeNotifyPrivilege'; 'SeRemoteShutdownPrivilege'; 'SeUndockPrivilege'; 'SeSyncAgentPrivilege'; 'SeEnableDelegationPrivilege'; 'SeManageVolumePrivilege'; 'SeImpersonatePrivilege'; 'SeCreateGlobalPrivilege';

Le funzioni che ottengono e modificano l' elenco dei Privilegi in un Token di accesso, usano il LUID (Locally Unique identifier) per identificare ognuno dei Privilegi (invece della stringa). Il LUID relativo ad un Privilegio differisce da un computer all' altro ma anche da un boot ad un altro sul medesimo computer unico nel contesto del computer. Per ottenere ogni volta il LUID corrispondente ad un determinato Privilegio si usa la funzione LookupPrivilegeValue. La funzione inversa (restituisce la stringa che da il nome al Privilegio, a partire dal LUID) è LookupProvilegeName. Il sistema fornisce anche un insieme di stringhe descrittive per ogni Privilegio. Per ottenere la descrizione a partire dal nome del Privilegio si usa la funzione LookupProvilegeDisplayName. Si può usare la funzione PrivilegeCheck per verificare se un Token di accesso detiene un insieme specifico di Privilegi.

Senza entrare nel dettaglio della definizione delle suddette API win32, andiamo nel seguito a descrivere la procedura che ci consente di abilitare un privilegio.

function ModificaPrivilegio(szPrivilege: pChar; fEnable: Boolean): boolean; var NewState: TTokenPrivileges; luid: TLargeInteger; hToken: Cardinal; ReturnLength: Cardinal; begin Result := false; hToken := 0; try //apro il token di accesso associato al processo corrente (il cui handle //è ottenuto tramite GetCurrentProcess. Specifico TOKEN_ADJUST_PRIVILEGES //come tipo di accesso al Token: in questa maniera sono in grado di abilitare //e/o disabilitare Privilegi. hToken è l' handle del token di accesso appena //aperto. if not OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, hToken) then begin ErrStr('OpenProcessToken'); Exit; end; //ricavo il LUID (Locally Unique Identifier) corrispondente al privilegio //specificato: si tratta in sostanza di un identificativo univoco del privilegio; //varia da sessione a sessione ed anche tra un riavvio e l' altro del sistema if not LookupPrivilegeValue(nil, szPrivilege, luid) then begin ErrStr('LookupPrivilegeValue'); Exit; end; //lavoro su NewState (di tipo TTokenPrivileges). Rappresenta un elenco di privilegi; //nel caso specifico conterrà un solo privilegio (ProvilegeCount = 1). L' arrary //Privileges contiene oggetti con 2 campi: il luid del privilegio (Luid) ed //il livello di abilitazione del medesimo (Attributes) NewState.PrivilegeCount := 1; NewState.Privileges[0].Luid := luid; if fEnable then //abilitiamo il privilegio NewState.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED else //disabilitiamo il privilegio NewState.Privileges[0].Attributes := 0; //eseguiamo la modifica sullo stato di abilitazione del privilegio //nel contesto del token di accesso aperto if not AdjustTokenPrivileges( hToken, FALSE, NewState, sizeof(NewState), nil, ReturnLength) then begin ErrStr('AdjustTokenPrivileges'); Exit; end; Result := True; finally //chiudo l' handle al token di accesso aperto if hToken <> 0 then begin if not CloseHandle(hToken) then begin ErrStr('CloseHandle'); end; end; end; end;

Nel nostro caso, il Privilegio che ci interessa è il Privilegio di Debug definito dalla stringa 'SeDebugPrivilege'. Questo Privilegio è sicuramente assegnato al gruppo Administrators mentre uno User non lo possiede (e quindi non può essere abilitato). Abilitando tale privilegio, si può eseguire l'injection anche su processi di sistema (tipo ad esempio il già citato winlongon.exe). Eseguendo un processo con una utenza amministrativa è quindi potenzialmente possibile eseguire dll injection su qualsiasi processo (fatta eccezione per smss.exe).

7. Determinare il PID di un processo dal nome dell'eseguibile con le tool helper api

Nelle procedure precedenti si fa sempre riferimento al PID (Process Identifier) del processo remoto. Nel seguito viene fornita una procedura per ricavare il PID di un processo a partire dal nome del modulo .exe associato. Per la realizzazione della procedura viene usata la Tool Help Library (rimando ancora una volta al Platform SDK per una descrizione dettagliata). Le api win32 che rientrano nella Tool Help Library sono tutte implementate nella kernel32.dll e le loro dichiarazioni in Delphi sono raccolte nella unit tlhelp32 (che dovrà quindi essere inclusa). Non ci addentreremo nella descrizione dettagliata delle funzioni che verranno usate (cosa che va ben oltre gli obbiettivi del presente articolo).

7.1 Procedura

function PidProcesso(NomeProcesso: string): Cardinal; var pe: TProcessEntry32; hSnap: Cardinal; begin Result := 0; hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); pe.dwSize := sizeof(TProcessEntry32); //Prelevo informazioni sul primo processo nello snapshot di sistema Process32First(hSnap, pe); repeat //loop sui processi Result := pe.th32ProcessID; if (LowerCase(pe.szExeFile) = LowerCase(NomeProcesso)) then begin break; end; until (not (Process32Next(hSnap, pe)) ) ; CloseHandle(hSnap); end;

7.2 Note

La procedura al 7.1 manca di una adeguata gestione degli errori (con GetLastError, etc..) tuttavia consente di raggiungere l'obiettivo prefissato. Bisogna osservare che ci possono essere più processi che fanno riferimento al medesimo modulo .exe (ad esempio 3 istanza di notapad.exe) nella stessa sessione o in sessioni differenti contemporaneamente aperte (ad esempio ogni sessione crea un nuovo processo per winlogon.exe): in tal caso viene preso la prima istanza incontrata nell'enumerazione.

8. Unload library

Dopo aver esaminato la problematica dell' injection di una dll (estensione della LoadLibrary a qualsiasi processo), è doveroso affrontare il problema opposto ovvero l'unloading della dll in questione (estensione della FreeLibrary). La FreeLibrary è una funzione dello stesso tipo della LoadLibraryA in quanto ha un solo parametro in input definito come puntatore ed è quindi una funzione valida per la CreateRemoteThread. La FreeLibrary prende in input l'handle alla dll caricata in memoria (valore restituito ad esempio da LoadLibrary o GetModuleHandle). Come già detto nei paragrafi precedenti il valore di questo handle ha un significato ben preciso: si tratta dell'indirizzo di base della dll nello spazio di memoria del processo in cui è caricata. Come determinare tale valore nel contesto di un altro processo? Come nel caso del paragrafo 7.1, anche in questa situazione ci vengono in aiuto le API win32 raccolte nella cosidetta Tool Help Library.  

8.1 Determinare il baseaddress di una dll in un processo remoto

function BaseAddrDllProcesso(PID: Cardinal; NomeDll: string): Cardinal; var me: TModuleEntry32; hSnap: THandle; begin Result := 0; hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid); me.dwSize := sizeof(TModuleEntry32); //Prelevo informazioni sul primo modulo del processo in questione Module32First(hSnap, me); repeat //loop sui moduli if LowerCase(me.szModule) = LowerCase(NomeDll) then begin result := Cardinal(me.modBaseAddr); break; end; until (not (Module32Next(hSnap, me))); CloseHandle(hSnap); end;

La procedura restituisce l'indirizzo di base della dll specificata da NomeDll nello spazio di memoria del processo identificato da PID. In caso negativo viene restituito 0 (dll non mappata nello spazio di memoria).

8.2 Procedura di unloading

La procedura di unloading ricalca quella di injection con la differenza che in questo caso non dobbiamo copiare nulla (usando VirtualAllocEx, WriteProcessMemory e poi VirtualFreeEx) in quanto il parametro non è più la stringa col nome della dll ma l'indirizzo di base della dll nello spazio di memoria del processo remoto (valore che otteniamo direttamente con la procedura BaseAddrDllProcesso definita all' 8.1).

function UnloadDll(PID: dword; DLL: string; synch: Boolean; var success: Boolean): Boolean; var BytesWritten, hProcess, hThread, TID: Cardinal; Parameters: pointer; BaseAddr: Cardinal; pThreadStartRoutine: Pointer; lpExitCode: Cardinal; begin Result := False; if PID = 0 then Exit; BaseAddr := BaseAddrDllProcesso(PID, DLL); if BaseAddr = 0 then Exit; hProcess := 0; hThread := 0; pThreadStartRoutine := nil; try hProcess := OpenProcess(PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION + PROCESS_VM_OPERATION + PROCESS_VM_WRITE + PROCESS_VM_READ, False, PID ); if hProcess = 0 then begin ErrStr('OpenProcess'); Exit; end; pThreadStartRoutine := GetProcAddress(GetModuleHandle('KERNEL32.DLL'), 'FreeLibrary'); if pThreadStartRoutine = nil then begin ErrStr('GetProcAddress'); Exit; end; hThread := CreateRemoteThread(hProcess, nil, 0, pThreadStartRoutine, Pointer(BaseAddr), 0, TID); if hThread = 0 then begin ErrStr('CreateRemoteThread'); Exit; end; //mi metto in attesa della terminazione del thread if synch then begin case WaitForSingleObject(hThread, INFINITE) of WAIT_FAILED: begin ErrStr('WaitForSingleObject'); Exit; end; end; if not GetExitCodeThread(hThread, lpExitCode) then begin ErrStr('GetExitCodeThread'); Exit; end; success := (lpExitCode > 0); end; Result := True; finally if hThread <> 0 then begin if not CloseHandle(hThread) then begin ErrStr('CloseHandle'); end; end; if hProcess <> 0 then begin if not CloseHandle(hProcess) then begin ErrStr('CloseHandle'); end; end; end; end;

anche in questo caso è opportuno abilitare il Privilegio di Debug per poter effettuare l'operazione su qualsiasi processo: questo può essere fatto tramite la funzione ModificaPrivilegio definita al 6.1

9. Conclusioni

In questo articolo è stata fatta una panoramica generale sulla tecnica di dll injection: senza avere la pretesa di approfondire nel dettaglio tutti gli argomenti citati (cosa che andrebbe ben oltre l'obiettivo dell'articolo) sono stati esaminati i punti cardine su cui si basa l'implementazione della tecnica. I miglioramenti attuabili sono numerosi e meritevoli di numerosi altri articoli dedicati (utilizzo delle API Native, lettura dei moduli caricati su un processo remoto, "code injection", utilizzo delle API per gestire il CONTEXT di un thread, etc... solo per citarne alcuni). 

 

 
 
Your Ad Here