Home | Chi sono | Contattami
 

Progr. lineare

Delphi
 
Componenti
  Database
 
Miei articoli

Windows

Miei articoli 

 

Un viaggio nel mondo dell' API Hooking in User Mode


Per Hooking si intende fondamentalmente la capacità di intercettare l'esecuzione di una determinata funzione: in questa maniera si possono costruire software di monitoraggio per tracciare le chiamate a determinate funzioni raccogliendo anche i valori dei parametri ad ogni chiamata, ma anche modificare il comportamento delle funzioni in questione. Esistono diversi approcci utilizzati per raggiungere questo tipo di obiettivo: la suddivisione principale è quella che separa le tecniche di Hooking nelle categorie User Mode e Kernel Mode. Per un maggiore approfondimento delle 2 suddette categorie rimando all'immensa mole di materiale liberamente disponibile su internet con l'avvertimento preventivo che si tratta di questioni assai complesse che richiedono anni di studi ed approfondimenti e che quindi vanno evitate come la peste da chi vuole improvvisarsi hacker in una settimana partendo da un paio di articoli scaricati dal web. Il contesto è vastissimo e decisamente labirintico e va affrontato senza troppe illusioni. Bene, dopo i dovuti preamboli, nel seguito vado a descrivere una tecnica abbastanza semplice ma al contempo notevolmente efficace per la realizzazione di Hooking in User Mode.

1. Bene .. partiamo

La cosa migliore è partire da un semplice applicativo da console e procedere per gradi: prendiamo in esame quindi la semplice Console Application che segue

program test; {$APPTYPE CONSOLE} uses windows, SysUtils; procedure test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 + i2; end; var risultato: Integer; begin test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); end.

Fino a qui non ci dovrebbero essere problemi: si tratta di una semplice somma di 2 interi. Bene, a questo punto aggiungiamo la funzione test_sottrazione e vediamo come fare in modo che, chiamando la funzione test_addizione, venga invece eseguita la funzione test_sottrazione

program test; {$APPTYPE CONSOLE} uses windows, SysUtils; procedure test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 + i2; end; procedure test_sottrazione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 - i2; end; var risultato: Integer; begin test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); test_sottrazione(5, 3, risultato); Writeln(inttostr(risultato)); end.

Se vogliamo che, nel momento in cui viene chiamata una funzione, ne venga effettivamente eseguita un'altra, basta sovrascrivere i primi 6 bytes della funzione con i seguenti bytes

$68 (istruzione assembly push): 1 byte
indirizzo della funzione a cui redirigere la chiamata: 4 bytes
$C3 (istruzione assembly ret che indica la terminazione di una funzione): 1 byte


2. Approfondiamo

A questo punto è opportuno introdurre una adeguata terminologia al fine di rendere il più comprensibile possibile tutto il discorso: anzitutto definiremo

Funzione ORIGINALE: è la funzione di cui voglio intercettare l'esecuzione

Funzione PERSONALIZZATA: è la funzione che viene chiamata nel momento in cui si rileva una chiamata alla Funzione ORIGINALE; in pratica quando nel sistema viene chiamata la Funzione ORIGINALE, il sistema di hooking redirige il flusso del programma alla Funzione PERSONALIZZATA.

Abbiamo usato la terminologia Funzione PERSONALIZZATA in quanto le tecniche di Hooking vengono utilizzate principalmente per modificare il comportamento delle API di Windows (oltre che naturalmente per eseguire dei log sulle chiamate a determinate API in softwares di monitoraggio API). Si sarebbero potute usare altre terminologie chiaramente e non è questa la sede per discutere sulla maggiore o minore intuitività di determinati nomi (ammesso vi possa essere discussione costruttiva al riguardo), in ogni caso questi sono i termini più appropriati che ho deciso di scegliere per orientarmi al meglio nella realizzazione della libreria di Hooking. Sono in pratica i nomi che mi è venuto istintivo dare alle due tipologie di funzioni nella realizzazione di questa implementazione.

Nel contesto dell'esempio sopra inserito si ha:

Funzione ORIGINALE : test_addizione
Funzione PERSONALIZZATA : test_sottrazione


Quando verrà chiamata la funzione test_addizione, ciò si tradurrà in una chiamata alla funzione test_sottrazione (abbiamo quindi ottenuto una versione personalizzata della funzione test_addizione che, invece di restituire la somma di 2 interi, restituisce la loro differenza).

