|
In questo articolo viene affrontata la tecnica di "code injection": questa
tecnica è una estensione della nota tecnica di "dll injection"; l'analisi che
verrà effettuata nei paragrafi successivi, parte direttamente
dall'implementazione della "dll injection", soffermandosi sulle caratteristiche
e sui limiti per poi procedere per gradi a tutti i miglioramenti applicabili. A
conclusione di ciò, avremo una procedura per mappare una dll nello spazio di
memoria di un processo remoto (che chiamaeremo RemoteLoadLibrary) ed una
procedura per eseguire l'unloading di una dll dallo spazio di memoria di un
processo remoto (che chiameremo RemoteFreeLibrary).
1. Introduzione
La tecnica di dll injection consiste nel caricare una dll nello spazio di
memoria di un processo remoto, estendendo di fatto la funzionalità dell'API
win32 che effettua il mapping limitatamente allo spazio di memoria del processo
chiamante. Il fulcro dell'implementazione è l'API win32 CreateRemoteThread
(implementata in kernel32.dll) che crea e manda in esecuzione un thread nello
spazio di memoria di un processo remoto. I parametri più importante di questa
funzione sono i seguenti:
1) hProcess: l'handle del processo remoto
2) lpStartAddress: l'indirizzo della funzione che verrà eseguita dal
thread (indirizzo nello spazio di memoria del processo remoto): funzione con un
unico parametro di tipo puntatore che rappresenta l'indirizzo dei dati da
passare alla funzione medesima
3) lpParameter: l'indirizzo dei dati da passare alla funzione puntata
da lpStartAddress (indirizzo nello spazio di memoria del processo
remoto); è quindi il valore del parametro della funzione puntata da
lpStartAddress
Nella tecnica di dll injection il valore di lpStartAddress viene
settato pari all'indirizzo di LoadLibrary (identico in tutti i processi);
l'argomento è il nome della dll: tale nome viene copiato nello spazio di memoria
del processo remoto tramite le API win32 VirtualAllocEx (allocazione di
memoria nello spazio di memoria di un processo remoto) e WriteProcessMemory
(copia una sequenza di byte da locale in remoto); lpParameter è
l'indirizzo di base della regione di memoria allocata e scritta (è l'output
della VirtualAllocEx).
La tecnica di "code injection" consiste nell'eseguire del codice generico
nello spazio di memoria di un processo remoto. In pratica il parametro
lpStartAddress rappresenta qualsiasi funzione che sia presente nello spazio
di memoria del processo remoto (non solo la LoadLibraryA). Nei paragrafi
successivi realizzeremo come esempio una funzione il cui obiettivo è caricare
una dll in memoria (prima verifica tramite GetModuleHandleA se il modulo
è già caricato ed in caso negativo chiama LoadLibraryA, il tutto
correlato dalla dovuta gestione degli errori e l'utilizzo dell'API win32
GetLastError) e vedremo come andare ad eseguire tale funzione nello spazio
di memoria di un processo remoto. E' da osservare una certa ambiguità
nell'utilizzo comune dei termini "dll injection" e "code injection" intendendosi
spesso per "dll injection" il mapping di una dll nello spazio di memoria di un
processo remoto indipendentemente dal tipo di implementazione. Nel seguito si
utilizzerà il termine "code injection" per esprimere l'esecuzione di una
qualsiasi funzione nello spazio di memoria di un processo remoto. Senza perdita
di generalità prenderemo in considerazione come gia detto una funzione che si
occupa di mappare una dll nello spazio di memoria di un processo remoto (con i
dovuti controlli e la dovuta gestione degli errori).
2. Passo dopo passo
Come prima cosa definiamo una procedura per loggare gli errori relativi alle
api win32
function ErrStr(nomeFunc: string): Boolean;
//funzione che crea una 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...
//ScriviSuFile('c:\Logger.txt', error_string);
result := True;
end;
Bene! Come già detto in precedenza per "Code injection" si intende l'esecuzione di una procedura nello spazio di
memoria di un processo remoto. Partiremo da una procedura di esempio ed
analizzeremo i vari passaggi da effettuare per tradurla in una procedura
eseguibile da remoto.
2.1 Procedura
function XLoadLibrary(NomeDll: PAnsiChar; var addr: Cardinal): Boolean;
var
error_code: Cardinal;
begin
Result := False;
//se il nome della dll non è specificato, esco
if NomeDll = nil then
Exit;
//determino l'indirizzo di base del
//modulo specificato; se addr <> 0 vuol dire che il modulo
//è già caricato in memoria quindi esco
//e addr contiene l'indirizzo di base della dll
addr := GetModuleHandleA(NomeDll);
if addr <> 0 then
begin
Result := True;
Exit;
end
else //viene restituito 0, ma devo in ogni caso
//verificare che il codice di errore
//sia quello corrispondente alla situazione
//"modulo non presente" ossia 126
begin
error_code := GetLastError;
if error_code <> 126 then
begin
Exit;
end;
end;
//caricola dll in memoria
addr := LoadLibraryA(NomeDll);
if addr = 0 then
begin
error_code := GetLastError;
end;
Result := True;
end;
La procedura sopra è un involucro alla LoadLibraryA: in pratica se il modulo che si vuol caricare è già presente in memoria (la verifica viene fatta tramite la GetModuleHandleA), il parametro addr riceve il valore dell'indirizzo di base attuale, altrimenti esegue la LoadLibraryA; il tutto correlato con la dovuta gestione degli errori. 2.2 Procedure di supporto La prima cosa da fare è definire delle procedure che vadano a definire il blocchi fondamentali per la realizzazione del "code injection" 2.2.1 InjectData Questa funzione si occupa di allocare una regione di memoria nello spazio di memoria di un processo remoto e copiarvi una sequenza di byte.
function InjectData(hProcess: Cardinal;
localData: Pointer;
DataSize: Cardinal;
Esecuzione: Boolean;
var remoteData: Pointer): Boolean;
var
BytesWritten: Cardinal;
protezione: Cardinal;
begin
Result := False;
//controllo la validità dei valori dei parametri
if ((hProcess = 0) or (localData = nil) or (DataSize = 0)) then
Exit;
try
if Esecuzione then
protezione := PAGE_EXECUTE_READWRITE
else
protezione := PAGE_READWRITE;
remoteData := VirtualAllocEx(
hProcess,
nil,
DataSize,
MEM_COMMIT,
protezione);
if remoteData = nil then
begin
ErrStr('VirtualAllocEx');
Exit;
end;
if not WriteProcessMemory(hProcess,
remoteData,
localData,
DataSize,
BytesWritten) then
begin
ErrStr('WriteProcessMemory');
Exit;
end;
Result := True;
finally
if not result then
begin
if remoteData <> nil then
begin
if VirtualFreeEx(hProcess,
remoteData,
0,
MEM_RELEASE) = nil then
begin
ErrStr('VirtualFreeEx');
end;
end;
end;
end;
end;
Analizziamo nel dettaglio i parametri: - hProcess:[input] handle del processo remoto; nel contesto della tecnica di code injection l'handle deve avere i seguenti diritti di accesso al processo in questione: PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION + PROCESS_VM_OPERATION + PROCESS_VM_WRITE + PROCESS_VM_READ
- localData:[input] puntatore alla sequenza di byte in locale che devo copiare in remoto
- DataSize:[input] dimensione in byte della sequenza di byte in locale che devo copiare in remoto (dimensione dell'informazione puntata da localData)
- Esecuzione:[input] boolean che indica se la sequenza di byte da copiare in remoto, rappresenta codice da eseguire; in caso positivo l'API VirtualAllocEx verrà chiamata con livello di protezione (valore dell'ultimo parametro) pari a PAGE_EXECUTE_READWRITE; in caso contrario il livello di protezione sarà PAGE_READWRITE
- remoteData:[ouptut] indirizzo, nello spazio di memoria del processo remoto, della sequenza di byte che è stata copiata
2.2.2 UnloadData Questa funzione ha il compito di deallocare la memoria precedentemente allocata tramite la InjectData
function UnloadData(hProcess: Cardinal; remoteData: Pointer): Boolean;
begin
Result := False;
//controllo la validità dei valori dei parametri
if (hProcess = 0) or (remoteData = nil) then
Exit;
try
if VirtualFreeEx(hProcess,
remoteData,
0,
MEM_RELEASE) = nil then
begin
ErrStr('VirtualFreeEx');
Exit;
end;
Result := True;
finally
end;
end;
Analizziamo i singoli parametri: - hProcess:[input] handle al processo remoto
- remoteData:[input] indirizzo di base della regione di memoria da deallocare (valore precedentemente ottenuto tramite chiamata alla InjectData)
2.2.3 Come creare la funzione da copiare ed eseguire in remoto Il nostro obiettivo (a titolo esemplificativo) è eseguire la procedura al paragrafo 2 nell'ambito di un thread creato nel contesto del processo remoto. Ci sono vari accorgimenti; il primo e più importante è la sostituzione delle chiamate alle varie API win32 con dei puntatori che hanno come valore l'indirizzo delle funzioni corrispondenti. Facendo riferimento sempre alla funzione definita nel paragrafo 2 abbiamo le seguenti API win32 - GetModuleHandleA
- LoadLibraryA
- GetLastError
Bisogna definire una variabile di tipo puntatore a funzione per ognuno delle suddette funzioni ed assegnare ad ognuna di esse l'indirizzo corrispondente, nella seguente maniera
var
//puntatore a GetModuleHandleA
pGetModuleHandleA: function(lpModuleName: PAnsiChar): Cardinal; stdcall;
//puntatore a LoadLibraryA
pLoadLibraryA: function(pLibFileName: PAnsiChar): Cardinal; stdcall;
//puntatore a GetLastError
pGetLastError: function(): Cardinal; stdcall;
...
...
...
begin
...
...
pGetModuleHandleA := GetProcAddress(GetModuleHandle('kernel32'),
'GetModuleHandleA');
pLoadLibraryA := GetProcAddress(GetModuleHandle('kernel32'),
'LoadLibraryA');
pGetLastError := GetProcAddress(GetModuleHandle('kernel32'),
'GetLastError');
Una cosa importante da osservare (a titolo informativo) è che le 3 funzioni in questione sono implementate in kernel32.dll; kernel32.dll viene sempre caricata al medesimo indirizzo di base (valore restituito da GetModuleHandle) nello spazio di memoria di qualsiasi processo e di conseguenza anche l'indirizzo di tutte le funzioni in essa implementate (valore restituito dalla GetProcAddress) sarà sempre lo stesso in tutti i processi; di conseguenza tali indirizzi possono essere calcolati una volta per tutte ed assegnati direttamente senza stare tutte le volte a ricalcolarli tramite GetModuleHandle e GetProcAddress. Più precisamente si ha Indirizzi di base dll: - Kernel32.dll: $7C800000
- Ntdll.dll: $7C910000
Offset API nelle dll corrispondenti: - GetModuleHandleA (Kernel32.dll): $0000B6A1
- LoadLibraryA (Kernel32.dll): $00001D77
- GetProcAddress (Kernel32.dll): $0000ADA0
- FreeLibrary (Kernel32.dll): $0000ABDE
- GetLastError (Kernel32.dll) forward --> RtlGetLastWin32Error (Ntdll.dll): $00010331
Quindi: Indirizzi delle API nello spazio di memoria di un processo: (indirizzo di base dll) + (offset nella dll) - GetModuleHandleA = $7C800000 + $0000B6A1 = $7C80B6A1
- LoadLibraryA = $7C800000 + $00001D77 = $7C801D77
- GetProcAddress = $7C800000 + $0000ADA0 = $7C80ADA0
- FreeLibrary = $7C800000 + $0000ABDE = $7C80ABDE
- GetLastError = RtlGetLastWin32Error = $7C910000 + $00010331 = $7C920331
Chiusa questa breve parentesi, torniamo all'obiettivo che ci siamo posti ossia tradurre la procedura al paragrafo 2 in una versione eseguibile da remoto. Vediamo direttamente il codice che andremo poi ad analizzare
type
TRemoteLoadLibraryData = record
errorcod: Cardinal; //codice di errore restituito dalla GetLastError
output: Cardinal; //risultato significativo nella funzione eseguita dal thread
//qui iniziano i campi specifici relativi alla nostra operazione
//prima di tutto gli indirizzi delle API usate
pExitThread: procedure (dwExitCode: Cardinal); stdcall; //API obbligatoria
pGetLastError: function(): Cardinal; stdcall;
pGetModuleHandleA: function(lpModuleName: PAnsiChar): Cardinal; stdcall;
pLoadLibraryA: function(pLibFileName: PAnsiChar): Cardinal; stdcall;
//ora mettiamo i puntatori ai parametri
//(che copiamo e scriviamo tramite InjectData)
pNomeDll: Pointer;
end;
...
...
procedure RemoteLoadLibraryThread(lpParameter: pointer); stdcall;
begin
with TRemoteLoadLibraryData(lpParameter^) do
begin
if pNomeDll = nil then
pExitThread(0);
//determino l'indirizzo di base del modulo specificato;
//se output <> 0 vuol dire che il modulo è già caricato
//in memoria quindi esco e output contiene
//l'indirizzo di base della dll
output := pGetModuleHandleA(pNomeDll);
if output <> 0 then
begin
pExitThread(1);
end
else //viene restituito 0, ma devo in ogni caso
//verificare che il codice di errore
//sia quello corrispondente alla situazione
//"modulo non presente" ossia 126
begin
errorcod := pGetLastError;
if errorcod <> 126 then
begin
pExitThread(0);
end;
end;
//carico la dll in memoria
output := pLoadLibraryA(pNomeDll);
if output = 0 then
begin
errorcod := pGetLastError;
pExitThread(0);
end;
end;
end;
RemoteLoadLibraryThread è la funzione che verrà eseguita in remoto mentre TRemoteLoadLibraryData è il tipo del parametro della funzione stessa. La funzione RemoteLoadLibraryThread ha come parametro il puntatore lpParameter: il valore di lpParameter sarà l'indirizzo, nello spazio di memoria del processo remoto, di un oggetto di tipo TRemoteLoadLibraryData. Il tutto nel rispetto della definizione dell'API win32 CreateRemoteThread. Si perviene alla seguente sequenza di passaggi per realizzare il "code injection" 1) copio la funzione RemoteLoadLibraryThread nello spazio di memoria del processo remoto tramite la InjectData: sia pRemoteLoadLibraryThread l'indirizzo della funzione nello spazio di memoria del processo remoto 2) copio un oggetto di tipo TRemoteLoadLibraryData nello spazio di memoria del processo remoto tramite la InjectData: sia pRemoteLoadLibraryData l'indirizzo dell'oggetto nello spazio di memoria del proceso remoto 3) eseguo l'API win32 CreateRemoteThread con i seguenti valori: - lpStartAddress (indirizzo della funzione che verrà eseguita dal thread che vado a creare) = pRemoteLoadLibraryThread
- lpParameter (indirizzo del parametro che verrà preso in input dalla funzione eseguita dal thread che vado a creare) = pRemoteLoadLibraryData
Come determinare la dimensione dei 2 suddetti valori che mi accingo a copiare nello spazio di memoria del processo remoto? Tale dimensione è infatti uno dei parametri di input della InjectData. Nel caso dell' oggetto di tipo TRemoteLoadLibrary, la dimensione si ottiene tramite la nota funzione SizeOf: dimensione=SizeOf(TRemoteLoadLibraryData). Nel caso della funzione (RemoteLoadLibraryThread) si possono seguire 2 strade: Dimensione di una funzione 1) Definisco una funzione dummy subito dopo la definizione della funzione di cui desidero sapere la dimensione: tale funzione dummy non fa nulla ed ha la seguente definizione
procedure Dummy;
begin
end;
In pratica scriverò:
function MyFunction(//parametri...
//implementazione
);
procedure Dummy;
begin
end;
la dimensione di MyFunction sarà data da
@Dummy - @MyFunction
2) Ogni funzione termina con l'istruzione assembly "ret" la cui rappresentazione binaria (in esadecimale) è data dal byte $C3. Si può quindi partire dall'inizio della funzione (l'indirizzo della funzione ossia @nomefunzione) e scorrere tutti i byte fino a trovare il valore $C3
function SizeOfProc(pAddr: pointer): Cardinal;
var
dwSize: Cardinal;
begin
dwSize := 0;
repeat
inc(dwSize);
until PByte(Cardinal(pAddr)+dwSize-1)^ = $C3;
Result := dwSize;
end;
2.2.4 InjectThread A questo punto andiamo a definire una funzione che incapsula la chiamata all'API win32 CreateRemoteThread. Prima di mostrare il codice ed analizzarlo, occorre fare una breve osservazione riguardo al record TRemoteLoadLibraryData definito al paragrafo 2.3.3: i primi 2 campi hanno il seguente significato - errorcod: è il codice dell'eventuale errore generato all'interno della RemoteLoadLibraryThread (codice di errore che può essere sia il valore restituito dalla GetLastError come conseguenza del fallimento di una delle API win32, sia naturalmente un qualsiasi altro valore che rappresenti un determinato tipo di errore nel contesto del programma che andiamo a sviluppare)
- output: rappresenta un valore significativo nell'ambito della RemoteLoadLibraryThread: ad esempio nel caso specifico di cui sopra, esso riceve il valore dell'indirizzo base di caricamento della dll (e quindi l'output di GetModuleHandleA o di LoadLibraryA)
Nel seguito si useranno sempre questi 2 campi come i primi 2 campi del record che andremo a definire come parametro della funzione da eseguire in remoto (qualsiasi sia la funzione da eseguire in remoto). Detto questo andiamo a definire la funzione InjectThread che si occupa di incapsulare la chiamata alla CreateRemoteThread con l'adeguata gestione degli errori e dei valori di output (ivi compresi i valori dei primi 2 campi del record)
function InjectThread(hProcess: Cardinal;
remoteFunction: Pointer;
remoteParameter: Pointer;
var outputValue: Cardinal;
var errorCode: Cardinal;
Synch: Boolean): Boolean;
var
hThread: Cardinal;
TID: Cardinal;
lpExitCode: Cardinal;
bytesRead: Cardinal;
begin
//N.B. i parametri outputValue e errorCode hanno senso solo se Synch=True
Result := False;
//controllo la valdità dei valori dei parametri
if ((hProcess = 0) or (remoteFunction = nil) or (remoteParameter = nil)) then
Exit;
try
hThread := CreateRemoteThread(hProcess,
nil,
0,
remoteFunction,
remoteParameter,
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;
//c'è stato un errore (lpExitCode è l'output
//della funzione eseguita dal thread remoto
//che nel nostro caso è RemoteLoadLibraryThread;
//più precisamente è l'argomento
//della ExitThread)
if lpExitCode = 0 then //ExitThread(0)
begin
//leggo il codice dell' errore
//che si è verificato nel thread remoto
//nel nostro caso TRemoteLoadLibraryData.errorcod
if not ReadProcessMemory(hProcess,
remoteParameter,
@errorcode,
4,
bytesRead) then
begin
ErrStr('ReadProcessMemory');
Exit;
end;
end
else
begin
//leggo il valore significativo generato nel thread remoto
//nel nostro caso TRemoteLoadLibraryData.output
if not ReadProcessMemory(hProcess,
Pointer(Cardinal(remoteParameter)+4),
@outputvalue,
4,
bytesRead) then
begin
ErrStr('ReadProcessMemory');
Exit;
end;
end;
end;
Result := True;
finally
if hThread <> 0 then
begin
if not CloseHandle(hThread) then
begin
ErrStr('CloseHandle');
end;
end;
end;
end;
Analizziamo nel dettaglio i singoli parametri - hProcess:[input] handle al processo remoto
- remoteFunction:[input] indirizzo della funzione (nel nostro caso la RemoteLoadLibraryThread) nello spazio di memoria del processo remoto (risultato della InjectData); è il valore del parametro lpStartaddress della CreateRemoteThread
- remoteParameter:[input] indirizzo del parametro della funzione da eseguire in remoto (nel nostro caso è l'indirizzo di un oggetto di tipo TRemoteLoadLibrary) (risultato anch'esso della InjectData); è il valore del parametro lpParameter della CreateRemoteThread
- outputvalue:[output] valore del campo output del record da usare come parametro della funzione da eseguire in remoto (nel nostro caso TRemoteLoadLibrary.output)
- errorcode:[output] valore del campo errorcod del record da usare come parametro della funzione da eseguire in remoto (nel nostro caso TRemoteLoadLibrary.errorcod)
- Synch:[input] indica se l'esecuzione del thread deve essere sincrona (ossia ci si deve mettere in attesa della sua terminazione tramite chiamata alla WaitForSingleObject sull'handle del thread creato dalla CreateRemoteThread)
A questo punto dobbiamo definire 2 funzioni, una che si occupa di inizializzare i campi del record, allocare regioni di memoria nello spazio di memoria del processo remoto e copiarvi dati da locale, l'altra che si occupa della deallocazione complessiva. Facendo sempre riferimento alla procedura al paragrafo 2 avremo:
var
hProcess: Cardinal; //handle al processo remoto
RemoteLoadLibraryData: TRemoteLoadLibraryData; //record da usare come parametro
pRemoteLoadLibraryData: Pointer; //indirizzo del record nello spazio di memoria
//del processo remoto
pRemoteLoadLibraryThread: Pointer; //indirizzo della funzione del thread
//nello spazio di memoria del processo remoto
...
...
...
//dealloco la memoria in remoto
function RemoteLoadLibraryUnloadData(): Boolean;
begin
Result := False;
with RemoteLoadLibraryData do
begin
UnloadData(hProcess, pNomeDll);
end;
//deallocazione spazio per il parametro
UnloadData(hProcess, pRemoteLoadLibraryData);
//deallocazione spazio per la funzione
UnloadData(hProcess, pRemoteLoadLibraryThread);
Result := True;
end;
//definisco i valori dei campi del record e copio i dati in remoto
function RemoteLoadLibraryInjectData(): Boolean;
begin
Result := False;
try
//inizializzazione valori campi:
with RemoteLoadLibraryData do
begin
errorcod := 0;
output := 0;
pGetLastError := GetProcAddress(GetModuleHandle('kernel32'),
'GetLastError');
pGetModuleHandleA := GetProcAddress(GetModuleHandle('kernel32'),
'GetModuleHandleA');
pLoadLibraryA := GetProcAddress(GetModuleHandle('kernel32'),
'LoadLibraryA');
if not InjectData(hProcess,
NomeDll,
Length(NomeDll),
False,
RemoteLoadLibraryData.pNomeDll) then
begin
Exit;
end;
end;
//copio il parametro
parameterSize := SizeOf(RemoteLoadLibraryData);
if not InjectData(hProcess,
@RemoteLoadLibraryData,
parameterSize,
False,
pRemoteLoadLibraryData) then
begin
Exit;
end;
//copio la funzione
functionSize := SizeOfProc(@RemoteLoadLibraryThread);
if not InjectData(hProcess,
@RemoteLoadLibraryThread,
functionSize,
True,
pRemoteLoadLibraryThread) then
begin
Exit;
end;
Result := True;
finally
if not Result then
begin
RemoteLoadLibraryUnloadData;
end;
end;
end;
Dal sorgente si può vedere che le chiamate ad InjectData sono racchiuse in blocco try finally: in caso di insuccesso della InjectData il flusso del programma viene rediretto al blocco finally dove lo aspetta la UnloadData; tutte le operazioni di InjectData devono terminare con successo altrimenti viene fatto un rollback totale ripristinando la memoria del processo remoto alla situazione iniziale. 3 Approccio generale Di seguito andremo a definire la versione definitiva della RemoteLoadLibrary; successivamente definiremo lo pseudocodice della procedura da utilizzare come scheletro di base tutte le volte che si vuole eseguire codice nello spazio di memoria di un processo remoto. Per finire daremo un ulteriore esempio di "code injection" implementando l'unloading di una dll nello spazio di memoria di un processo remoto. Per prima cosa però è opportuno definire una funzione che faccia da wrapper all'api win32 OpenProcess e che chiameremo OpenProcessEx. 3.1 OpenProcessEx Per ottenere un handle a determinati processi tipo ad esempio winlogon.exe, è necessario avere abilitato il Privilegio di Debug. La funzione OpenProcessEx che vado a presentare, prova ad ottenere un handle al processo remoto e, in caso di insuccesso tenta l'abilitazione del Privilegio di Debug e, in caso di successo, chiama nuovamente la OpenProcess.
function ModificaPrivilegio(szPrivilege: pChar; fEnable: Boolean): Boolean;
var
NewState: TTokenPrivileges;
luid: TLargeInteger;
hToken: Cardinal;
ReturnLength: Cardinal;
begin
Result := False;
hToken := 0;
try
if not OpenThreadToken(
GetCurrentThread(),
TOKEN_ADJUST_PRIVILEGES,
False,
hToken) then
begin
if GetLastError = ERROR_NO_TOKEN then
begin //non ho ottenuto il token asociato al thread
//e allora provo col processo
if not OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES,
hToken) then
begin
ErrStr('OpenProcessExToken');
Exit;
end;
end
else
begin
ErrStr('OpenThreadToken');
Exit;
end;
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;
function OpenProcessEx(dwDesiredAccess: Cardinal;
bInheritableHandle: LongBool;
dwProcessId: Cardinal): Cardinal;
var
hProcess: Cardinal;
begin
hProcess := OpenProcess(dwDesiredAccess,
bInheritableHandle,
dwProcessId); //provo ad ottenere un handle al processo
if hProcess = 0 then //se non ci riesco provo ad assegnare il privilegio di debug
begin
if ModificaPrivilegio('SeDebugPrivilege', True) then
begin
hProcess := OpenProcess(dwDesiredAccess,
bInheritableHandle,
dwProcessId);
end;
end;
result := hProcess;
end;
3.1 RemoteLodLibrary Si perviene alla seguente funzione mirata all'esecuzione della LoadLibraryA nello spazio di memoria del processo remoto
function RemoteLoadLibrary(PID: Cardinal;
NomeDll: PAnsiChar;
var addr: Cardinal;
var lastError: Cardinal;
synch: Boolean): Boolean;
type
TRemoteLoadLibraryData = record
errorcod: Cardinal; //codice di errore restituito dalla GetLastError
output: Cardinal; //risultato significativo nella funzione eseguita dal thread
//qui iniziano i campi specifici relativi alla nostra operazione
//prima di tutto gli indirizzi delle API usate
pExitThread: procedure (dwExitCode: Cardinal); stdcall; //API obbligatoria
pGetLastError: function(): Cardinal; stdcall;
pGetModuleHandleA: function(lpModuleName: PAnsiChar): Cardinal; stdcall;
pLoadLibraryA: function(pLibFileName: PAnsiChar): Cardinal; stdcall;
//ora mettiamo i puntatori ai parametri
//(che copiamo e scriviamo tramite InjectData)
pNomeDll: Pointer;
end;
var
hProcess: Cardinal;
RemoteLoadLibraryData: TRemoteLoadLibraryData;
//indirizzo del record nello spazio di memoria del processo remoto
pRemoteLoadLibraryData: Pointer;
//indirizzo della funzione del thread nello spazio
//di memoria del processo remoto
pRemoteLoadLibraryThread: Pointer;
output_value: Cardinal;
error_code: Cardinal;
functionSize, parameterSize: Cardinal;
procedure RemoteLoadLibraryThread(lpParameter: pointer); stdcall;
begin
with TRemoteLoadLibraryData(lpParameter^) do
begin
if pNomeDll = nil then
pExitThread(0);
//determino l'indirizzo di base del modulo specificato;
//se output <> 0 vuol dire che il modulo
//è già caricato in memoria quindi esco e
//output contiene l'indirizzo di base della dll
output := pGetModuleHandleA(pNomeDll);
if output <> 0 then
begin
pExitThread(1);
end
else //viene restituito 0, ma devo in ogni caso
//verificare che il codice di errore
//sia quello corrispondente alla situazione
//"modulo non presente" ossia 126
begin
errorcod := pGetLastError;
if errorcod <> 126 then
begin
pExitThread(0);
end;
end;
//carico la dll in memoria
output := pLoadLibraryA(pNomeDll);
if output = 0 then
begin
errorcod := pGetLastError;
pExitThread(0);
end;
pExitThread(1);
end;
end;
//dealloco la memoria in remoto
function RemoteLoadLibraryUnloadData(): Boolean;
begin
Result := False;
with RemoteLoadLibraryData do
begin
UnloadData(hProcess, pNomeDll);
end;
//deallocazione spazio per il parametro
UnloadData(hProcess, pRemoteLoadLibraryData);
//deallocazione spazio per la funzione
UnloadData(hProcess, pRemoteLoadLibraryThread);
Result := True;
end;
//definisco i valori dei campi del record e copio i dati in remoto
function RemoteLoadLibraryInjectData(): Boolean;
begin
Result := False;
try
//inizializzazione valori campi:
with RemoteLoadLibraryData do
begin
errorcod := 0;
output := 0;
pExitThread := GetProcAddress(GetModuleHandle('kernel32'),
'ExitThread');
pGetLastError := GetProcAddress(GetModuleHandle('kernel32'),
'GetLastError');
pGetModuleHandleA := GetProcAddress(GetModuleHandle('kernel32'),
'GetModuleHandleA');
pLoadLibraryA := GetProcAddress(GetModuleHandle('kernel32'),
'LoadLibraryA');
if not InjectData(hProcess,
NomeDll,
Length(NomeDll),
False,
pNomeDll) then
begin
Exit;
end;
end;
//copio il parametro
parameterSize := SizeOf(RemoteLoadLibraryData);
if not InjectData(hProcess,
@RemoteLoadLibraryData,
parameterSize,
False,
pRemoteLoadLibraryData) then
begin
Exit;
end;
//copio la funzione
functionSize := SizeOfProc(@RemoteLoadLibraryThread);
if not InjectData(hProcess,
@RemoteLoadLibraryThread,
functionSize,
True,
pRemoteLoadLibraryThread) then
begin
Exit;
end;
Result := True;
finally
if not Result then
begin
RemoteLoadLibraryUnloadData;
end;
end;
end;
begin
Result := False;
//controllo la validità dei valori dei parametri:
if ((PID = 0) or (NomeDll = nil)) then
Exit;
//inizializzo a zero le variabili locali
hProcess := 0;
pRemoteLoadLibraryData := nil;
pRemoteLoadLibraryThread := nil;
output_value := 0;
error_code := 0;
try
hProcess := OpenProcessEx(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;
if not RemoteLoadLibraryInjectData() then
Exit;
if not InjectThread(hProcess,
pRemoteLoadLibraryThread,
pRemoteLoadLibraryData,
output_value,
error_code,
synch) then
Exit;
//output_value e error_code sono stati inizializzati a 0.
//sono stati modificati dalla InjectThread solo se synch = true;
//se synch=false sono rimasti uguali a zero
addr := output_value;
lastError := error_code;
Result := True;
finally
if synch or (not Result) then
begin
RemoteLoadLibraryUnloadData;
end;
if hProcess <> 0 then
begin
if not CloseHandle(hProcess) then
begin
ErrStr('CloseHandle');
end;
end;
end;
end;
3.2 Pseudocodice da usare come scheletro di base per il "code injection" Sia CodeInjectionFunc il nome della funzione che implementa il "code injection", il seguente pseudocodice definisce l'implementazione di tale procedura
function CodeInjectionFunc(
PID: Cardinal; //PID del processo remoto
...; //dichiarazione di tutti i parametri che vogliamo
var outValue: Cardinal; //valore importante ottenuto dalla funzione remota
var codError: Cardinal; //errore riscontrato dalla funzione remota
synch: Boolean): Boolean; //esecuzione sincrona del thread remoto
type
TCodeInjectionFuncData = record
errorcod: Cardinal; //codice di errore rilevato nella funzione remota
output: Cardinal; //risultato significativo nella funzione remota
//Indirizzi delle API usate: elenco puntatori a funzioni; N.B. rigorosamente stdcall
//Es. pLoadLibraryA: function(pLibFileName: PAnsiChar): Cardinal; stdcall;
pExitThread: procedure (dwExitCode: Cardinal); stdcall; //API obbligatoria
//Puntatori a dati; Es. tutte le stringhe vanno messe qui dentro
//Es. pNomeDll: Pointer;
end;
var
hProcess: Cardinal; //handle al processo remoto
CodeInjectionFuncData: TCodeInjectionFuncData;
//indirizzo del record nello spazio di memoria del processo remoto
pCodeInjectionFuncData: Pointer;
//indirizzo della funzione del thread nello
//spazio di memoria del processo remoto
pCodeInjectionFuncThread: Pointer;
output_value: Cardinal;
error_code: Cardinal;
functionSize, parameterSize: Cardinal;
procedure CodeInjectionFuncThread(lpParameter: pointer); stdcall;
//var
//qui posso definire delle variabili locali
begin
with TCodeInjectionFuncData(lpParameter^) do
begin
//implementazione: tutte le API vengono chiamate
//tramite i puntatori nel record
...
...
//N.B. chiamare sempre pExitCodeThread
//per definire il codice di uscita del thread
//Es. pExitCodeThread(1) significa successo
// pExitCodeThread(0) significa fallimento
end;
end;
//dealloco la memoria in remoto
function CodeInjectionFuncUnloadData(): Boolean;
begin
Result := False;
with CodeInjectionFuncData do
begin
//chiamo UnloadData su tutti i puntatori
//a dati inclusi nel record
//Es. UnloadData(hProcess, pNomeDll);
end;
//deallocazione spazio per il parametro
UnloadData(hProcess, pCodeInjectionFuncData);
//deallocazione spazio per la funzione
UnloadData(hProcess, pCodeInjectionFuncThread);
Result := True;
end;
//definisco i valori dei campi del record e copio i dati in remoto
function CodeInjectionFuncInjectData(): Boolean;
begin
Result := False;
try
//inizializzazione valori campi del record:
with CodeInjectionFuncData do
begin
errorcod := 0;
output := 0;
//assegno i valori ai puntatori
//a funzione (tramite GetModuleHandle e GetProcAddres)
//Es. pLoadLibraryA := GetProcAddress(GetModuleHandle('kernel32'),
'LoadLibraryA');
pExitThread := GetProcAddress(GetModuleHandle('kernel32'),
'ExitThread'); //API Obbligatoria
...
...
//assegno i valori ai puntatori ai dati (tramite InjectData);
//N.B. se una InjectData ritorna False allora bisogna chiamare Exit
end;
//copio il parametro
parameterSize := SizeOf(CodeInjectionFuncData);
if not InjectData(hProcess,
@CodeInjectionFuncData,
parameterSize,
False,
pCodeInjectionFuncData) then
begin
Exit;
end;
//copio la funzione
functionSize := SizeOfProc(@CodeInjectionFuncThread);
if not InjectData(hProcess,
@CodeInjectionFuncThread,
functionSize,
True,
pCodeInjectionFuncThread) then
begin
Exit;
end;
Result := True;
finally
if not Result then
begin
CodeInjectionFuncUnloadData;
end;
end;
end;
begin
//inizializzo a zero le variabili locali
hProcess := 0;
pCodeInjectionFuncData := nil;
pCodeInjectionFuncThread := nil;
output_value := 0;
error_code := 0;
try
hProcess := OpenProcessEx(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;
if not CodeInjectionFuncInjectData() then
Exit;
if not InjectThread(hProcess,
pCodeInjectionFuncThread,
pCodeInjectionFuncData,
output_value,
error_code,
synch) then
Exit;
//output_value e error_code sono stati inizializzati a 0.
//sono stati modificati dalla InjectThread solo se synch = true;
//se synch=false sono rimasti uguali a zero
outvalue := output_value;
codError := error_code;
Result := True;
finally
if synch or (not Result) then
begin
CodeInjectionFuncUnloadData;
end;
if hProcess <> 0 then
begin
if not CloseHandle(hProcess) then
begin
ErrStr('CloseHandle');
end;
end;
end;
end;
Si noti anche l'utilizzo di una standardizzazione nella definizione dei nomi delle funzioni, dei tipi e delle variabili. Questo pseudocodice può essere usato come base per qualsiasi implementazione di "code injection" (si può salvare la procedura in un file di testo e poi creare una semplice procedura che sostituisce la stringa "CodeInjectionFunc" con una qualsiasi altra stringa su tutto il testo per ottenere versioni personalizzate) . 3.3 RemoteFreeLibrary Come ulteriore esempio della tecnica di "code injection" di seguito la funzione per eseguire l'unloading di una dll nello spazio di memoria di un processo remoto.
function RemoteFreeLibrary(
PID: Cardinal; //PID del processo remoto
NomeDll: PAnsiChar;
var outValue: Cardinal; //valore importante ottenuto dalla funzione remota
var codError: Cardinal; //errore riscontrato dalla funzione remota
synch: Boolean): Boolean; //esecuzione sincrona del thread remoto
type
TRemoteFreeLibraryData = record
errorcod: Cardinal; //codice di errore rilevato nella funzione remota
output: Cardinal; //risultato significativo nella funzione remota
//Indirizzi delle API usate: elenco puntatori a funzioni; N.B. rigorosamente stdcall
//Es. pLoadLibraryA: function(pLibFileName: PAnsiChar): Cardinal; stdcall;
pExitThread: procedure (dwExitCode: Cardinal); stdcall; //API obbligatoria
pGetLastError: function(): Cardinal; stdcall;
pGetModuleHandleA: function(lpModuleName: PAnsiChar): Cardinal; stdcall;
pFreeLibrary: function (hLibModule: Cardinal): Cardinal; stdcall;
//ora mettiamo i puntatori ai parametri
//(che copiamo e scriviamo tramite InjectData)
pNomeDll: Pointer;
end;
var
hProcess: Cardinal; //handle al processo remoto
RemoteFreeLibraryData: TRemoteFreeLibraryData;
//indirizzo del record nello spazio di memoria del processo remoto
pRemoteFreeLibraryData: Pointer;
//indirizzo della funzione del thread nello
//spazio di memoria del processo remoto
pRemoteFreeLibraryThread: Pointer;
output_value: Cardinal;
error_code: Cardinal;
functionSize, parameterSize: Cardinal;
procedure RemoteFreeLibraryThread(lpParameter: pointer); stdcall;
var
hDll: Cardinal;
begin
with TRemoteFreeLibraryData(lpParameter^) do
begin
if pNomeDll = nil then
pExitThread(0);
//determino l'indirizzo di base del modulo specificato;
//se hDll = 0 vuol dire che il modulo
//non è caricato in memoria quindi esco
hDll := pGetModuleHandleA(pNomeDll);
if hDll = 0 then
begin
errorcod := pGetLastError;
pExitThread(0);
end;
//libero la dll dalla memoria
output := pFreeLibrary(hDll);
if output = 0 then
begin
errorcod := pGetLastError;
pExitThread(0);
end;
pExitThread(1);
end;
end;
//dealloco la memoria in remoto
function RemoteFreeLibraryUnloadData(): Boolean;
begin
Result := False;
with RemoteFreeLibraryData do
begin
//chiamo UnloadData su tutti
//i puntatori a dati inclusi nel record
UnloadData(hProcess, pNomeDll);
end;
//deallocazione spazio per il parametro
UnloadData(hProcess, pRemoteFreeLibraryData);
//deallocazione spazio per la funzione
UnloadData(hProcess, pRemoteFreeLibraryThread);
Result := True;
end;
//definisco i valori dei campi del record e copio i dati in remoto
function RemoteFreeLibraryInjectData(): Boolean;
begin
Result := False;
try
//inizializzazione valori campi del record:
with RemoteFreeLibraryData do
begin
errorcod := 0;
output := 0;
//assegno i valori ai puntatori
//a funzione (tramite GetModuleHandle e GetProcAddres)
//Es. pLoadLibraryA := GetProcAddress(GetModuleHandle('kernel32'), 'LoadLibraryA');
pExitThread := GetProcAddress(GetModuleHandle('kernel32'),
'ExitThread'); //API Obbligatoria
pGetLastError := GetProcAddress(GetModuleHandle('kernel32'),
'GetLastError');
pGetModuleHandleA := GetProcAddress(GetModuleHandle('kernel32'),
'GetModuleHandleA');
pFreeLibrary := GetProcAddress(GetModuleHandle('kernel32'),
'FreeLibrary');
//assegno i valori ai puntatori ai dati (tramite InjectData);
//N.B. se una InjectData ritorna False allora bisogna chiamare Exit
if not InjectData(hProcess,
NomeDll,
Length(NomeDll),
False,
pNomeDll) then
begin
Exit;
end;
end;
//copio il parametro
parameterSize := SizeOf(RemoteFreeLibraryData);
if not InjectData(hProcess,
@RemoteFreeLibraryData,
parameterSize,
False,
pRemoteFreeLibraryData) then
begin
Exit;
end;
//copio la funzione
functionSize := SizeOfProc(@RemoteFreeLibraryThread);
if not InjectData(hProcess,
@RemoteFreeLibraryThread,
functionSize,
True,
pRemoteFreeLibraryThread) then
begin
Exit;
end;
Result := True;
finally
if not Result then
begin
RemoteFreeLibraryUnloadData;
end;
end;
end;
begin
//inizializzo a zero le variabili locali
hProcess := 0;
pRemoteFreeLibraryData := nil;
pRemoteFreeLibraryThread := nil;
output_value := 0;
error_code := 0;
try
hProcess := OpenProcessEx(PROCESS_CREATE_THREAD +
PROCESS_QUERY_INFORMATION +
PROCESS_VM_OPERATION +
PROCESS_VM_WRITE +
PROCESS_VM_READ,
False,
PID
);
if hProcess = 0 then
begin
ErrStr('OpenProcessEx');
Exit;
end;
if not RemoteFreeLibraryInjectData() then
Exit;
if not InjectThread(hProcess,
pRemoteFreeLibraryThread,
pRemoteFreeLibraryData,
output_value,
error_code,
synch) then
Exit;
//output_value e error_code sono stati inizializzati a 0.
//sono stati modificati dalla InjectThread solo se synch = true;
//se synch=false sono rimasti uguali a zero
outvalue := output_value;
codError := error_code;
Result := True;
finally
if synch or (not Result) then
begin
RemoteFreeLibraryUnloadData;
end;
if hProcess <> 0 then
begin
if not CloseHandle(hProcess) then
begin
ErrStr('CloseHandle');
end;
end;
end;
end;
RemoteLoadUnload
|