Kernel Pool Overflow Exploitation in Real World - Windows 7

Blog articles

1) Introduction



This article will focus on a vulnerability (CVE-2017-6008) we identified in the HitmanPro standalone scan version 3.7.15 - Build 281. This tool is a part of the HitmanPro.Alert solution and has been integrated into the Sophos solutions as SophosClean.exe. The vulnerability has been reported to Sophos in February 2017. The version 3.7.20 – Build 286 patched the vulnerability in May 2017. We discovered the first crash while playing with Ioctlfuzzer [1]. Ioctlfuzzer is a great and simple tool made to fuzz the I/O Request Packets (IRP). The fuzzer hooks the DeviceIoControlFile API function and place itself as a man in the middle. For each IRP the fuzzer receives, it lands severals malformed IRP before sending the original one. The first crash occurred at the very beginning of the scan, in the Initialization phase, with a BAD_POOL_HEADER code. Before going deeper, I strongly recommend readers learn a bit more on IOCTL and IRP on Windows. The MSDN documentation provides a lot of information you must know to fully understand this article. This blog post will be focused on x64 architectures since it's harder to exploit than x32 architectures.

2) Investigation



2.1) Analyzing the crash data



Firstly, we need to know what the BAD_POOL_HEADER error code means. The pool is commonplace for every dynamic allocation in the kernel. This code means that there was a problem while processing a pool header. A pool header is a structure at the beginning of a chunk that gives information about the chunk.

Kernel Pool Chunk



A pool header has probably been corrupted, triggering the crash. Then, using a debugger, the dumps, and the logs generated by the fuzzer, we quickly find the vulnerable IRP:

IOCTL Code: 0x0022e100

Outbuff: 0x03ffe1f4, Outsize: 0x00001050

Inbuff : 0x03ffd988, Insize: 0x00000200

//Device/Hitman Pro 37 [/??/C:/Windows/system32/drivers/hitmanpro37.sys]

Several pieces of information are important here:

  • C:/Windows/system32/drivers/hitmanpro37.sys: the driver that handles the IRP. Since the crash occurs because of a pool corruption, this driver is certainly responsible for the crash.
  • IOCTL Code: 0x0022e100: The IOCTL code provides a lot of information, which I will explain later. We can also use it to retrieve where this IRP is processed in the above driver by reversing it.
  • Outsize /Insize: Those sizes are used to allocate some buffers in the pool, and might be related to the pool corruption.

If we refer to the MSDN documentation ([2]), we can draw those information from the IOCTL code:

DeviceType = 0x22

Access = 0x3

Function = 0x840

Method = 0x0


Method 0x0 = METHOD_BUFFERED
After a few research on the METHOD_BUFFERED, we find a quick definition:

METHOD_BUFFERED

For this transfer type, IRPs supply a pointer to a buffer at Irp->AssociatedIrp.SystemBuffer. This buffer represents both the input buffer and the output buffer that is specified in calls to DeviceIoControl and IoBuildDeviceIoControlRequest. The driver transfers data out of, and then into, this buffer.

For input data, the buffer size is specified by Parameters.DeviceIoControl.InputBufferLength in the driver's IO_STACK_LOCATION structure.

For output data, the buffer size is specified by Parameters.DeviceIoControl.OutputBufferLength in the driver's IO_STACK_LOCATION structure.

The size of the space that the system allocates for the single input/output buffer is the larger of the two length values.


Finally, we reverse the HitmanPro.exe executable, in order to find how the IOCTL is sent in normal behavior. Using the IOCTL code and the Search tool of IDA, we quickly retrieve the function.

Kernel Pool-IOCTL



The Outsize and Insize gave to DeviceIoControl match with the crash data. In this way, the SystemBuffer allocated by the IRP Manager should, in a normal behavior, be at least 0x1050 bytes long.

2.2) Reversing the driver



Now that we have a bunch of information on the crash, it's time to reverse the driver hitmanpro37.sys and take a look at the handle of our IOCTL. First, we have to locate the function which dispatches the IRP given its IOCTL code. It's usually a big function containing some switch jumps. Since this driver is not that big, we quickly find the dispatcher:

Kernel Pool Dispatcher



Kernel Pool JumpSwitch