IMPORTANTE!: le 2 funzioni in questioni (ORIGINALE e PERSONALIZZATA) devono essere dello stesso tipo ossia avere lo stesso tipo in output e lo stesso numero di parametri in input ognuno ordinatamente dello stesso tipo.

3. Passiamo al codice

Ora vediamo di tradurre un pò in codice quanto esposto nei paragrafi precedenti: partiamo dai 6 bytes con i quali andremo a sovrascrivere i primi 6 bytes della Funzione ORIGINALE. Possono essere espressi tramite il tipo packed record come di seguito

type TJmpCode = packed record bPush: Byte; //1 byte pAddr: Pointer; //4 bytes bRet : Byte; //1 byte end;

In questo modo possiamo settare tutte le volte l'indirizzo della Funzione PERSONALIZZATA tramite il campo pAddr.

Si perviene quindi al seguente codice:

program test; {$APPTYPE CONSOLE} uses windows, SysUtils; type TJmpCode = packed record bPush: Byte; pAddr: Pointer; bRet: Byte; end; PJmpCode = ^TJmpCode; procedure test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 + i2; end; procedure test_sottrazione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 - i2; end; var risultato: Integer; JumpCode: TJmpCode; dwJmpSize: Cardinal; begin test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); JumpCode.bPush := $68; JumpCode.pAddr := @test_sottrazione; JumpCode.bRet := $C3; dwJmpSize := SizeOf(TJmpCode); CopyMemory(@test_addizione, @JumpCode, dwJmpSize); test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); end.

In pratica abbiamo definito una variabile JumpCode di tipo TJmpCode ed abbiamo settato il suo campo pAddr all'indirizzo della funzione test_sottrazione (Funzione PERSONALIZZATA) e poi abbiamo copiato JumpCode nei primi 6 bytes di test_addizione (Funzione ORIGINALE) tramite la funzione CopyMemory di Delphi. Tuttavia viene sollevata una eccezione proprio in corrispondenza dell'esecuzione della CopyMemory. Ci viene in aiuto l'API win32 VirtualProtect di cui diamo di seguito una descrizione dettagliata nel paragrafo successivo.

3.1 VirtualProtect

function VirtualProtect(lpAddress: Pointer; dwSize, flNewProtect: Cardinal; lpflOldProtect: Pointer): Boolean; stdcall;

la funzione VirtualProtect modifica il livello di protezione in una regione di pagine di memoria adiacenti e committed residenti nello spazio di memoria del processo chiamante. In particolare tutte le pagine adiacenti presenti nella suddetta regione di memoria devono appartenere alla medesima regione di memoria riservata allocata tramite chiamata all'api win32 VirtualAlloc o VirtuaAllocEx con il parametro MEM_RESERVE. Non è quindi valida una chiamata a VirtualProtect inerente pagine di memoria adiacenti che coprono complessivamente regioni di memoria riservate differenti (ossia allocate tramite differenti chiamate all'api Win32 VirtualAlloc o VirtualAllocEx col parametro MEM_RESERVE). Tutta la regione di memoria interessata deve essere stata allocata dalla medesima chiamata a VirtualAlloc o VirtualAllocEx. La funzione restituisce False in caso di errore.

Analizziamo ora i singoli parametri

lpAddress: indirizzo di base della regione di memoria di cui si vuol modificare la protezione

dwSize: dimensione della regione di memoria di cui si vuol modificare la protezione: la regione di memoria interessata conterrà tutte le pagine di memoria nell'intervallo [lpAddress, lpAddress + dwSize]

flNewProtect: nuovo livello di protezione. Il valore può essere uno dei seguenti:

In aggiunta a questi valori ci sono anche i seguenti:

PAGE_EXECUTE_WRITECOPY ($80) e PAGE_WRITECOPY ($08) che riguardano la Copy-On-Write Protection (concetto per cui rimando alla documentazione sul Platform SDK)

Per finire ci sono anche i seguenti valori (da usarsi rigorosamente in combinazione coi valori descritti nella tabella sopra) per i quali rimando ancora alla documentazione sul Platform SDK:

PAGE_GUARD, PAGE_NOCACHE, PAGE_WRITECOMBINE

Nel caso specifico nostro, useremo il valore PAGE_EXECUTE_READWRITE

