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.
WFP e Callout Driver
La Windows Filtering Platform (WFP) è una API che consente di scrivere programmi di analisi e ispezione dei dati di rete come, ad esempio, firewall e antivirus. Un Callout Driver è un driver che sfrutta tale API usandola in kernel mode. In questa modalità WFP offre estensioni e vantaggi rispetto ad applicazioni che la usano in user mode. Prendendo in considerazione l'immagine sotto, il Filter Engine è il cuore della WFP ed è la componente che effettua le operazioni di filtraggio e processamento dei dati di rete. Il Network Stack può essere immaginato come un bus dati dove passano tutte le informazioni di rete. All'interno del Filter Engine esistono dei punti di filtraggio chiamati Filtering Layer, ognuno dedicato ad una tipologia particolare di informazioni, che passano i dati dal Network Stack al Filter Engine per fare in modo che quest'ultimo li processi. Ai Filtering Layer del Filter Engine si possono aggiungere dei filtri che contengono una azione da avviare se si verificano tutte le condizioni che il filtro pone. Se il filtro non pone condizioni particolari l'azione verrà avviata semplicemente quando ci sono dati disponibili per quel filtro nel relativo Filtering Layer. Ad esempio, se il Filtering Layer è uno di quelli che si occupa delle richieste di connessione in uscita, l'azione di un filtro che non pone ulteriori condizioni verrà avviata non appena ci sono richieste di connessione in uscita. Un azione spesso associata ad un filtro è una Callout che è un insieme di tre funzioni di callback ed un identificatore numerico che serve a distinguere le varie Callout che è possibile registrare nel sistema. Una Callout può essere aggiunta al Filter Engine ed un filtro può far riferimento a tale Callout. Quindi quando il Filter Engine riceve i dati li processa e controlla se tutte le condizione di un dato filtro sono soddisfatte. Se la risposta è affermativa l'azione del filtro viene avviata ed una specifica funzione di callback (delle tre disponibili) viene invocata fornendo tutte le informazioni disponibili a quel filtro in quel particolare momento.
Quella appena descritta può essere considerata una panoramica generale su WFP ed i Callout Driver che evita volutamente di entrare nei dettagli ma è quanto basta, almeno per il momento, per iniziare a scrivere un po' di codice. Se in futuro mi dovessi trovare nelle condizioni di approfondire ulteriormente l'argomento aggiornerò questa stessa lezione. Prima di cominciare, però, si devono effettuare un paio modifiche al progetto. E' necessario innanzitutto aggiungere le librerie fwpkclnt.lib e uuid.lib.
Dopodiché è richiesto il valore NDIS_SUPPORT_NDIS6 come definizione per il preprocessore.
A questo punto si può partire con il codice vero e proprio. Si comincia con l'entrypoint che crea il Device Object ed invoca la funzione utente WFPLoad. Il Device Object è necessario per registrare la callout nel sistema e decretare di fatto il relativo driver come un Callout Driver. DriverUnload invoca la funzione utente WFPUnload ed elimina il Device Object.
#include <ntddk.h> #pragma warning(push) #pragma warning(disable:4201) // unnamed struct/union #include <fwpsk.h> #pragma warning(pop) #include <fwpmk.h> // Variabili globali #define DEV_NAME L"\\Device\\MY_WFP_DEV_NAME" #define SYM_NAME L"\\??\\MY_WFP_SYM_NAME" // Handle al filter engine HANDLE g_hEngine; // ID della callout che servirà poi a rimuoverla UINT32 g_AleConnectCalloutId; // ID del filtro che servirà poi a rimuoverlo UINT64 g_AleConnectFilterId; // Identificatore della callout GUID GUID_ALE_AUTH_CONNECT_CALLOUT_V4 = { 0x6812fc83, 0x7d3e, 0x499a, 0xa0, 0x12, 0x55, 0xe0, 0xd8, 0x5f, 0x34, 0x8b }; NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath) { UNREFERENCED_PARAMETER(pRegPath); DbgPrint("Enter DriverEntry\n"); NTSTATUS status = STATUS_SUCCESS; pDriverObject->DriverUnload = DriverUnload; // Per registrare la callout è necessario il device object collegato al driver CreateDevice(pDriverObject); // Crea callout e filtro WFPLoad(pDriverObject->DeviceObject); DbgPrint("Leave DriverEntry\n"); return status; }
VOID DriverUnload(PDRIVER_OBJECT pDriverObject) { UNREFERENCED_PARAMETER(pDriverObject); // Rimuove callout e filtro WFPUnload(); UNICODE_STRING ustrSymName; RtlInitUnicodeString(&ustrSymName, SYM_NAME); IoDeleteSymbolicLink(&ustrSymName); if (pDriverObject->DeviceObject) { IoDeleteDevice(pDriverObject->DeviceObject); } DbgPrint("[MyDriver]Unloaded!\n"); }
NTSTATUS CreateDevice(PDRIVER_OBJECT pDriverObject) { DbgPrint("Enter CreateDevice\n"); NTSTATUS status = STATUS_SUCCESS; PDEVICE_OBJECT pDevObj = NULL; UNICODE_STRING ustrDevName, ustrSymName; RtlInitUnicodeString(&ustrDevName, DEV_NAME); RtlInitUnicodeString(&ustrSymName, SYM_NAME); status = IoCreateDevice(pDriverObject, 0, &ustrDevName, FILE_DEVICE_NETWORK, 0, FALSE, &pDevObj); if (!NT_SUCCESS(status)) { ShowError("IoCreateDevice", status); return status; } status = IoCreateSymbolicLink(&ustrSymName, &ustrDevName); if (!NT_SUCCESS(status)) { ShowError("IoCreateSymbolicLink", status); return status; } DbgPrint("Leave CreateDevice\n"); return status; }
VOID ShowError(PCHAR lpszText, NTSTATUS ntStatus) { KdPrint(("%s[Error:0x%X]\n", lpszText, ntStatus)); }
WFPLoad chiama la funzione utente RegisterCalloutForLayer che si occupa di registrare la callout, assegnarla come azione di un filtro ed aggiungere entrambi al Filter Engine. Il valore FWPM_LAYER_ALE_AUTH_CONNECT_V4 indica che i dati a cui siamo interessati riguardano le richieste di connessione in uscita alla rete. WFPUnload si occupa di annullare la registrazione della callout, eliminare filtro e callout e chiudere le comunicazioni con il Filter Engine.
NTSTATUS WFPLoad(PDEVICE_OBJECT pDevObj) { NTSTATUS status = STATUS_SUCCESS; // Prima Registra una callout e poi aggiunge tale callout ed un filtro al filter engine. status = RegisterCalloutForLayer( pDevObj, // Device Object &FWPM_LAYER_ALE_AUTH_CONNECT_V4, // ID del Filtering Layer (punto di filtraggio, dati di interesse) &GUID_ALE_AUTH_CONNECT_CALLOUT_V4, // ID della callout classifyFn, // azione del filtro: funzione di callback notifyFn, // funzione di callback (non ci interessa) flowDeleteFn, // funzione di callback (non ci interessa) &g_AleConnectCalloutId, // Param. di output, riceve ID che identifica Callout &g_AleConnectFilterId, // Param. di output, riceve ID che identifica Filtro &g_hEngine); // Param. di output, riceve handle che identifica Filter Engine if (!NT_SUCCESS(status)) { ShowError("RegisterCalloutForLayer", status); return status; } return status; }
NTSTATUS WFPUnload() { if (NULL != g_hEngine) { // Rimuove il filtro dal filter engine tramite ID relativo ritornato da FwpmFilterAdd FwpmFilterDeleteById(g_hEngine, g_AleConnectFilterId); // Rimuove callout dal filter engine tramite ID relativo ritornato da FwpsCalloutRegister FwpmCalloutDeleteById(g_hEngine, g_AleConnectCalloutId); // Azzera ID del filtro g_AleConnectFilterId = 0; // Annulla la registrazione della callout FwpsCalloutUnregisterById(g_AleConnectCalloutId); // Azzera ID della callout g_AleConnectCalloutId = 0; // chiude sessione aperta con filter engine FwpmEngineClose(g_hEngine); g_hEngine = NULL; } return STATUS_SUCCESS; }
// Prima Registra una callout e poi aggiunge tale callout ed un filtro al filter engine. NTSTATUS RegisterCalloutForLayer( IN PDEVICE_OBJECT pDevObj, IN const GUID *layerKey, IN const GUID *calloutKey, IN FWPS_CALLOUT_CLASSIFY_FN classifyFn, IN FWPS_CALLOUT_NOTIFY_FN notifyFn, IN FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN flowDeleteNotifyFn, OUT UINT32 *calloutId, OUT UINT64 *filterId, OUT HANDLE *engine) { NTSTATUS status = STATUS_SUCCESS; // Crea e registra una callout status = RegisterCallout( pDevObj, calloutKey, classifyFn, notifyFn, flowDeleteNotifyFn, calloutId); if (!NT_SUCCESS(status)) { return status; } // Aggiunge callout e filtro al filter engine. status = SetFilter( layerKey, calloutKey, filterId, engine); if (!NT_SUCCESS(status)) { return status; } return status; }
// Crea e registra una Callout NTSTATUS RegisterCallout( PDEVICE_OBJECT pDevObj, IN const GUID *calloutKey, IN FWPS_CALLOUT_CLASSIFY_FN classifyFn, IN FWPS_CALLOUT_NOTIFY_FN notifyFn, IN FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN flowDeleteNotifyFn, OUT UINT32 *calloutId) { NTSTATUS status = STATUS_SUCCESS; // Una callout è un insieme di 3 funzioni di callback e un ID che identifica l'intera callout. // Per il momento ci interessa solo classifyFn come funzione di callback che è quella che viene // invocata se una callout viene assegnata come azione di un filtro. FWPS_CALLOUT sCallout = { 0 }; sCallout.calloutKey = *calloutKey; sCallout.classifyFn = classifyFn; sCallout.flowDeleteFn = flowDeleteNotifyFn; sCallout.notifyFn = notifyFn; // Crea e registra nel sistema una Callout e mette l'ID identificativo di tale callout // nell'ultimo parametro. status = FwpsCalloutRegister(pDevObj, &sCallout, calloutId); if (!NT_SUCCESS(status)) { ShowError("FwpsCalloutRegister", status); return status; } return status; }
// Aggiunge callout e filtro al filter engine. NTSTATUS SetFilter( IN const GUID *layerKey, IN const GUID *calloutKey, OUT UINT64 *filterId, OUT HANDLE *engine) { HANDLE hEngine = NULL; NTSTATUS status = STATUS_SUCCESS; FWPM_SESSION session = { 0 }; FWPM_FILTER mFilter = { 0 }; FWPM_CALLOUT mCallout = { 0 }; FWPM_DISPLAY_DATA mDispData = { 0 }; // Imposta parametri della sessione da aprire con Filter Engine. // Il flag FWPM_SESSION_FLAG_DYNAMIC permette ad ogni filtro aggiunto al filter // engine durante una sessione aperta di essere automaticamente rimosso dal filter // engine quando la sessione viene chiusa. session.flags = FWPM_SESSION_FLAG_DYNAMIC; // Apre una sessione con il filter engine // e mette l'handle a tale sessione nell'ultimo parametro. status = FwpmEngineOpen(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &hEngine); if (!NT_SUCCESS(status)) { ShowError("FwpmEngineOpen", status); return status; } // Avvia una transazione all'interno della sessione corrente. // Necessario prima di iniziare a passare "istruzioni" al filter // engine riguardo callout e filtri da aggiungere. status = FwpmTransactionBegin(hEngine, 0); if (!NT_SUCCESS(status)) { ShowError("FwpmTransactionBegin", status); return status; } // La struttura FWPM_DISPLAY_DATA specifica un nome e una descrizione per una callout. mDispData.name = L"MY WFP TEST"; mDispData.description = L"WORLD OF DEMON"; // Imposta membri di callout da impostare come azione del filtro. // In particolare si indicano l'identificatore del Filtering Layer dove risiedono i filtri // a cui è possibile applicare questa callout (layerKey) e l'identificatore della callout // registrata nel sistema (calloutKey). // Questa callout verrà passata ad una funzione del tipo FwpmXXX quindi è necessario // usare la struttura FWPM_CALLOUT per identificare la callout, diversamente da quanto visto in // precedenza dove è stato usata la struttura FWPS_CALLOUT per una callout passata // ad un metodo del tipo FwpsXXX. mCallout.applicableLayer = *layerKey; mCallout.calloutKey = *calloutKey; mCallout.displayData = mDispData; // Aggiunge una callout al filter engine status = FwpmCalloutAdd(hEngine, &mCallout, NULL, NULL); if (!NT_SUCCESS(status)) { ShowError("FwpmCalloutAdd", status); return status; } // Imposta proprietà e condizioni (nessuna condizione in questo caso) del filtro. // Il membro action identifica l'azione da intraprendere quando tutte le condizioni // del filtro vengono soddisfatte (nessuna condizione in questo caso). // FWP_ACTION_CALLOUT_TERMINATING indica che l'azione è una callout che o blocca o permette // l'operazione per cui è stata invocata. // Il membro displayData indica un nome e una descrizione del filtro. // Il membro layerkay indica l'identificatore del Filtering Layer dove verrà posizionato il filtro. // Un Filtering Layer può essere inteso come la tipologia di dati a cui si è interessati. // FWPM_LAYER_ALE_AUTH_CONNECT_V4 è l'identificatore del Filtering Layer // che consente di autorizzare o meno richieste di connessione in uscita. // Il membro subLayerKey permette di essere più specifici su cosa è di interesse: // FWPM_SUBLAYER_UNIVERSAL indica che non verranno specificate ulteriori caratteristiche. // Il membro weight indica il peso del filtro: FWP_EMPTY assegnato al campo // type indica che verrà assegnato un peso in modo automatico. mFilter.action.calloutKey = *calloutKey; mFilter.action.type = FWP_ACTION_CALLOUT_TERMINATING; mFilter.displayData.name = L"MY WFP TEST"; mFilter.displayData.description = L"WORLD OF DEMON"; mFilter.layerKey = *layerKey; mFilter.subLayerKey = FWPM_SUBLAYER_UNIVERSAL; mFilter.weight.type = FWP_EMPTY; // Aggiunge il filtro al filter engine e mette l'ID che identifica tale filtro // nell'ultimo parametro. status = FwpmFilterAdd(hEngine, &mFilter, NULL, filterId); if (!NT_SUCCESS(status)) { ShowError("FwpmFilterAdd", status); return status; } // FwpmTransactionCommit esegue il commit della transazione corrente nella sessione corrente. status = FwpmTransactionCommit(hEngine); if (!NT_SUCCESS(status)) { ShowError("FwpmTransactionCommit", status); return status; } // Salva handle al Filter Engine. *engine = hEngine; return status; }
// Funzione di callback classifyFn della callout VOID NTAPI classifyFn( _In_ const FWPS_INCOMING_VALUES* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, _In_ const FWPS_FILTER* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT* classifyOut ) { UNREFERENCED_PARAMETER(layerData); UNREFERENCED_PARAMETER(classifyContext); UNREFERENCED_PARAMETER(filter); UNREFERENCED_PARAMETER(flowContext); // Una mole impressionante di dati e metadati vengono resi disponibili al filtro e passate in forma di array // indicizzabile alla funzione di callback classifyFn (invocata quando le condizioni del filtro sono soddisfatte // e una callout è impostata come a azione di tale filtro). // In particolare, per FWPM_LAYER_ALE_AUTH_CONNECT_V4 queste informazioni includono, ad esempio, // ID e percorso del processo che fa la richiesta di connessione in uscita, porta di comunicazione locale // e remota, indirizzo IP locale e remoto, ecc. // Si veda [4], [5] e [6] per maggiori dettagli. ULONG ulLocalIp = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_ADDRESS].value.uint32; UINT16 uLocalPort = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_PORT].value.uint16; ULONG ulRemoteIp = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS].value.uint32; UINT16 uRemotePort = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_PORT].value.uint16; KIRQL kCurrentIrql = KeGetCurrentIrql(); ULONG64 processId = inMetaValues->processId; UCHAR szProcessPath[256] = { 0 }; CHAR szProtocalName[256] = { 0 }; RtlZeroMemory(szProcessPath, 256); ULONG i = 0; for (i = 0; i < inMetaValues->processPath->size; i++) { szProcessPath[i] = inMetaValues->processPath->data[i]; } // Permette la connessione in uscita classifyOut->actionType = FWP_ACTION_PERMIT; // Blocca la connessione in uscita per Microsoft Edge if (wcsstr((PWCHAR)szProcessPath, L"microsoftedgecp.exe") != NULL) { KdPrint(("Microsoft Edge [FWP_ACTION_BLOCK]\n")); classifyOut->actionType = FWP_ACTION_BLOCK; classifyOut->rights = classifyOut->rights & (~FWPS_RIGHT_ACTION_WRITE); classifyOut->flags = classifyOut->flags | FWPS_CLASSIFY_OUT_FLAG_ABSORB; } // Recupera nome del protocollo usato per la connessione ProtocolIdToName(inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL].value.uint16, szProtocalName); // Stampa informazioni raccolte. DbgPrint("Protocal=%s, LocalIp=%u.%u.%u.%u:%d, RemoteIp=%u.%u.%u.%u:%d, IRQL=%d, PID=%I64d, Path=%S\n", szProtocalName, (ulLocalIp >> 24) & 0xFF, (ulLocalIp >> 16) & 0xFF, (ulLocalIp >> 8) & 0xFF, (ulLocalIp) & 0xFF, uLocalPort, (ulRemoteIp >> 24) & 0xFF, (ulRemoteIp >> 16) & 0xFF, (ulRemoteIp >> 8) & 0xFF, (ulRemoteIp) & 0xFF, uRemotePort, kCurrentIrql, processId, (PWCHAR)szProcessPath); }
// Funzione di callback notifyFn della callout (non fa niente) NTSTATUS NTAPI notifyFn( _In_ FWPS_CALLOUT_NOTIFY_TYPE notifyType, _In_ const GUID* filterKey, _Inout_ FWPS_FILTER* filter ) { UNREFERENCED_PARAMETER(notifyType); UNREFERENCED_PARAMETER(filterKey); UNREFERENCED_PARAMETER(filter); NTSTATUS status = STATUS_SUCCESS; return status; }
// Funzione di callback flowDeleteFn della callout (non fa niente) VOID NTAPI flowDeleteFn( _In_ UINT16 layerId, _In_ UINT32 calloutId, _In_ UINT64 flowContext ) { UNREFERENCED_PARAMETER(layerId); UNREFERENCED_PARAMETER(calloutId); UNREFERENCED_PARAMETER(flowContext); return; }
// Recupera nome del protocollo usato per la connessione da ID numerico NTSTATUS ProtocolIdToName(UINT16 protocalId, PCHAR lpszProtocalName) { NTSTATUS status = STATUS_SUCCESS; switch (protocalId) { case 1: { // ICMP (compreso carattere nullo finale) RtlCopyMemory(lpszProtocalName, "ICMP", 4 + 1); break; } case 2: { // IGMP RtlCopyMemory(lpszProtocalName, "IGMP", 4 + 1); break; } case 6: { // TCP RtlCopyMemory(lpszProtocalName, "TCP", 3 + 1); break; } case 17: { // UDP RtlCopyMemory(lpszProtocalName, "UDP", 3 + 1); break; } case 27: { // RDP RtlCopyMemory(lpszProtocalName, "RDP", 3 + 1); break; } default: { // UNKNOW RtlCopyMemory(lpszProtocalName, "UNKNOWN", 7 + 1); break; } } return status; }
Una cosa importante da notare è che, come si evince dall'output dell'esempio di codice, la funzione di callback a volte viene invocata ad IRQL $= 0$ mentre altre ad IRQL $= 2$ e questo può comportare un problema poiché, come visto in una delle prime lezioni, il codice eseguito ad IRQL $\ge 2$ è sottoposto a severe restrizioni.
Codice sorgente:
MonitorNetworkDriver.zip
Riferimenti:
[1] https://github.com/dearfuture/WindowsHack/tree/master/内核层/5/WFP网络监控/WFP_Network_Test/WFP_Network_Test
[2] https://github.com/andylau004/LookDrvCode/tree/master/WIN64驱动编程基础教程/代码/%5B4-7%5DMonitorInternetAccessByWFP
[3] https://docs.microsoft.com/en-us/windows-hardware/drivers/network/windows-filtering-platform-architecture-overview
[4] https://docs.microsoft.com/en-us/windows/win32/api/fwpstypes/ns-fwpstypes-fwps_incoming_values0
[5] https://docs.microsoft.com/en-us/windows/win32/api/fwpstypes/ns-fwpstypes-fwps_incoming_value0
[6] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/fwpsk/ne-fwpsk-fwps_fields_ale_auth_connect_v4_
[7] https://github.com/9176324/WinDDK/tree/master/7600.16385.1/src/network/trans
Nessun commento:
Posta un commento