Following the jumps, we finally reach the function that handles our vulnerable IOCTL. The SystemBuffer provided by the IRP is first used as the ObjectName argument of the function IoGetDeviceObjectPointer.

Kernel Pool normal_behavior



Then..

Kernel Pool memset_all



And.. here we are. Remember the method used for this IOCTL? METHOD_BUFFERED?

The size of the space that the system allocates for the single input/output buffer is the larger of the two length values.

It means we perfectly control the size of the SystemBuffer, and the driver calls memset on it with a hardcoded size of 0x1050. If the SystemBuffer is smaller than 0x1050, the call to memset will corrupt the pool, and trigger a crash. The vulnerability here is called a Kernel Pool Overflow. That being said, we didn't find any way to control the writing of this buffer. It's set to 0, then filled with addresses and names collected in DeviceObject and DriverObject structures, which we can't control without being an administrator. This vulnerability will remain an OS crash, which is not that bad! The CVE-2017-6007 has been assigned to this vulnerability.

2.3) Pivoting



We couldn't give up here, so we decided to reverse more handlers. We picked a random handler, and it was actually interesting:

Kernel Pool Handler



The SystemBuffer (our input) is used in a subfunction, then if the subfunction returns the right value, something is copied into the SystemBuffer, using memcpy. The control code used to reach this function is 0x00222000:

DeviceType = 0x22

Access = 0x0

Function = 0x0

Method = 0x0


It's the same method used: METHOD_BUFFERED. If we're lucky, there might be the same type of vulnerability here. However, this part of the driver is pretty weird:
  • We didn't find in the HitmanPro executable any function that launches an IRP with the control code 0x00222000. So, as expected, when we set a breakpoint in this part of the driver, it's never triggered!
  • We couldn't identify what was the exact purpose of the functions there, but we found a vulnerability, which is enough for us.

So I started reversing. The handler, turned into pseudo-code :

Kernel Pool understable_code



Kernel Pool understable_code



The driver uses a handle provided in the SystemBuffer to get a FILE_OBJECT. If this FILE_OBJECT is not busy, it calls ObQueryNameString to get the name of the file pointed by the FILE_OBJECT and put it in a temporary buffer. Then it copies the name from the temporary buffer to the SystemBuffer. The driver calls memcpy with:
  • dest = SystemBuffer ; we control its size
  • src = The file name of the handle we gave ; we control both writing and size
  • n = The size of the src buffer. ; ...

The only constraint is the ObQueryNameString function: This function is protected and doesn't copy anything if the source is too big for the target. Since the target is a buffer with a hardcoded size of 0x400, we can't give a filename bigger than 0x400. Of course, 0x400 characters is way enough in order to exploit a buffer overflow.

3) Exploitation



3.1) Introduction



Since the vulnerability is a Kernel Pool Overflow, there is a bunch of attacks we can use. And there is no better paper than Tarjei Mandt's [3] on the subject. It's a MUST read if you want to fully understand what happens next. The attack we use here is a Quota Process Pointer Overwrite. We chose this attack because it's one of the most elegant, and it can be achieved on both x32 and x64 architectures. In this attack, we have to overwrite the process pointer of the next chunk.

Kernel Pool reallocate



In the last 4 bytes of a pool header there is a pointer to a EPROCESS structure. When a pool chunk is freed, if the Quota bit is set in the PoolType (see _POOL_HEADER structure), this pointer will be used to decrement some values related to the EPROCESS object:
  • The Reference Count of the Object (a process is an Object)
  • A value pointed by the QuotaBlock field

But since there is some checks before the decrementation, we cant use directly the ReferenceCount of the object, but we can create a fake EPROCESS structure, and put an arbitrary pointer in the QuotaBlock field to decrement an arbitrary value (even in kernel addresses) !

kd> dt nt!_EPROCESS

+0x000 Pcb : _KPROCESS

+0x098 ProcessLock : _EX_PUSH_LOCK

+0x0a0 CreateTime : _LARGE_INTEGER

+0x0a8 ExitTime : _LARGE_INTEGER

+0x0b0 RundownProtect : _EX_RUNDOWN_REF

+0x0b4 UniqueProcessId : Ptr32 Void

+0x0b8 ActiveProcessLinks : _LIST_ENTRY

+0x0c0 ProcessQuotaUsage : [2] Uint4B