lpflOldProtect: puntatore alla variabile che memorizza lo stato di protezione attuale; ci serve quando andiamo a richiamare nuovamente VirtualProtect per ripristinare lo stato di protezione originale.

Il codice corretto sarà quindi il seguente:

program test; {$APPTYPE CONSOLE} uses windows, SysUtils; type TJmpCode = packed record bPush: Byte; pAddr: Pointer; bRet : Byte; end; procedure test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 + i2; end; procedure test_sottrazione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 - i2; end; var risultato: Integer; JumpCode: TJmpCode; dwOldProtect: Cardinal; dwJmpSize: Cardinal; begin //eseguo "test_addizione" ed ottengo la somma come risultato test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); //definisco il salto alla "test_sottrazione" JumpCode.bPush := $68; JumpCode.pAddr := @test_sottrazione; JumpCode.bRet := $C3; dwJmpSize := SizeOf(TJmpCode); //cambio la protezione sui primi 6 bytes di "test_addizione" VirtualProtect(@test_addizione, dwJmpSize, PAGE_EXECUTE_READWRITE, dwOldProtect); //copio l'istruzione di salto a "test_sottrazione" nei primi 6 bytes di "test_addizione" CopyMemory(@test_addizione, @JumpCode, dwJmpSize); //ripristino la protezione originale sui primi 6 bytes di "test_addizione" VirtualProtect(@test_addizione, dwJmpSize, dwOldProtect, nil); //eseguo "test_addizione" ed ottengo come risultato la differenza test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); end.

4. Progrediamo ancora

A questo punto sappiamo come modificare a nostro piacimento una funzione qualsiasi intercettandone l'esecuzione e redirigendo il flusso di programma alla nostra funzione personalizzata. Il problema è che nella maggior parte dei casi si vuole rilevare l'esecuzione di una determinata funzione, fare un log dell'avvenuta chiamata con valori dei relativi parametri oppure eseguire determinate operazioni in presenza di determinati valori dei parametri: sintetizzando il tutto, si vuole rilevare l'esecuzione di una determinata funzione mantenendo comunque il completo funzionamento della funzione in questione. Supponiamo ad esempio di voler rilevare tutte le chiamate all'api win32 CreateFileW per poter eseguire un log di tutti i file create e/o eseguire determinate operazioni in corrispondenza della creazione di un file o in generale qualsiasi altra cosa. E' chiaro che con quello che abbiamo fatto finora, rileviamo la creazione di un file ma, sostituendo l'api CreateFileW con la chiamata alla nostra funzione, impediamo l'esecuzione della CreateFileW originale impedendo quindi la creazione del file. Bisogna quindi studiare ulteriormente la situazione al fine di garantire il corretto e completo funzionamento originale dell'api che vuol essere intercettata. Come prima cosa riscriviamo il codice di esempio sostituendo la funzione test_sottrazione con la funzione log_test_addizione che esegue appunto la stampa a video della funzione test_addizione con i valori dei parametri ogni volta che viene chiamata la funzione test_addizione

program test; {$APPTYPE CONSOLE} uses windows, SysUtils; type TJmpCode = packed record bPush: Byte; //1 byte pAddr: Pointer; //4 bytes bRet : Byte; //1 byte end; procedure test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 + i2; end; procedure log_test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin Writeln('log_test_addizione(' + inttostr(i1) + ', ' + inttostr(i2) + ')'); end; var risultato: Integer; JumpCode: TJmpCode; dwOldProtect: Cardinal; dwJmpSize: Cardinal; begin //eseguo "test_addizione" ed ottengo la somma come risultato test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); //definisco il salto alla "log_test_addizione" JumpCode.bPush := $68; JumpCode.pAddr := @log_test_addizione; JumpCode.bRet := $C3; dwJmpSize := SizeOf(TJmpCode); //cambio la protezione sui primi 6 bytes di "test_addizione" VirtualProtect(@test_addizione, dwJmpSize, PAGE_EXECUTE_READWRITE, dwOldProtect); //copio l'istruzione di salto a "log_test_addizione" nei primi 6 bytes di "test_addizione" CopyMemory(@test_addizione, @JumpCode, dwJmpSize); //ripristino la protezione originale sui primi 6 bytes di "test_addizione" VirtualProtect(@test_addizione, dwJmpSize, dwOldProtect, nil); //eseguo "test_addizione" ed ottengo come risultato una stringa che contiene il //nome della funzione coi valori dei 2 parametri test_addizione(5, 3, risultato); end.

