Thanks to visit codestin.com
Credit goes to github.com

Skip to content

alex3O/BYOVD-DriverKiller

Repository files navigation

BYOVD-DriverKiller

⚠️ Avertissement : Ce projet est strictement éducatif et démonstratif. Il n’a pas vocation à être utilisé dans un contexte malveillant. L’objectif est d’apprendre la méthodologie de reverse engineering et les étapes d’exploitation d’un driver Windows.


J'explique ici la démarche que j’ai suivie pour résoudre l’exercice proposé par d1rk(SaadAhla) https://github.com/SaadAhla, consistant à effectuer du reverse engineering et de l’exploitation sur un driver légitime, signé, et non présent dans les blocklists (HVCI, LOLBIN...). Un programme C permettant de terminer n'importe quel processus actif sur le système via ce Kernel-mode Driver est disponible, je détaille son fonctionnement un peu plus bas.

POC-BYOD

📃 Usage : DriverKiller.exe <nom_processus.exe> [-d]

Option -d : Permet de supprimer le service et le Driver du système après l'exploitation.

Le testsigning mode doit être activé sur la machine cible car le certificat du Driver a expiré.


Partie 1 - Reverse engineering :

L’exercice fournit un fichier .sys, nommé avec son hash SHA-256. La première étape consiste à ouvrir ce fichier avec IDA.
IDA est disponible gratuitement. Il suffit de se rendre sur le site d’Hex-Rays afin de générer une licence et de télécharger le logiciel.

On commence par lister l’IAT (Import Address Table) du Driver et rechercher l’appel à l’API qui nous intéresse : ZwTerminateProcess.

screen1-git

En double-cliquant sur ZwTerminateProcess, IDA nous redirige vers le code compilé de cette fonction. En sélectionnant l’entrée puis en affichant les cross-references, on obtient la liste des fonctions du Driver qui l’appellent.

screen2-git

On observe que c’est la fonction sub_12EF4, à l’offset 1CE, qui utilise ZwTerminateProcess. Après un double-clic, IDA affiche son code compilé.

screen11-git

Le code décompilé révèle les appels à ZwOpenProcess (qui ouvre un handle vers le processus cible) et à ZwTerminateProcess (qui termine le processus via ce handle).

