Per quanto riguarda questo corso introduttivo sulla programmazione dei device driver in Windows direi che è tutto. Il viaggio è ancora lungo ma arrivati a questo punto si dovrebbe essere perfettamente in grado di proseguire con lo studio attingendo alle risorse che si trovano in rete e ai libri che trattano l'argomento. Il mio consiglio è di leggere il libro di Pavel Yosifovich ed integrare con quello di Walter Oney. Fatto questo si dovrebbe cercare altresì di comprendere e testare un po' del codice fornito nel paragrafo "Codice di riferimento" (si veda sotto) e consultare il resto delle risorse elencate solo all'occorrenza e per argomenti specifici (anche se una sfogliata ai vecchi numeri di NT Insider ed una rapida lettura di alcune parti di Windows Internals non possono che far bene).
Testi di riferimento:
Windows Kernel Programming - Pavel Yosifovich
https://www.amazon.com/Windows-Kernel-Programming-Pavel-Yosifovich/dp/1977593372
https://leanpub.com/windowskernelprogramming
Visualizzazione post con etichetta Windows. Mostra tutti i post
Visualizzazione post con etichetta Windows. Mostra tutti i post
venerdì 11 ottobre 2019
giovedì 3 ottobre 2019
23 - I/O Manager, ci penso io!
Nella scorsa lezione si è visto che è possibile intercettare richieste fatte ad un altro device per soddisfarle o passarle ad un altro device ma, in alcuni casi, si può andare ben oltre. Ad esempio, è possibile creare le richieste (nel senso di costruire l'IRP relativo), inviarle ad altri device e smaltirle una volta soddisfatte, riducendo così al minimo il supporto fornito dall'I/O Manager. In un'altra lezione, dove si è discusso di quella parte della Kernel API specifica per i file (si veda [2]), si sono scritte alcune funzioni che, alla fine, non facevano altro che invocare alcuni metodi dell'API per creare, leggere o scrivere un file (ad esempio ZwCreateFile, ZwReadFile, ZwWriteFile, ecc.). Quello che fanno queste funzioni, in sostanza, è inviare una richiesta al device del disco (in realtà al device del volume montato su tale disco) con l'aiuto dell'I/O Manager. Ma se si volesse evitare di invocare queste funzioni per raggiungere lo stesso risultato si perderebbe inevitabilmente anche il supporto dell'I/O Manager. Poco male perché, in effetti, è possibile costruirsi l'IRP della richiesta ed inviarla al disco direttamente: tutto quello che serve è solo l'indirizzo del device object a cui vogliamo inviare la richiesta, il resto lo farà IoCallDriver, come visto nella lezione precedente.
Il codice di esempio di questa lezione è tratto da [1] e, data la mole rilevante, richiede un approccio diverso da quello usato finora. Benché, nella maggior parte dei casi, si tratti di roba già vista in alcune parti di codice emergono dettagli ad un livello estremamente basso. Spiegare quindi le istruzioni riga per riga, con commenti o con lo spiegone finale dopo una funzione, è qualcosa di improponibile perché risulterebbe semplicemente illeggibile o pesante da digerire. Per questo motivo si è preferito inserire solo i commenti necessari, fornendo link a riferimenti dove poter leggere tutte le spiegazioni del caso. Purtroppo più si va in profondità e più il numero di risorse diminuisce, la documentazione ufficiale diventa più criptica (o praticamente assente) e l'arte di andarsi a cercare le informazioni necessarie diventa indispensabile. Per questo motivo consiglio di non saltare la presente lezione e proseguire nella lettura, leggendo anche tutti i riferimenti inseriti nei commenti al codice. Questo perché contengono un gran numero di informazioni che sicuramente torneranno utili in futuro. Ad ogni modo, tutto quello che fa l'esempio di codice di questa lezione è eseguire una serie di operazioni sui file. In particolare, si crea un file chiamato test.txt in C:\, si scrive e si legge su tale file, si calcola la sua dimensione ed infine si elencano i file presenti nella directory C:\
Il codice di esempio di questa lezione è tratto da [1] e, data la mole rilevante, richiede un approccio diverso da quello usato finora. Benché, nella maggior parte dei casi, si tratti di roba già vista in alcune parti di codice emergono dettagli ad un livello estremamente basso. Spiegare quindi le istruzioni riga per riga, con commenti o con lo spiegone finale dopo una funzione, è qualcosa di improponibile perché risulterebbe semplicemente illeggibile o pesante da digerire. Per questo motivo si è preferito inserire solo i commenti necessari, fornendo link a riferimenti dove poter leggere tutte le spiegazioni del caso. Purtroppo più si va in profondità e più il numero di risorse diminuisce, la documentazione ufficiale diventa più criptica (o praticamente assente) e l'arte di andarsi a cercare le informazioni necessarie diventa indispensabile. Per questo motivo consiglio di non saltare la presente lezione e proseguire nella lettura, leggendo anche tutti i riferimenti inseriti nei commenti al codice. Questo perché contengono un gran numero di informazioni che sicuramente torneranno utili in futuro. Ad ogni modo, tutto quello che fa l'esempio di codice di questa lezione è eseguire una serie di operazioni sui file. In particolare, si crea un file chiamato test.txt in C:\, si scrive e si legge su tale file, si calcola la sua dimensione ed infine si elencano i file presenti nella directory C:\
domenica 29 settembre 2019
22 - Filter Driver
In modo simile a quanto visto nella lezione precedente, in generale è quasi sempre possibile per un device intercettare le richieste fatte ad un altro device. Ad esclusione del file system (che richiede l'uso del Filter Manager ed i Minifilter) e dei device aperti in modo esclusivo (per ovvi motivi), per un driver è possibile "attaccare" il device di cui si occupa in cima ad uno stack relativo al device di cui intende intercettare le richieste (targert device) ed i cui elementi (dello stack) sono device object. Le richieste verranno trasmesse dall'I/O Manager a partire dal device più in alto nello stack (top device). Il driver del top device a quel punto può decidere se completare la richiesta o passarla al device che sta sotto di lui (lower device). Quello che non può fare, però, è trascurare la richiesta perché il client potrebbe ricevere un errore di operazione non valida ingiustificato rispetto ad una operazione che dovrebbe invece essere offerta dal target device. L'immagine sotto è una rappresentazione semplificata di questo meccanismo di filtraggio ma è sufficiente a spiegare quanto affrontato in questa lezione (per una discussione più approfondita si veda [4]).
sabato 28 settembre 2019
21 - Comunicazione Client / Driver: Minifilter
Nelle lezioni precedenti si sono esaminati alcuni tipi di supporto nella comunicazione tra client e driver (nessuna, buffered e direct I/O) ma tutte e tre sono praticamente comunicazioni monodirezionali dove il client interroga il driver inviando una richiesta senza nessuna possibilità da parte di quest'ultimo di poter comunicare qualcosa al client in maniera diretta. I minifilter contemplano invece questa possibilità fornendo un tipo di comunicazione bidirezionale tra client e minifilter. Se si è seguita la lezione precedente sul monitoraggio dei file attraverso i minifilter non si dovrebbero incontrare particolari difficoltà nell'affrontare questa lezione in quanto ripropone lo stesso codice aggiungendo giusto qualche istruzione in più. L'esempio presentato mostra un client in attesa di comunicazione da parte del minifilter riguardo le scritture avvenute su tutti i file nominati readme.txt. Si è deciso invece di non implementare nessun tipo di comunicazione da parte del client al driver (benché contemplata) in quanto un minifilter è pur sempre un normale driver e per le comunicazioni dal client al server si possono usare le normali richieste usate finora: mettere a disposizione, da parte del driver, ulteriore codice da eseguire, al servizio del client, è qualcosa che dovrebbe essere fatta solo se realmente necessario.
domenica 22 settembre 2019
20 - Monitoraggio: File (Minifilter)
Parlare di File System, Filter Manager e Minifilter in maniera approfondita richiederebbe, come minimo, un corso separato ma questo non esclude il fatto che si possano introdurre i concetti generali e scrivere un esempio di codice funzionante come quello proposto in questa lezione e che permette di monitorare (e volendo anche impedire) le operazioni di I/O sui file. I principi generali non sono difficili da comprendere e l'ostacolo del copioso codice iniziale da scrivere viene agevolmente superato grazie all'uso di un template di Visual Studio specifico per i Minifilter che genera quasi più del 90% del codice necessario.
File System, Filter Manager e Minifilter
Il ruolo dei Minifilter è quello di prendere in gestione le richieste fatte al disco fisico prima che queste vengano intercettate dal driver del File System che si occupa di processare ed inviare tali richieste ai driver che dialogano direttamente con tale disco fisico.
File System, Filter Manager e Minifilter
Il ruolo dei Minifilter è quello di prendere in gestione le richieste fatte al disco fisico prima che queste vengano intercettate dal driver del File System che si occupa di processare ed inviare tali richieste ai driver che dialogano direttamente con tale disco fisico.
giovedì 19 settembre 2019
19 - Monitoraggio: Orologio di Sistema (Callback Object)
Probabilmente monitorare i cambiamenti apportati all'orologio di sistema non è propriamente tra le nozioni principali o i concetti prioritari per chi inizia a programmare in kernel mode ma sicuramente è il modo più semplice di introdurre i Callback Object. Finora si è visto come sia possibile essere notificati dal sistema quando si verifica un dato evento. E' bene sapere che è possibile non solo per il sistema ma anche per qualsiasi driver emettere delle notifiche. A questo scopo un driver deve creare un Callback Object che altro non è che un oggetto che conserva tutti i puntatori delle funzioni di callback da invocare quando si verifica un certo evento di interesse. A loro volta i driver interessati alla notifica di un evento non devono fare altro che registrare la propria funzione di callback nel Callback Object del driver che intende mandare le notifiche per quello stesso evento. Non si entrerà nel dettaglio su come creare un Callback Object per notificare un evento ad altri soggetti interessati (anche se è bene sapere che ciò è possibile), piuttosto si sfrutteranno i Callback Object predefiniti di sistema. Ogni Callback Object deve avere un nome per essere referenziato ed i nomi dei Callback Object predefiniti si possono osservare con WinObj
domenica 15 settembre 2019
18 - Monitoraggio: Accesso alla rete (WFP)
Se ad oggi non esistono moltissimi articoli, tutorial ed esempi di codice (libri neanche a parlarne) riguardo la Windows Filtering Platform (WFP) o i Callout Driver è perché si tratta di una piattaforma articolata e abbastanza difficile da maneggiare. D'altro canto se ne percepisce subito l'enorme potenziale grazie all'elevato numero di funzionalità che può offrire: si va dal semplice monitoraggio di connessioni in entrata o uscita alla completa ispezione o modifica del pacchetti inviati e ricevuti. In pratica una manna dal cielo per sviluppatori di applicazioni tipo firewall, antivirus e tool di monitoraggio della rete.
In un articolo introduttivo come questo, però, non si può far altro che scalfirne la superficie spiegando come funziona in generale la piattaforma per poi prendere in considerazione solo quella parte funzionale alla lezione e che riguarda appunto il monitoraggio dell'accesso in uscita alla rete. In particolare, il codice di questa lezione è tratto da [1] e [2] e prevede la scrittura di un Callout Driver interessato ai processi che richiedono una connessione in uscita alla rete e che blocca, a titolo di esempio, quelle fatte da Microsoft Edge. Il risultato è quello che si può vedere nelle immagini sotto. Diversamente dalle precedenti lezioni, non si fornirà una analisi dettagliata di tutte le funzioni, strutture e valori coinvolti in quanto richiederebbe un corso separato. Ad ogni modo, quasi ogni riga di codice è stata commentata e non si dovrebbero incontrare particolari problemi nella comprensione del codice.
In un articolo introduttivo come questo, però, non si può far altro che scalfirne la superficie spiegando come funziona in generale la piattaforma per poi prendere in considerazione solo quella parte funzionale alla lezione e che riguarda appunto il monitoraggio dell'accesso in uscita alla rete. In particolare, il codice di questa lezione è tratto da [1] e [2] e prevede la scrittura di un Callout Driver interessato ai processi che richiedono una connessione in uscita alla rete e che blocca, a titolo di esempio, quelle fatte da Microsoft Edge. Il risultato è quello che si può vedere nelle immagini sotto. Diversamente dalle precedenti lezioni, non si fornirà una analisi dettagliata di tutte le funzioni, strutture e valori coinvolti in quanto richiederebbe un corso separato. Ad ogni modo, quasi ogni riga di codice è stata commentata e non si dovrebbero incontrare particolari problemi nella comprensione del codice.
giovedì 12 settembre 2019
17 - Monitoraggio: Handle (Processi e Thread)
In una lezione precedente si è visto come fosse possibile monitorare eventi come la creazione o distruzione di thread e processi ma queste non sono le uniche operazioni per cui è possibile essere notificati. Quando si parla di thread e processi è possibile monitorare anche le operazioni sugli handle a tali oggetti: in particolare, si possono ricevere notifiche quando un handle, ad un thread o processo, viene creato, aperto o duplicato. Per fare in modo che il nostro driver riceva tali notifiche è necessario registrare una o più funzioni di callback tramite il metodo ObRegisterCallbacks ed annullare poi tale registrazione con ObUnRegisterCallbacks quando non si è più interessati a ricevere tali notifiche. Il fatto che si possano registrare una o più funzioni di callback può risultare alquanto bizzarro all'inizio ma il fatto è che ObRegisterCallbacks permette di registrare in un colpo solo funzioni di callback per operazioni diverse (creazione, apertura o copia di handle), per oggetti diversi (thread e processi) e, per ogni combinazione possibile di queste ultime due, perfino indicare due funzioni di callback che vengono invocate una prima e l'altra dopo che l'operazione sull'handle venga portata a termine. Il fatto che tutto ciò possa sembrare complicato non deve destare preoccupazione in quanto spesso si usa ObRegisterCallbacks per registrare una sola callback per una sola operazione e per un solo tipo di oggetto, che riduce il caso a quello visto nelle lezioni precedenti. Il thread che eseguirà le funzione di callback è (contrariamente a quello che si può intuire leggendo la documentazione ufficiale) quello che da il via all'operazione di creazione, apertura o copia dell'handle.
NTSTATUS ObRegisterCallbacks( POB_CALLBACK_REGISTRATION CallbackRegistration, PVOID *RegistrationHandle );
void ObUnRegisterCallbacks( PVOID RegistrationHandle );
sabato 7 settembre 2019
16 - Monitoraggio: Registro
Per fare in modo che il proprio driver riceva notifiche quando c'è un'operazione che coinvolge il registro di sistema è necessario registrare una funzione di callback con CmRegisterCallback o CmRegisterCallbackEx ed annullare tale registrazione con CmUnRegisterCallback quando non si è più interessati a ricevere tali notifiche. Il thread che eseguirà tale funzione di callback sarà quello che sta per effettuare o ha effettuato l'operazione sul registro.
NTSTATUS CmRegisterCallback( PEX_CALLBACK_FUNCTION Function, PVOID Context, PLARGE_INTEGER Cookie );
NTSTATUS CmUnRegisterCallback( LARGE_INTEGER Cookie );
giovedì 5 settembre 2019
15 - Monitoraggio: Processi, Thread e Moduli
Arrivati a questo punto sarebbe interessante vedere quali strumenti il kernel offre per il monitoraggio e la notificazione. In questa lezione si vedrà come fare in modo che il proprio driver riceva notifiche quando un processo sta per essere creato o distrutto, un thread è stato creato o distrutto oppure quando un modulo (sia esso una DLL, un eseguibile o un driver) sta per essere caricato in memoria. Il codice di esempio di questa lezione è tratto da [1] e [2].
martedì 3 settembre 2019
14 - Kernel API: Altre funzioni (Driver e Thread di sistema)
In modo molto simile a quanto mostrato nella precedente lezione (si veda [1]), si vedrà adesso come elencare i driver caricati ed avviati nel sistema. Inoltre si creerà un thread di sistema per svolgere un compito banale che, però, mostrerà come usare gli eventi per mettersi in attesa fino a che tale thread non termina il suo lavoro. Il codice presentato in questa lezione è tratto da [2].
venerdì 30 agosto 2019
13 - Kernel API: Altre funzioni (Processi, Thread e DLL)
In questa lezione di stampo prettamente pratico si uscirà leggermente dal seminato per fare luce sul lato oscuro della Kernel API. L'esempio di codice proverà ad elencare tutti i processi in esecuzione sul sistema, i relativi thread e le DLL caricate per tale processo. In realtà esistono alcune funzioni dell'API ben documentate in grado di svolgere questi compiti in modo relativamente semplice. Ad esempio ZwQuerySystemInformation funziona in modo simile alla funzione ZwQueryDirectoryFile vista in una precedente lezione. Il problema è che questo metodo esclude i processi nascosti (circostanza spesso non desiderabile quando si è in kernel mode) e le info recuperabili sono davvero limitate. E' necessario quindi usare un'altra tecnica. L'idea è quella di non andarci troppo per il sottile e recuperare le informazioni direttamente dalle strutture in cui si trovano. L'inconveniente di questa tecnica è che prevede l'utilizzo di funzioni non documentate ma che sono comunque esportate da Ntoskrnl.exe ed esposte in Ntoskrnl.lib quindi è sufficiente dichiararle esplicitamente per usarle. Il codice presentato in questa lezione è tratto da [2] e, almeno per questa volta, si eviterà di esaminare ogni singola funzione in dettaglio ma, piuttosto, verranno forniti abbondanti commenti a supporto di quasi ogni riga di codice in modo che tutto sia relativamente comprensibile. Per una descrizione dettagliata riguardo molte delle funzioni non documentate della Kernel API si veda [3].
martedì 27 agosto 2019
12 - Kernel API: Registro
In kernel mode a volte capita di dover interagire con il registro di sistema per configurare qualcosa o per salvare dei dati in modo permanente. La prima funzione che si prenderà in esame in questa lezione è ZwCreateKey, che restituisce un handle alla chiave di registro che si vuole creare o aprire. Una funzione molto simile è ZwOpenKey, che però permette solo di aprire una chiave già esistente.
NTSYSAPI NTSTATUS ZwCreateKey( PHANDLE KeyHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, ULONG TitleIndex, PUNICODE_STRING Class, ULONG CreateOptions, PULONG Disposition );
NTSYSAPI NTSTATUS ZwOpenKey( PHANDLE KeyHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes );
mercoledì 21 agosto 2019
11 - Kernel API: File
Due funzioni simili e molto importanti, quando si parla di file e directory, sono ZwCreateFile e IoCreateFile che restituiscono un handle all'oggetto kernel che rappresenta il file o la directory che si vuole creare o aprire (se già esistente). Il prototipo di queste due funzioni è molto simile quindi verrà presa in esame ZwCreateFile perché è leggermente più concisa.
NTSYSCALLAPI NTSTATUS ZwCreateFile( PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength );
giovedì 15 agosto 2019
10 - Kernel API: Stringhe
In kernel mode si usano prevalentemente stringhe unicode, che richiedono due byte per ogni carattere (tecnicamente tale affermazione non sarebbe corretta ma grazie a questa semplificazione si rende più comprensibile il resto della lezione). A volte, però, capita di avere a che fare con qualche stringa ANSI, che richiede un solo byte (anche questa è una semplificazione di comodo), perché magari è stata passata con questa codifica dal client al driver. Fortunatamente, l'API del kernel fornisce quasi sempre funzioni aggiuntive in grado di accettare stringhe ANSI come parametro.
Inoltre, in kernel mode usare direttamente stringhe di tipo char* o wchar* che terminano con il carattere nullo è un po' rischioso in quanto basterebbe sovrascrivere quest'ultimo carattere per portare a risultati devastanti. Per questo motivo in kernel mode si usano i wrapper UNICODE_STRING e ANSI_STRING
Inoltre, in kernel mode usare direttamente stringhe di tipo char* o wchar* che terminano con il carattere nullo è un po' rischioso in quanto basterebbe sovrascrivere quest'ultimo carattere per portare a risultati devastanti. Per questo motivo in kernel mode si usano i wrapper UNICODE_STRING e ANSI_STRING
typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING; typedef struct _ANSI_STRING { USHORT Length; USHORT MaximumLength; PCHAR Buffer; } ANSI_STRING, *PANSI_STRING;
09 - Supporto all'accesso in memoria: Direct I/O
Nella lezione precedente (si veda [1]) si è visto che usare il Buffered I/O, come tipo di trasferimento dati tra client e driver, serve a risolvere alcuni problemi legati all'accesso diretto ai buffer in user space ma, ironicamente, ne introduce uno nuovo: prevede almeno una copia di memoria tra user space e kernel space che può portare a gravi inefficienze se la dimensione dei dati da copiare è considerevole.
Un altro tipo di trasferimento dei dati, detto Direct I/O ed argomento di questa lezione, permette di avere gli stessi vantaggi del Buffered I/O evitando qualsiasi operazione di copia. Come nel caso precedente, si parte con un po' di teoria per analizzare e spiegare come funziona questo tipo di trasferimento sia per ReadFile\WriteFile che per DeviceIoControl.
Un altro tipo di trasferimento dei dati, detto Direct I/O ed argomento di questa lezione, permette di avere gli stessi vantaggi del Buffered I/O evitando qualsiasi operazione di copia. Come nel caso precedente, si parte con un po' di teoria per analizzare e spiegare come funziona questo tipo di trasferimento sia per ReadFile\WriteFile che per DeviceIoControl.
08 - Supporto all'accesso in memoria: Buffered I/O
Fino ad ora la comunicazione tra client e driver è stata rappresentata ed implementata come un semplice accesso diretto alla memoria in user space da parte del driver. Questo, purtroppo, non sempre è conveniente o, addirittura, possibile. Nelle lezioni precedenti si è visto, infatti, che si è dovuti ricorrere ad un blocco __try\__exception durante l'accesso al buffer per evitare eventuali eccezioni e possibili crash nel caso in cui il client fornisse un indirizzo non valido per il buffer. Il blocco __try\__exception, benché non elegante, risolve questo problema ma, purtroppo, questa non è tutta la storia. Se il nostro codice in kernelmode è eseguito come DPC, ad un IRQL $= 2$, sappiamo che il contesto del thread è arbitrario (si veda [3]) e quindi non è garantito che il thread che si occupa di eseguire la DPC sia lo stesso che ha effettuato la richiesta al device in usermode (in questo modo non è possibile accedere al buffer in user space da kernelmode perché il thread non "vedrà" tale buffer dato che probabilmente è in esecuzione con associato uno spazio di indirizzamento virtuale diverso). Inoltre, per IRQL $\ge 2$ non è possibile fare riferimento a memoria Paged e questo è un problema poiché non c'è alcuna garanzia che la memoria in user space sia in RAM.
Per evitare tutti questi problemi è possibile chiedere supporto all'I/O Manager riguardo l'accesso dei buffer forniti dal client. Come visto nella lezione precedente (si veda [1]) con il codice di controllo è possibile indicare il tipo di trasferimento dei dati da client a driver per una data operazione. Con METHOD_NEITHER si afferma che non si vuole alcun supporto da parte dell'I/O Manager e si vuole accedere ai buffer del client direttamente. Atri valori permettono di attivare tale supporto per DeviceIoControl e relativa dispatch routine. Per quanto riguarda ReadFile\WriteFile e relative dispatch routine il discorso è simile, cambia solo modo ti attivare il supporto da parte dell'I/O Manager. Un'altra buona notizia è che tra del codice che non implementa tale supporto e lo stesso che lo implementa la differenza si riduce a poche righe di codice. Ma prima di vedere degli esempi pratici è necessaria un po' di teoria per analizzare e chiarire come funziona il supporto dell'I/O Manager, in particolare quello che utilizza il trasferimento dei dati noto come Buffered I/O (argomento di questa lezione), sia per ReadFile\WriteFile che per DeviceIoControl.
Per evitare tutti questi problemi è possibile chiedere supporto all'I/O Manager riguardo l'accesso dei buffer forniti dal client. Come visto nella lezione precedente (si veda [1]) con il codice di controllo è possibile indicare il tipo di trasferimento dei dati da client a driver per una data operazione. Con METHOD_NEITHER si afferma che non si vuole alcun supporto da parte dell'I/O Manager e si vuole accedere ai buffer del client direttamente. Atri valori permettono di attivare tale supporto per DeviceIoControl e relativa dispatch routine. Per quanto riguarda ReadFile\WriteFile e relative dispatch routine il discorso è simile, cambia solo modo ti attivare il supporto da parte dell'I/O Manager. Un'altra buona notizia è che tra del codice che non implementa tale supporto e lo stesso che lo implementa la differenza si riduce a poche righe di codice. Ma prima di vedere degli esempi pratici è necessaria un po' di teoria per analizzare e chiarire come funziona il supporto dell'I/O Manager, in particolare quello che utilizza il trasferimento dei dati noto come Buffered I/O (argomento di questa lezione), sia per ReadFile\WriteFile che per DeviceIoControl.
giovedì 25 aprile 2019
Rilevare Remote Thread Injection in UserMode
Rilevare in usermode la presenza di codice iniettato nel proprio processo non è semplice in quanto esistono diversi modi per iniettare codice ma non esiste un unica soluzione che li rilevi tutti. Ogni metodo di iniezione richiede la sua tecnica di rilevazione che, per quanto ingegnosa, resta circoscritta al singolo caso e spesso aggirabile usando semplicemente un altro metodo di iniezione. Quindi, a meno che uno non investa gran parte del suo tempo ad implementare ed aggiornare la parte del suo programma che riguarda il rilevamento di codice iniettato anziché a sviluppare le feature del programma stesso, resta abbastanza impraticabile come contromisura.
Ci si potrebbe chiedere, a questo punto, che senso abbia scrivere qualcosa su questo argomento. In realtà questo articolo è il pretesto per introdurre un paio di argomenti davvero interessanti: API Hooking e la funzione NtQuerySystemInformation. Inoltre, si è preso in esame il caso forse più utile, e cioè la rilevazione di iniezione che sfrutta l'esecuzione di un thread remoto.
La tecnica presentata in questo articolo si basa sul processo di avvio dei thread: ogni volta che si crea un thread con CreateThread, CreateRemoteThread o funzioni simili, prima di eseguire il codice della funzione passata come argomento viene chiamata la funzione LdrInitializeThunk in NTDLL.DLL (per approfondire l'argomento si veda [2]). Eseguendo l'hook di questa funzione si possono intercettare alcune informazioni interessanti ai fini della rilevazione di codice iniettato tramite creazione di un thread remoto.
Ci si potrebbe chiedere, a questo punto, che senso abbia scrivere qualcosa su questo argomento. In realtà questo articolo è il pretesto per introdurre un paio di argomenti davvero interessanti: API Hooking e la funzione NtQuerySystemInformation. Inoltre, si è preso in esame il caso forse più utile, e cioè la rilevazione di iniezione che sfrutta l'esecuzione di un thread remoto.
La tecnica presentata in questo articolo si basa sul processo di avvio dei thread: ogni volta che si crea un thread con CreateThread, CreateRemoteThread o funzioni simili, prima di eseguire il codice della funzione passata come argomento viene chiamata la funzione LdrInitializeThunk in NTDLL.DLL (per approfondire l'argomento si veda [2]). Eseguendo l'hook di questa funzione si possono intercettare alcune informazioni interessanti ai fini della rilevazione di codice iniettato tramite creazione di un thread remoto.
giovedì 18 aprile 2019
Process Hollowing (Dynamic Forking)
Per Process Hollowing, o Dynamic Forking, si intende quella tecnica in cui viene creato un processo che alla fine esegue l'immagine di un eseguibile diversa rispetto a quella associata quando è creato il processo. Per eseguire tale tecnica i passaggi fondamentali sono tre:
Un'applicazione, che chiameremo iniettore o hijacker (dipende dal compito che tale applicazione sta svolgendo), crea un processo target indicando un eseguibile di suo gradimento e sospendendo il thread principale.
Un'applicazione, che chiameremo iniettore o hijacker (dipende dal compito che tale applicazione sta svolgendo), crea un processo target indicando un eseguibile di suo gradimento e sospendendo il thread principale.
giovedì 11 aprile 2019
DLL injection tramite ShellCode
In un altro articolo (si veda [1]) si è visto come è possibile iniettare una DLL in un processo remoto semplicemente caricando DLL e parte del loader (quella che si occupa di fixare la DLL in memoria) in tale processo e creando un nuovo thread, sempre nello stesso, che eseguisse il codice del loader/fixer . Un approccio leggermente diverso (affrontato in questo articolo) ma che porta allo stesso risultato è quello di caricare del codice assembly in forma di array di byte, noto come shellcode, da eseguire nel target. Per farlo, uno dei tanti modi disponibili è quello di sospendere il thread principale del target e forzarlo a riprendere dall'inizio dello shellcode, che quindi provvederà a caricare la DLL chiamando semplicemente LoadLibrary. L'immagine sotto descrive tale tecnica in modo semplificato.
Iscriviti a:
Post (Atom)