Come si può notare, quando chiamo la funzione test_addizione mi viene stampato a video il nome della funzione con i valori dei parametri di input, ma la funzione test_addizione non viene eseguita ossia non viene fatta la somma dei 2 valori di input i1 e i2. Partendo da queste osservazioni si deduce che è necessario salvarsi da qualche parte quei primi 6 bytes della Funzione ORIGINALE in maniera tale da poter chiamare la Funzione ORIGINALE all'interno della Funzione PERSONALIZZATA. Occorre definire la cosidetta Funzione TRAMPOLINO

Funzione TRAMPOLINO: funzione che contiene le istruzioni assembly che occupano i primi 6 bytes della Funzione ORIGINALE e, a seguire, una istruzione di salto all'istruzione assembly immediatamente successiva alle precedenti; in questo modo si può ricostruire l'esecuzione completa della Funzione ORIGINALE.

IMPORTANTE!: come nel caso della Funzione PERSONALIZZATA, la Funzione TRAMPOLINO deve essere dello stesso tipo della Funzione ORIGINALE ossia deve avere lo stesso tipo in output e lo stesso numero di parametri in input ognuno ordinatamente dello stesso tipo

Complessivamente, il tutto può essere sintetizzato attraverso le 2 seguenti figure

Figura 1

 

Figura 2

 


Analizziamo nel dettaglio la Figura 1: (la Figura 2 contiene gli stessi concetti)

Vengono rappresentate la Funzione ORIGINALE e la Funzione TRAMPOLINO.
1) Le istruzioni assembly che occupano i primi 6 bytes della Funzione ORIGINALE, vengono salvate nella Funzione TRAPOLINO
2) Successivamente, dopo le istruzioni salvate, sempre nella Funzione TRAMPOLINO viene inserita l'istruzione di salto alla Funzione ORIGINALE (più precisamente all'indirizzo immediatamente successivo alle istruzioni precedentemente salvate): quindi chiamando la Funzione TRAMPOLINO, eseguo in pratica tutta la Funzione ORIGINALE

La prima cosa che salta all'occhio è il problema di determinare qual è la dimensione complessiva delle istruzioni assembly che comprono i primi 6 bytes della Funzione ORIGINALE (SizeAll). Questa sarà la dimensione del blocco di bytes che dovrò salvare nella Funzione TRAMPOLINO. Come fare? Ci serve un disassemblatore. Ci sono esempi in C/C++, Delphi, etc... tuttavia la soluzione più immediata è utilizzare un Length Disassmbler: cos'è un Length Disassembler??? Un Length Disassembler è una variante mini di un classico disassemblatore che, invece di restituire il codice delle istruzioni assemby, restituisce la dimensione dell'istruzione assembly definita ad un indirizzo specifico. Il Length Disassembler per antonomasia è quello realizzato da Z0mbie e denominato LDE32 (Length Disassembler Engine per sistemi a 32 bit). Quello che useremo è la versione 1.05, totalmente in assembler. In particolare ci interfacceremo da Delphi direttamente col file compilato .obj. Il .obj ha una dimensione ridottissima (3 KB) e fornisce una unica funzione di nome disasm_main che prende in input un indirizzo e restituisce la dimensione dell'istruzione Assembly presente a partire da quell'indirizzo (oppure -1 in caso di errore). Utilizzare questa funzione da Delphi è semplicissimo

... ... implementation {$L LDE32.OBJ} //funzioni di LDE32.OBJ (Length Disassembler Engine di Z0mbie) function disasm_main(addr: Pointer): Integer; cdecl; external;

In pratica con la direttiva $L includo il .obj e poi dichiaro la funzione implementata con un external in fondo. Bene, a questo punto possiamo crearci una funzione che, dato un indirizzo di memoria pFunction ed una dimensione n, restituisce la dimensione totale delle istruzioni assembly che coprono l'area di memoria da pFunction a pFunction + n.

//determina la dimensione totale delle istruzioni assembler consecutive presenti nei primi n bytes function SizeAllInFirstNBytes(pFunction: Pointer; n: Cardinal): Integer; var instrSize: Integer; SizeAll: Cardinal; ptr: Pointer; error: boolean; begin SizeAll := 0; ptr := pFunction; try repeat instrSize := disasm_main(ptr); Error := (instrSize = -1); Inc(SizeAll, instrSize); ptr := Pointer(Cardinal(ptr) + instrSize); until ((SizeAll >= n) or Error); except error := True; end; if error then Result := -1; else Result := SizeAll; end;

5. Realizzazione del codice sorgente

Di seguito andiamo ad esporre il codice sorgente relativo alle 2 figure viste in precedenza

5.1 Figura 1

1) Calcolo la dimensione totale in bytes delle istruzioni assembly che coprono i primi 6 bytes e se riscontro un errore (risultato = -1) allora esco direttamente:

SizeAll := SizeAllInFirstNBytes(pOriginalFunction, 6); if SizeAll = -1 then Exit;

2) Vado ad allocare memoria per la Funzione TRAMPOLINO: la dimensione sarà la somma di SizeAll (dimensione totale delle istruzioni assembly che coprono i primi 6 bytes della Funzione ORIGINALE e che devono essere salvate nella Funzione TRAMPOLINO) e 6

pTrampolineFunction := VirtualAlloc(nil, dwJmpSize + 6, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);

3) Copio le istruzioni dalla Funzione ORIGINALE alla Funzione TRAMPOLINO: non c'è bisogno di eseguire un cambio di protezione sull'area di memoria di destinazione (Funzione TRAMPOLINO) in quanto il livello di protezione di quest'area è già stato impostato a PAGE_EXECUTE_READWRITE dalla VirtualAlloc precedente

CopyMemory(pTrampolineFunction, pOriginalFunction, SizeAll);

4) Inserisco nella Funzione TRAMPOLINO il salto alla Funzione ORIGINALE (indirizzo pOriginalFunction+SizeAll )

JumpCode.bPush := $68; JumpCode.pAddr := Pointer(Cardinal(pOriginalFunction) + SizeAll) JumpCode.bRet := $C3; CopyMemory(Pointer(Cardinal(pTrampolineFunction) + SizeAll), @JumpCode, 6);

5.2 Figura 2

1) Cambio il livello di protezione dei primi 6 bytes della Funzione ORIGINALE

VirtualProtect(pOriginalFunction, 6, PAGE_EXECUTE_READWRITE, dwOldProtect);

2) Inserisco nei primi 6 bytes della Funzione ORIGINALE il salto alla Funzione PERSONALIZZATA

JumpCode.bPush := $68; JumpCode.pAddr := pCustomFunction JumpCode.bRet := $C3; CopyMemory(pOriginalFunction, @JumpCode, 6)

5.3 Funzione per l'Hooking

function HookCode(pOriginalFunction, pCustomFunction: Pointer; var pTrampolineFunction: Pointer): Boolean; stdcall; var sName : String; dwSizeAll : Integer; dwOldProtect: Cardinal; dwJmpSize : Cardinal; JumpCode : TJmpCode; Begin dwSizeAll := 0; dwJmpSize := SizeOf(Tjmpcode); JumpCode.bPush := $68; JumpCode.bRet := $C3; Result := False; (*calcolo la dimensione totale in byte delle istruzioni assembler consecutive che si trovano nei primi 6 byte a partire dall'indirizzo pOriginalFunction il risultato viene salvato nella variabile dwSizeAll*) dwSizeAll := SizeAllInFirstNBytes(pOriginalFunction, dwJmpSize); if dwSizeAll = -1 then Exit; //è stata trovata una istruzione assembler non valida (*anche se la riscrittura dei primi 6 bytes della Funzione ORIGINALE avviene solo dopo aver salvato le istruzioni nella Funzione TRAMPOLINO è bene accertarsi fin da subito che si possa scrivere in tale area di memoria; in caso negativo tanto vale uscir subito*) if (not VirtualProtect(pOriginalFunction, dwJmpSize, PAGE_EXECUTE_READWRITE, dwOldProtect)) then Exit; pTrampolineFunction := VirtualAlloc(nil, dwJmpSize + dwSizeAll, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (pTrampolineFunction = nil) then Exit; {Non è stato possibile allocare memoria} try (*copio le istruzioni assembler che si trovano nei primi 6 bytes a partire da pOriginalFucntion (dimensione totale dwSizeAll), nello spazio di memoria puntato da pTrampolineFunction *) CopyMemory(pTrampolineFunction, pOriginalFunction, dwSizeAll); (*setto l'istruzione di salto in maniera tale che punti all'indirizzo immediatamente successivo alle istruzioni che abbiamo appena copiato *) JumpCode.pAddr := Pointer(DWord(pOriginalFunction) + dwSizeAll); (*copio l'istruzione di salto (dimensione dwJumpSize) nelle posizioni immediatamente successive *) CopyMemory(Pointer(Cardinal(pTrampolineFunction) + dwSizeAll), @JumpCode, dwJmpSize); (*setto l'istruzione di salto in maniera tale che punti alla Funzione PERSONALIZZATA*) JumpCode.pAddr := pCustomFunction; (*copio l'istruzione di salto (dimensione dwJumpSize) all'indirizzo della funzione originale*) CopyMemory(pOriginalFunction, @JumpCode, dwJmpSize); Result := True; except (*se c'è stato qualche problema nell'hooking della funzione allora libero la memoria precedentemente allocata *) VirtualFree(pTrampolineFunction, dwJmpSize + dwSizeAll,MEM_RELEASE); end; //ripristino lo stato di protezione originale della Funzione ORIGINALE VirtualProtect(pOriginalFunction, dwJmpSize, dwOldProtect, dwOldProtect); end;

6. Unhooking

E' importante osservare che, se all'interno dell'implementazione della Funzione PERSONALIZZATA, vado a chiamare la Funzione Trampolino, in pratica ricostruisco il flusso completo di esecuzione della Funzione ORIGINALE.

Bene, a questo punto è opportuno esaminare anche la procedura che consente di ripristinare la situazione così come in principio ossia la procedura di Unhooking. Anche in questo caso ci viene in aiuto il seguente grafico



 

Come nei casi precedenti scriviamo in pseudcodice i punti salienti dell'operazione; N.B. eseguiremo un completo Unhooking prendendo in input solo il puntatore alla Funzione TRAMPOLINO. Dunque, conosco solo il puntatore alla Funzione TRAMPOLINO (pTrampolineFunction) e voglio ripristinare la situazione iniziale nella Funzione ORIGINALE: ecco come procedere

1) Come prima cosa devo determinare l'indirizzo della Funzione ORIGINALE (ossia il valore di pOriginalFunction). Per fare questo devo accedere al blocco situato all'indirizzo pTrampolineFunction + SizeAll: il blocco di 6 bytes che si trova all'indirizzo in questione rappresenta il salto alla Funzione ORIGINALE (nello specifico definisce un salto all'indirizzo immediatamente successivo alle istruzioni assembly salvate nella Funzione TRAMPOLINO) quindi contiene il valore pOriginalFunction + SizeAll . Se conosco SizeAll conosco di conseguenza anche pTrampolineFunction + SizeAll e, di conseguenza, ottengo il valore di pOriginalFunction (sottraendo SizeAll alla quantità pOriginalFunction + SizeAll) . Il valore SizeAll era stato determinato nel corso della procedura di Hooking utilizzando la funzione SizeAllInFirstNBytes. Ed è proprio questa funzione che andremo ad usare anche in questo caso, passandogli come indirizzo il valore pTrampolineFunction.

SizeAll := SizeAllInFirstNBytes(pTrampolineFunction, 6) CopyMemory(@JmpCode, Pointer(Cardinal(pTrampolineFunction) + SizeAll), 6); pOriginalFunction := Pointer(Cardinal(JmpCode.pAddr) - SizeAll);

2) Ora che conosco l'indirizzo della Funzione ORIGINALE, posso procedere a ripristinare i primi 6 bytes copiandoli dalla Funzione Trampolino

VirtualProtect(pOriginalFunction, 6, PAGE_EXECUTE_READWRITE, dwOldProtect) CopyMemory(pOriginalFunction, pTrampolineFunction, 6);

3) A questo punto posso deallocare la Funzione Trampolino

VirtualFree(pTrampolineFunction,0,MEM_RELEASE);

Si perviene quindi alla seguente funzione per l'Unhooking