En consultant la documentation de ZwOpenProcess (https://learn.microsoft.com/fr-fr/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess), on constate que le paramètre ClientID correspond à un pointeur indiquant le PID du processus visé.

Sur la ligne au-dessus, ClientId.UniqueProcess est initialisé avec la variable v22. Cette dernière est définie juste au-dessus :

v22 = (void )((_QWORD *)i + 10);

Pour comprendre cette affectation, il faut identifier la variable i et le champ +10.

screen3-git

Plus haut dans cette fonction, on observe un appel à ZwQuerySystemInformation avec le paramètre SYSTEM_PROCESS_INFORMATION. On comprend également que i est l’itérateur sur les entrées de cette structure avec la variable v6.

D’après la documentation de ZwQuerySystemInformation : (https://learn.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation), cette fonction retourne un tableau contenant une entrée par processus actif sur le système.

La structure SYSTEM_PROCESS_INFORMATION est décrite ici : https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation

typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    BYTE Reserved1[48];
    UNICODE_STRING ImageName;
    KPRIORITY BasePriority;
    HANDLE UniqueProcessId;
    PVOID Reserved2;
    ULONG HandleCount;
    ULONG SessionId;
    PVOID Reserved3;
    SIZE_T PeakVirtualSize;
    SIZE_T VirtualSize;
    ULONG Reserved4;
    SIZE_T PeakWorkingSetSize;
    SIZE_T WorkingSetSize;
    PVOID Reserved5;
    SIZE_T QuotaPagedPoolUsage;
    PVOID Reserved6;
    SIZE_T QuotaNonPagedPoolUsage;
    SIZE_T PagefileUsage;
    SIZE_T PeakPagefileUsage;
    SIZE_T PrivatePageCount;
    LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;

Rappel : tailles de certains types sur Windows x64

  • ULONG = 4 octets
  • USHORT = 2 octets
  • HANDLE = 8 octets
  • PWSTR = 8 octets
  • KPRIORITY (typedef d'un LONG) = 4 octets
  • UNICODE_STRING = 16 octets car voici sa structure :
  typedef struct _UNICODE_STRING {
    USHORT Length;        -> 2      
    USHORT MaximumLength; -> + 2 = 4
    PWSTR  Buffer;        -> + 8 = 12 (12 n'est pas un multiple de 8 donc padding de 4 ajouté en amont de Buffer) = 16
} UNICODE_STRING;

Calcul de l’offset de UniqueProcessId :

    ULONG NextEntryOffset;        -> 4
    ULONG NumberOfThreads;        -> + 4 = 8
    BYTE Reserved1[48];           -> + 48 = 56
    UNICODE_STRING ImageName;     -> + 16 = 72
    KPRIORITY BasePriority;       -> + 4 = 76 (76 n'est pas un multiple de 8 donc padding de 4 ajouté) = 80
    HANDLE UniqueProcessId;       -> + 8 = 88

Le membre UniqueProcessId est donc à l’offset 0x50 (80 décimal).

En regardant l'affectation de notre variable v22, on constate que i est casté en pointeur QWORD (8 octets)

v22 = (void )((_QWORD *)i + 10);
Donc v22 correspond l'adresse de i + 10 * 8 = 80 octets. Cette variable contient donc bien le PID récupéré de la structure SYSTEM_PROCESS_INFORMATION.

Pour savoir quel PID sera passé à ZwTerminateProcess, il faut analyser la condition qui entoure cette affectation.

screen4-git

On constate que le nom de l’image du processus est d’abord récupéré :

v9 = (wchar_t )((_QWORD *)i + 8);
Car v9 = adresse de i + 8 × 8 = 64 octets. Cela correspond au Buffer du membre ImageName, puisque ce membre se trouve à l’offset 56 + 2 (USHORT) + 2 (USHORT) + 4 (padding) = 64

Au vu des manipulations et des boucles dessous, on peut émettre l’hypothèse qu’une comparaison entre le nom du processus passé en argument (a2) et les processus actifs sur le système v9/String est effectuée.

sub_1C078(String, v9, (int)v13);
v17 = strupr(a2);
v18 = strupr(String);

C’est donc le paramètre a2 qui est censé contenir le nom du processus à terminer via ZwTerminateProcess. On remarque que a2 est un paramètre de la fonction sub_12EF4. Pour aller plus loin, il faut examiner les références de cette fonction (je l’ai renommée ZwTerminateProcessCaller pour une meilleure lisibilité).

screen5-git

On constate que ZwTerminateProcessCaller est appelée par la fonction sub_13624 à l'offset 61A.

screen6-git

Avant d’analyser ce code décompilé, je vais chercher les références de la fonction sub_13624 (renommée ZwTerminateProcessCallerCaller) afin de m’assurer que ce code est bien utilisé après un appel API à DeviceIoControl depuis le UserMode.

screen§-git

On constate que ZwTerminateProcessCallerCaller est appelée par la fonction sub_14130 (renommée ZwTerminateProcessCallerCallerCaller ...heureusement pour nous, c'est la dernière avant le point d'entrée 😅).

screen7-git

On constate que ZwTerminateProcessCallerCallerCaller est appelée par la fonction sub_1A4A8 à l'offset 306.

screen8-git

On trouve l'assignation de la fonction ZwTerminateProcessCallerCallerCaller :

memset64(DriverObject->MajorFunction, (unsigned __int64)ZwTerminateProcessCallerCallerCaller, 0x1Cu);
Ce qui signifie que cette fonction est assignée à toutes les entrées de la table MajorFunction (0x1B = 27, et il existe 28 IRP majeures).

screen9-git

Avant de revenir à la fonction sub_13624 (alias ZwTerminateProcessCallerCaller), on récupère le Symbolic Name et le Device Name (identiques ici) : Viragtlt.

screen12-git

En revenant sur ZwTerminateProcessCallerCaller, on remarque que son deuxième paramètre (donc a2) correspond à MasterIrp->AssociatedIrp.SystemBuffer.

screen13-git

Juste au dessus de l'appel à ZwTerminateProcessCaller on trouve le IOCTL code : -2106392528 (en hexadécimal : 0x82730030).

Grâce à ces informations, on peut en déduire que pour exploiter ce Driver, il faut envoyer un appel API DeviceIoControl au Driver avec le nom du processus à terminer dans le SystemBuffer.


🔷 Informations récupérées grâce au reverse engineering :

  • IOCTLCode : 0x82730030
  • Device Name : Viragtlt
  • Symbolic Name : Viragtlt
  • SystemBuffer doit contenir le nom du processus cible

Partie 2 - Exploitation

Pour exploiter ce Driver (s'il est installé et actif sur la machine cible), il est nécessaire d'ouvrir un handle vers celui-ci, puis de faire un appel API DeviceIoControl avec un Buffer contenant le nom du processus que l'on souhaite terminer.
Pour cet exercice, j'ai développé un projet C qui :

  • Vérifie si le Driver est présent et actif sur le système (avec un nom de service précis) :
    • Si oui, le programme exploite le Driver avec un appel API DeviceIoControl.
    • Si non, le programme extrait le driver de ses ressources, le déploie sur le bureau de l'utilisateur, crée un service actif puis exploite le Driver avec un appel API DeviceIoControl. (Nécessite les droits admin car une création de service est effectuée.)
  • Si le Driver est présent sur le système mais que le service n'est pas démarré, le programme essaye de démarrer le service puis l'exploite avec un appel API DeviceIoControl.

J'ai également ajouté une option -d qui permet de supprimer le service et le Driver du système après l'exploitation.

Voici le comportement du programme C dans son cycle d'exécution complet :

git

Evasion AV/EDR

Dans ce cas, DriverKiller.exe n’est pas détecté par Microsoft Defender, ni en statique ni en dynamique. L’évasion n’a pas vraiment de sens ici car le Driver exploité possède un certificat expiré, son utilisation en conditions réelles est donc difficilement envisageable. Mais pour une meilleure furtivité, on aurait pu implémenter :

  • Le masquage de certains appels API de la table IAT grâce à des implémentations personnalisées de GetProcAddress et GetModuleHandle
  • Un rapprochement du Kernel pour l’exécution des appels API (Direct/Indirect Syscalls)
  • Des techniques Anti-VM / Anti-Debug

Détection sur le driver au 29/08/2025 (résultat déjà existant, je n'ai rien soumis sur VirusTotal pour des raisons évidentes) :

image

⚠️ Ce projet est réalisé dans un cadre d’apprentissage. Il peut contenir des imprécisions ou des erreurs. Toute suggestion, correction ou discussion est la bienvenue ! 😃 Merci à d1rk(SaadAhla) : https://github.com/SaadAhla !

About

Driver Reverse & Exploitation

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages