|
L' obiettivo di questo articolo è presentare la tecnica di dll
injection che consente l 'esecuzione del codice del main di una dll nel contesto
di un determinato processo. Vediamo per gradi quali sono i passaggi da seguire.
Inanzitutto apriamo il processo destinazione (quello all' interno del
quale vogliamo iniettare il nostro codice) tramite l' api
OpenProcess definita
nel seguente modo
function OpenProcess(dwDesiredAccess: DWORD;
bInheritHandle: BOOL;
dwProcessId: DWORD
): THandle; stdcall;
dwDesiredAccess definisce il tipo di accesso al processo: per stare sul sicuro
(per il momento, poi vedremo in seguito nei dettagli qual è effettivamente il
tipo di accesso richiesto dalle operazioni che dovremo andare ad eseguire)
assegnamo il massimo ossia PROCESS_ALL_ACCESS (definito nella unit Windows). bInheritHandle sta ad indicare se si vuole o meno (TRUE o FALSE) che l' Handle
al processo in questione (ossai il valore restituito da OpenProcess) sia
disponibile per il processi figli del processo in questione: in questo contesto
non ci interessa per cui lo impostiamo a FALSE. dwProcessId è il PID
(identificativo di processo) del processo in questione: per ottenere il PID di
un processo dato il nome dell'eseguibile si può utilizzare la seguente
procedura:
function PidProcesso(NomeProcesso: string): LongWord;
var
pe: TProcessEntry32;
hSnap: THandle;
begin
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 (pe.szExeFile = NomeProcesso) then
begin
break;
end;
until (not (Process32Next(hSnap, pe)) ) ;
CloseHandle(hSnap);
end;
(N. B. Bisogna includere la unit TlHelp32). Per poter avere un accesso
completo al processo destinazione (poterlo aprire con PROCESS_ALL_ACCESS) è bene
procedere all' abilitazione del privilegio di Debug: per l' abilitazione (ed
anche disabilitazione di privilegi) si può utilizzare la seguente procedura:
procedure ModificaPrivilegio(szPrivilege: pChar; fEnable: Boolean);
var
NewState: TTokenPrivileges;
luid: TLargeInteger;
hToken: THandle;
ReturnLength: DWord;
begin
OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES,
hToken);
LookupPrivilegeValue(nil, szPrivilege, luid);
NewState.PrivilegeCount := 1;
NewState.Privileges[0].Luid := luid;
if fEnable then
NewState.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED
else
NewState.Privileges[0].Attributes := 0;
AdjustTokenPrivileges(
hToken,
FALSE,
NewState,
sizeof(NewState),
nil,
ReturnLength);
CloseHandle(hToken);
end;
Per abilitare il privilegio di Debug al processo corrente (ossia il priogramma
che stiamo sviluppando) è sufficiente chiamare la suddetta procedura in questa
manieraModificaPrivilegio('SeDebugPrivilege', TRUE); Bene: a questo punto dobbiamo fare in modo che il processo destinazione (che
abbiamo appena aperto con accesso completo) vada a caricare la dll il cui main
contiene il codice che vogliamo che venga eseguito nel contesto del processo
destinazione; come fare??? Il caricamento della dll avviene tramite l' api
LoadLibrary che prende in input appunto il nome della dll in questione: con una
chiamata a LoadLibrary('nome.dll') un processo carica nel proprio spazio di
memoria la dll 'nome.dll'. Quindi in sostanza dobbiamo fare in modo che il
processo destinazione esegua LoadLibrary('nome.dll'). Tutto questo può essere
realizzato tramite l' api CreateRemoteThread che appunto attiva un thread nel
contesto di un qualsiasi processo remoto (diverso da quello corrente).
CreateRemoteThread è definita nel seguente modo
function CreateRemoteThread(hProcess: THandle;
lpThreadAttributes: Pointer;
dwStackSize: DWORD;
lpStartAddress: TFNThreadStartRoutine;
lpParameter: Pointer;
dwCreationFlags: DWORD;
var lpThreadId: DWORD
): THandle; stdcall;
hProcess è l' handle del processo destinazione (ottenuto tramite OpenProcess);
lpThreadAttributes non ci interessa e lo settiamo a nil; dwStackSize non ci interessa e viene quindi settato a 0. lpStartAddress è un puntatore alla procedura che
definisce l' operato del thread; lpParameter è un puntatore all' area di memoria
(nel contesto del processo destinazione) che contiene i parametri di input della
procedura riferita dal puntatore lpStartAdress. dwCreationFlags non ci interessa
e lo impostiamo quindi a 0. lpThreadId riceverà l' identificativo del thread
creato. Concentriamoci sui paramatri lpStartaddress e lpParameter
(rispettivamente procedura ed elenco dei paramatri di input della medesima): la
procedura a cui punta lpStartAddress potrebbe essere la LoadLibrary (definita
nella dll Kernel32.dll) mentre nell' area di memoria puntata da lpParameter
potrei inserire la stringa che mi definisce il nome della dll che voglio appunto
iniettare nel processo destinazione (ricordiamo infatti LoadLibrary('nome.dll')). Partiamo dall' assegnazione del paramatro
lpStartaddress (e poi passeremo a lpParamater): dobbiamo in sostanza passargli
l' indirizzo a partire dal quale è implementata la funzione LoadLibrary
(definita nella dll Kernel32.dll). Se il mio processo ha una dll di nome ad
esempio pippo.dll caricata nel proprio spazio di memoria, allora posso ottenere
un puntatore alla funzione pluto implementata nella dll in questione tramite la
chiamata seguente: GetProcAddress(GetModuleHandle('pippo.dll'), 'pluto'); Il risultato mi dice in pratica a quale indirizzo di memoria (relativamante
allo spazio di memoria utilizzato dal mio processo) inizia l' implementazione
della funzione pippo. Bene: se la stessa dll viene caricata da un altro processo
nel proprio spazio di memoria non stà scritto da nessuna parte che l' indirizzo
di memoria a partire dal quale è stata caricata sia identico all' indirizzo di
memoria a partire dal quale è stata caricata nel contesto del mio processo; in
sostanza non è garantito che una determinata dll venga caricata sempre nello
stesso punto nell' ambito dello spazio di memoria di un processo. E di
conseguenza l' indirizzo di una funzione implementata nella dll medesima
(ottenuto appunto tramite GetProcAddress) può variare a seconda del processo.
Stando a quanto detto quindi, non potrei ottenere l' indirizzo dell' api
LoadLibrary tramite una chiamata del tipo GetProcAddress(GetModuleHandle('kernel32.dll'), 'LoadLibrary'); ed utilizzarlo nel contesto di un altro processo (e quindi in sostanza
passarlo come valore del parametro lpStartaddress. Fortunatamente però la dll
Kernel32.dll (l' eccezione che conferma la regola) viene caricata sempre allo
stesso indirizzo e quindi l' operazione è effettivamante possibile: quindi posso
passare come valore del parametro lpStartAddress il valore GetProcAddress(GetModuleHandle('kernel32.dll'), 'LoadLibrary'); Bene, a questo punto è il momento del parametro lpParameter: deve essere il
valore dell' indirizzo di memoria (nel contesto del processo destinazione) a
partire dal quale è presente la stringa che mi definisce il nome della dll da
caricare. In questo ambito mi vengono in aiuto le api VirtualAllocEx e
VirutalAllocFree (per allocare e liberare memoria in un processo diverso da
quello corrente) e WriteProcessMemory (per scrivere nello spazio di memoria di
un processo diverso da quello corrente); entriamo un pò più nei dettagli
function VirtualAllocEx(hProcess: THandle;
lpAddress: Pointer;
dwSize,
flAllocationType: DWORD;
flProtect: DWORD
): Pointer; stdcall;
L' api VirtualAllocEx va ad allocare (o alternativamente a riservare) una
regione di memoria nello spazio di memoria di un determinato processo. hProcess
rappresenta l' handle del processo destinazione (ottenuto tramite OpenProcess);
lpAddress rappresenta l' indirizzo, nel contesto del processo remoto, a partire
dal quale verrà allocata la memoria: 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 esprime la
dimensione della regione di memoria che si vuole allocare: nel nostro caso sarà
la lunghezza della stringa che mi indica il nome della dll (con eventuale
percorso completo) e quindi Lenght(nome_dll). flAllocationType indica
il tipo di operazione da effettuare (in quanto la funzione VirtualAllocEx oltre
ad effettuare allocazioni di memoria si occupa anche di riservare l' area
specificata dai paramateri lpAddress e dwSize): nel nostro contesto, ossia
allocazione di memoria, dobbiamo settare il valore a MEM_COMMIT (allocazione di
memoria di dimensione pari a dwSize con inizializzazione a 0). flProtect 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 acnhe del valore PAGE_EXECUTE). La
funzione restituisce un puntatore all 'indirizzo (nel contesto del processo
destinazione) a partire dal quale vi è la regione di memoria appena allocata. Un
valore 0 indica invece un errore.
function WriteProcessMemory(hProcess: THandle;
const lpBaseAddress: Pointer;
lpBuffer: Pointer;
nSize: DWORD;
var lpNumberOfBytesWritten: DWORD
): BOOL; stdcall;
L' api WriteProcessMemory scrive nello spazio di memoria di un dato processo.
hProcess è l' Handle del processo destinazione (ricavabile tramite OpenProcess);
lpAddress indica l' indirizzo (nel contesto del processo destinazione) a partire
dal quale inizia la scrittura: può essere ad esempio il puntatore restituito da
VirtualAllocEx. lpBuffer è un puntatore (nel contesto del processo corrente)
all' informazione che deve essere scritta nel processo destinazione (analogo al
parametro dwSize dell' api VirtualAllocEx). nSize è la dimensione dell'
informazione che deve essere scritta. lpNumberOfBytesWritten restituisce il
numero di byte scritti: lo si può settare a nil per trascurarlo.
function VirtualFreeEx(hProcess: THandle;
lpAddress: Pointer;
dwSize,
dwFreeType: DWORD
): Pointer; stdcall;
L' api VirtualFreeEx va a deallocare una regione di memoria nello spazio di
memoria di un determianto processo (nel nostro caso serve per deallocare la
memoria allocata tramite VirtualAllocEx). hProcess è l' handle del processo
destinazione; lpAddress rappresenta l' indirizzo di partenza (nel contesto del
processo destinazione) della regione di memoria da deallocare (nel nostro caso è
il valore restituito da VirtualAllocEx); dwSize rappresenta la dimensione della
regione di memoria da deallocare (quanti byte devono essere deallocati a partire
dall' indirizzo specificato da lpAddress). dwFreeType indica il tipo di
operazione da effettuare sulla regione di memoria identificata da lpAddress e
dwSize (stesse considreazioni che abbiamo fatto sul parametro flAllocationType
della funzione VirtualAllocEx): il valore da asseganre è MEM_RELEASE. Se la
funzione ha successo restituisce un valore diverso da 0; 0 altrimenti (in caso
di errore). Facciamo quindi un riassuntino: 1) abilito il privilegio di debug per poter
avere accesso completo al processo destinazione 2) Ottengo un handle al
processo destinazione trmite OpenProcess 3) Vado ad allocare una regione di
memoria nello spazio di memoria del processo destinazione tramite
VirtualAllocEx: tale regione deve contenere il nome della dll (con eventuale
percorso completo); deve avere quindi una dimensione pari alla lunghezza del
nome della dll. 4) Vado a scrivere il nome della dll nello spazio di memoria del processo
destinazione a partire dall' indirizzo (relativo allo spazio di memoria del
processo destinazione) restituito dalla funzione VirtualAllocEx: per fare questo
mi servo della funzione WriteProcessMemory. 5) Creo un thread nel processo
destinazione utilizzando la funzione CreateRemoteThread: il Thread consiste
nell' esecuzione della funzione LoadLibrary implementata nella dll Kernel32.dll
e che si trova sempre nella stessa posizione nell' ambito di tutti i processi
grazie al fatto che la kernel32.dll, a differenza del comportamento di tutte le
altre dll, viene caricata sempre nello stesso punti in tutti i processi. In
pratica come indirizzo del ciodice da eseguire (parametro lpStartAddress) gli
passo l' indirizzo della LoadLibrary che vado ad ottenere tramite la seguente
chiamata GetProcAddress(GetModuleHandle('kernel32.dll'), 'LoadLibrary');
Come indirizzo a partire dal quale sono memorizzati i parametri (parametro
lpParameter), gli passo il valore restituito dalla funzione VirtualAllocEx. La
CreateRemoteThread restituisce l' identificativo del thread creato nel processo
destinazione A questo punto devo attendere la terminazione del thread appena
creato nel processo destinazione: lo posso fare con la seguente chiamata
WaitForSingleObject(Thread, INFINITE); dove Thread rappresenta appunto l'
handle del thread in questione (valore restituito da CreateRemoteThread)
ed INFINITE stà ad indicare che non vi è alcun timeout e quindi si può passare
all' istruzione successiva solo dopo che il thread è terminato Per compeltatre
l' opera (una volta concluso il thread creato nel processo remoto) si chiudono
gli handle rispettivamente al processo destinazione (ottenuto con OpenProcess) e
al thread creato nel processo destinazione (restituito da CreateRemoteThread)
passando i medesimi alla funzione CloseHandle Per finire un approfondimento
sul tipo di accesso richiesto sul processo destinazione per effettuare il tutto:
si tratta in pratica del valore da assegnare al parametro dwDesiredAccess della
funzione OpenProcess (che all' inizio abbiamo impostato pari a PROCESS_ALL_ACCESS) Bene: (elenchiamo le singole api ed il tipo di accesso
minimo richiesto) VirtualAllocEx: PROCESS_VM_OPERATION WriteProcessMemory: PROCESS_VM_OPERATION, PROCESS_VM_WRITE
CreateRemoteThread: PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, PROCESS_VM_READ VirtualFreeEx: PROCESS_VM_OPERATION Quindi: PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, PROCESS_VM_READ Di seguito il sorgente della procedura di dll injection:
procedure InjectDll(PID: dword; DLL: pChar);
var
BytesWritten, Process, Thread, ThreadId: dword;
Paramaters: pointer;
begin
ModificaPrivilegio('SeDebugPrivilege', TRUE);
Process := OpenProcess(PROCESS_CREATE_THREAD +
PROCESS_QUERY_INFORMATION +
PROCESS_VM_OPERATION +
PROCESS_VM_WRITE +
PROCESS_VM_READ,
False,
PID
);
Paramaters := VirtualAllocEx(
Process,
nil,
Length(DLL),
MEM_COMMIT,
PAGE_READWRITE);
WriteProcessMemory(Process,
Paramaters,
Pointer(DLL),
Length(DLL),
BytesWritten);
Thread := CreateRemoteThread(Process,
nil,
0,
GetProcAddress(GetModuleHandle('KERNEL32.DLL'), 'LoadLibraryA'),
Paramaters,
0,
ThreadId);
WaitForSingleObject(Thread, INFINITE);
VirtualFreeEx(Process,
Paramaters,
0,
MEM_RELEASE);
CloseHandle(Thread);
CloseHandle(Process);
end;
A questo punto non ci resta che creare una dll di esempio per completare l'
opera: per rendere l' idea della mappatura della dll nel processo remoto si può
implementare il main della dll tramite una chiamata all' api GetCommandLine e
verificare appunto che la linea di comando restuita è relativa al
processo remoto
library CommandDll;
uses
Windows,
SysUtils,
Classes;
{$R *.RES}
procedure EntryPointProc(reason: integer);
var
str: pChar;
begin
case reason of
DLL_PROCESS_ATTACH:
begin
str := GetCommandLine;
MessageBox(0, 'Ciao: ora sono un thread interno del processo che hai scelto tu', 'Ciaooo', 0);
MessageBox(0, 'Per dimostrartelo ti restituisco la linea di comando', 'Ciaooo', 0);
MessageBox(0, str, 'Ciaooo', 0);
end;
DLL_PROCESS_DETACH:
begin
end;
DLL_THREAD_ATTACH:
begin
end;
DLL_THREAD_DETACH:
begin
end;
end;
end;
begin
DLLProc := @EntryPointProc;
EntryPointProc(DLL_PROCESS_ATTACH);
end.
Per fare una prova possiamo aprire Notepad ed iniettargli la nostra dll CommandDll.dllInjectDll(PidProcesso('notepad.exe'), 'CommandDll.dll'); |