+0x0c8 ProcessQuotaPeak : [2] Uint4B

+0x0d0 CommitCharge : Uint4B

 +0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK 
[...]


typedef struct _EPROCESS_QUOTA_BLOCK {

   EPROCESS_QUOTA_ENTRY QuotaEntry[3];

   LIST_ENTRY QuotaList;

   ULONG ReferenceCount;

   ULONG ProcessCount;

 } EPROCESS_QUOTA_BLOCK, *PEPROCESS_QUOTA_BLOCK;


3.2) Overflow



In order to use the Quota Process Pointer Overwrite attack, we need to overwrite two things using our overflow:
  • The PoolType of the next chunk, because we need to make sure the Quota bit is set
  • The Process Pointer of the next chunk, and replace it with a pointer leading to a fake EPROCESS structure

Since we have to reach the process pointer of the next chunk, we have to overwrite the whole pool header of the next chunk anyway. But we can't just put some random data in the pool header, or we will trigger a BSOD. We have to make sure the following fields are correct:
  • The BlockSize
  • The PreviousSize
  • The PoolType

The only way to achieve this is to know exactly which chunk we're going to overflow, and this can be achieved using basic Pool Spraying. I won't explain in details here how to spray the pool, since an article about it is already published on this blog. But the idea is to have this kind of pool :

Kernel Pool PoolNotSprayed



Look like this:

Kernel Pool PoolSprayed



Our overflow:

Before overflow:

Kernel Pool chunk_x64_before_overflow



After overflow:

Kernel Pool chunk_x64_after_overflow



3.3) Payload



Ok, we can decrement any value at any address. What's next? We found a great paper of Cesar Cerrudo [4] which describes several techniques to escalate privilege. The second trick is interesting: there is a field in TOKEN structure called Privileges:

 typedef struct _TOKEN 

 {
 [...]
 /*0x040*/ typedef struct _SEP_TOKEN_PRIVILEGES
           {
               UINT64 Present;
 /*0x048*/     UINT64 Enabled;
               UINT64 EnabledByDefault;
           } SEP_TOKEN_PRIVILEGES, *PSEP_TOKEN_PRIVILEGES;
 [...]
 }TOKEN, *PTOKEN;


This field is a structure containing a few bitmasks. The bitmask Enabled defines which operations the attached process can perform. By default, this bitmask is set to 0x80000000, which gives us the SeChangeNotifyPrivilege privilege. Consider now subtracting 1 from this bitmask; it becomes 0x7fffffff, which is a lot of privileges! MSDN documentation provides the list of available privileges in this bitmask.

So, the idea is to write the address of the Enabled field of the _TOKEN structure of our process at the QuotaBlock field of our fake EPROCESS structure. But we don't have the address of our _TOKEN structure, and we're not supposed to because it's a kernel address! Fortunately, we can use the well-known leak by NtQuerySystemInformation to get the kernel address of an object using its handle. And we can get a handle on our token by calling the function OpenProcessToken()! So actually, it's pretty easy to leak the address of the current process' token. If you want to know more about NtQuerySystemInformation() and general leaking of kernel addresses, you should read this [5].

We chose to trigger the vulnerability until we obtain the SeDebugPrivilege, which gives us full control of all processes on the system, but you can do it to get any privilege you want! The SeDebugPrivilege allows us to start a thread in a System process and spawn a system shell.

4) Conclusion



Notice that this exploit can’t work on Windows 8 or more, since Microsoft did a great job at mitigating kernel vulnerabilities. Actually, this exploit won't work on Windows 8 on more, but it doesn't mean it's impossible to exploit. You can find the source code of the exploit on my github [7]. The exploitation of the very same vulnerability on Windows 10 will be the subject of a conference at the Nuit du Hack XV, June 24th / 25th 2017.

5) References



If you enjoyed this article and exploit, there is a bunch of things you must see:

[1] Simple ioctl fuzzer

[2] Defining IOCTL code

[3] Tarjei Mandt paper

[4] Easy local Windows Kernel exploitation by Cesar Cerrudo

[5] Leaking Kernel Addresses

[6] This extension is great for investigating the pool state

[7] Source code of the exploit
Research, exploit and article by BAYET Corentin

Related Contents

This website uses cookies to ensure you get the best experience on our website. Learn more.