function UnhookCode(var pTrampolineFunction: Pointer): Boolean; stdcall; var sName : String; dwSizeAll : Integer; dwOldProtect: Cardinal; dwJmpSize : Cardinal; JmpCode : TJmpCode; pOriginalFunction: Pointer; Begin dwSizeAll := 0; dwJmpSize := SizeOf(TJmpCode); JmpCode.bPush := $00; JmpCode.bRet := $00; Result := False; if (pTrampolineFunction = nil) then Exit; {calcolo dei bytes totali occupati dalle istruzioni assembler che si trovano nei primi 6 byte} dwSizeAll := SizeAllInFirstNBytes(pTrampolineFunction, dwJmpSize); if dwSizeAll = -1 then Exit; //è stata trovata una istruzione assembler non valida {non riesco a leggere i primi 6 bytes della funzione trampolino (che devo andare a copiare nei primi 6 bytes della funzione originale ripristinando quindi la funzione originale} if (isBadReadPtr(pTrampolineFunction,dwJmpSize)) then Exit; (*otteniamo il record TJmpCode presente nella funzione trampolino (dopo le istruzioni assembler di dimensione totale dwSizeAll copiate dalla funzione originale)*) CopyMemory(@JmpCode, Pointer(DWord(pTrampolineFunction) + dwSizeAll), dwJmpsize); (*se non c'è un record TJmpCode allora vuol dire che questa non è una funzione trampolino*) if (JmpCode.bPush <> $68) or (JmpCode.bRet <> $C3) then Exit; (*JmpCode.pAddr è l'indirizzo, nella funzione originale, immediatamente successivo alle istruzioni assembler copiate nella funzione trampolino. Pointer(DWord(JmpCode.pAddr) - dwSizeAll) è l'indirizzo della funzione originale. I primi dwJmpSize bytes della funzione originale contengono il salto alla funzione custom *) pOriginalFunction := Pointer(DWord(JmpCode.pAddr) - dwSizeAll); {non riesco a leggere i primi 6 bytes della funzione originale} if (isBadReadPtr(pOriginalFunction,dwJmpSize)) then Exit; if (not VirtualProtect(pOriginalFunction, dwJmpSize, PAGE_EXECUTE_READWRITE, dwOldProtect)) then Exit; {vado a riscrivere i primi 6 byte nella funzione originale copiandoli dai primi 6 byte della funzione trampolino} CopyMemory(pOriginalFunction, pTrampolineFunction, dwJmpSize); (*ripristino la protezione originale (avevo in precedenza impostato il livello di accesso PAGE_EXECUTE_READWRITE per eseguire la copia dei bytes)*) VirtualProtect(pOriginalFunction, dwJmpSize, dwOldProtect, dwOldProtect); Result := True; //vado a deallocare la funzione trampolino VirtualFree(pTrampolineFunction,0,MEM_RELEASE); end;

Come si può notare dal codice è opportuno fare le dovute verifiche sugli indirizzi di memoria ottenuti per gestire ad esempio la situazione in cui pTrampolineFunction non faccia riferimento ad una effettiva funzione trampolino o altre situazioni problematiche. Si è fatto un largo uso dell'api win32 IsBadReadPtr che verifica che il processo chiamante abbia accesso in lettura all'area di memoria specificata per evitare di lavorare con zone di memoria inaccessibili e prevenire errori.

7. Riscriviamo l'esempio iniziale

program test; {$APPTYPE CONSOLE} uses windows, SysUtils, uHook; var risultato: Integer; Trampolinetest_addizione: procedure(i1: Integer; i2: integer; var ris: Integer); stdcall; procedure test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin ris := i1 + i2; end; procedure log_test_addizione(i1: Integer; i2: integer; var ris: Integer); stdcall; begin Writeln('log_test_addizione(' + inttostr(i1) + ', ' + inttostr(i2) + ')'); Trampolinetest_addizione(i1, i2, ris); end; begin test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); risultato := 0; HookCode(@test_addizione, @log_test_addizione, @Trampolinetest_addizione); test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); risultato := 0; UnhookCode(@Trampolinetest_addizione); test_addizione(5, 3, risultato); Writeln(inttostr(risultato)); end.

uHook è la Unit che contiene l'implementazione delle 2 funzioni HookCode e UnhookCode. Dall'esempio si può verificare appunto che quando setto l'Hook su test_addizione viene chiamata la funzione log_test_addizione ma viene preservata l'esecuzione della funzione test_addizione eseguendo la somma dei 2 parametri di input.

ApiHookingUserMode

Z0mbie engine

 

 

 

 
 
Your Ad Here