• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Ядро Windows / Внутреннее устройство

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
В этой теме буду публиковать переводы вот отсюда
Спасибо heybabyone за материал.
Если есть предложения что перевести, для них есть тема

Часть 1 - Настройка среды отладки ядра с помощью kdnet и WinDBG Preview
Это небольшая заметка, показывающая, как начать отладку ядра Windows с помощью kdnet.exe и WinDBG Preview (новый WinDBG, который вы можете получить в Windows Store).

Термины
  • Debugger - локальный хост, на котором будет работать WinDBG. В моем случае хост с IP 192.168.2.79
  • Debuggee - удаленный хост, который будет отлаживаться хостом, на котором запущен отладчик. В моем случае - хост с IP 192.168.2.68
На Debuggee
Скопируйте kdnet.exe и VerifiedNICList.xml на хост отладки. Получите эти файлы с хоста, на котором установлен Windows Development Kit, в C: \Program Files (x86)\Windows Kits\10\Debuggers\x64:

1.png


В привилегированой консоли:
Bash:
kdnet 192.168.2.79 50001

Ниже показано, как kdnet выводит команду, которую необходимо запустить на хосте отладчика:
Bash:
windbg -k net:port=50001,key=1dk3k2bprui6m.26vzkoub4jmjl.3v6rvfqjys3ek.6kyxal1u1w6s

2.png


Скопируйте, сохраните в блокноте и перезагрузите debugee.

На Debugger
В WinDBG Preview перейдите к: Начать отладку>Присоединиться к ядру и введите порт и ключ, полученный при запуске kdnet на debuggee:

3.png


Нажмите OK, и теперь вы должны быть готовы начать отладку хоста 192.168.2.68:

71cd163f763c4239a320681a12f9b4e7I5G4VzngpFTK4654-0.png

*В оригинале сдесь gif, но он почему-то отказался подгружаться на форум

Ссылки
 
Часть 2 - Компиляция простого драйвера ядра, DbgPrint, DbgView
Простейший драйвер на Windows Driver Framework (WDF)
Выберите драйвер режима ядра, Emtpy (KMDF) из шаблонов:

1.png


Создайте driver.c
Создайте driver.c в Source Files

2.png


Добавьте код драйвера
C:
#include <ntddk.h>
#include <wdf.h>

DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd;
EVT_WDF_DRIVER_UNLOAD UnloadDriver;

_Use_decl_annotations_
void UnloadDriver(IN WDFDRIVER driver)
{
    UNREFERENCED_PARAMETER(driver);
    DbgPrint("Driver unloaded");
}

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
    WDF_DRIVER_CONFIG config;
    WDF_DRIVER_CONFIG_INIT(&config, EvtDriverDeviceAdd);
    config.EvtDriverUnload = UnloadDriver;
    NTSTATUS status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
    
    DbgPrint("Driver loaded");

    return status;
}

NTSTATUS EvtDriverDeviceAdd(_In_ WDFDRIVER Driver,_Inout_ PWDFDEVICE_INIT DeviceInit)
{
    UNREFERENCED_PARAMETER(Driver);
    WDFDEVICE device;
    NTSTATUS status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &device);
    
    return status;
}

Включение мониторинга DbgPrint для WinDBG
Измените подробность вывода отладки:
Bash:
ed kd_default_mask 0xf

3.png


Запуск драйвера позволяет нам увидеть вывод отладки в WinDBG:
*Следующий пост про запуск драйвера

4.png


Включение мониторинга DbgPrint для DbgView
Создайте под ключ Debug Print Filter, если он не существует:
Код:
Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter

Добавьте новое DWORD значение DEFAULT и установите для его в значение 0xf:

5.png


Если мы загрузим драйвер сейчас и запустим его, мы также сможем увидеть вывод отладки в DbgView:

6.png


Запрошенный элемент управления недействителен для этой службы
Приведенное ниже сообщение об ошибке отображается, если вы пытаетесь остановить драйвер WDF с помощью загрузчика драйверов OSR или собственного sc.exe,
даже если вы определили процедуру выгрузки драйвера:

7.png


Я не смог найти решения этой проблемы, но у драйвера WDM такой проблемы нет - см. Код ниже.

Загрузка и выгрузка драйвера ядра простой модели драйвера Windows (WDM)
Ниже приведен простой драйвер WDM, который можно скомпилировать, а затем загрузить и остановить с помощью загрузчика драйверов OSR:
C:
#include <ntddk.h>

void DriverUnload(PDRIVER_OBJECT dob)
{
    UNREFERENCED_PARAMETER(dob);
    DbgPrint("Driver unloaded");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {

    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);

    DriverObject->DriverUnload = DriverUnload;
    DbgPrint("Driver loaded");

    return STATUS_SUCCESS;
}

Ниже показано, как наш драйвер загружается и выгружается с помощью загрузчика OSR, в то время как DbgView распечатывает наши выходные данные DbgPrint,
определенные в приведенных выше процедурах DriverEntry и DriverUnload:

8.png


Ссылки

 
Пожалуйста, обратите внимание, что пользователь заблокирован
Очень годный перевод! Однозначно лайк. Статья очень хорошо подойдет тем, кто хочет прогать драйвера и писать на ring0 (руткиты, обход проактивки).

Jeleshka, если есть желание то на том сайте очень много интересного материала
 
Часть 3 - Загрузка драйвера ядра Windows для отладки
Загрузка драйвера с помощью загрузчика OSR
В системе, в которой вы хотите загрузить драйвер (отладчик), из командной строки с повышенными привилегиями отключите проверку целостности драйверов,
чтобы мы могли загрузить наши неподписанные драйверы в Windows 10:
Bash:
bcdedit /set nointegritychecks on; bcdedit /set testsigning on

1.png


После перезагрузки системы откройте загрузчик OSR и загрузите драйвер, как показано ниже:

2.gif


Обратите внимание, что мое имя драйвера было kmdfHelloDriver. Теперь мы можем подтвердить успешную загрузку драйвера, отладив ядро:
Bash:
0: kd> db kmdfHelloDriver

3.gif


Кроме того, мы можем проверить это таким образом, показав некоторые основные сведения о загруженном модуле:
Bash:
0: kd> ln kmdfHelloDriver

4.png


Если мы проверим это через диспетчер конфигурации службы, мы также увидим, что наш драйвер теперь загружен и работает:
Bash:
sc.exe query kmdfHelloDriver

5.png


Загрузка драйвера через командную строку + WinDBG

Преимущество загрузки драйвера ядра таким способом заключается в том, что он не зависит от загрузчика драйверов OSR или каких-либо других сторонних инструментов,
а также является гораздо более эффективным.

Важно: Чтобы этот метод работал, отладчик WinDBG должен быть присоединен к целевой машине.

Подготовка профиля Powershell
В отлаживаемой среде запустите консоль PowerShell с повышенными привилегиями и выполните следующие действия:
Bash:
notepad $PROFILE.AllUsersAllHosts

в профиле powershell добавьте следующую функцию powershell:
Bash:
function Install-Driver($name)
{
    $cleanName = $name -replace ".sys|.\\", ""

    sc.exe stop $cleanName
    sc.exe delete $cleanName

    cp $name c:\windows\system32\drivers\ -verbose -force
    sc.exe create $cleanName type= kernel start= demand error= normal binPath= c:\windows\System32\Drivers\$cleanName.sys DisplayName= $cleanName

    sc.exe start $cleanName
}

Вышеупомянутая функция Install-Driver принимает один параметр $name, который обозначает имя драйвера, который мы хотим установить.

Функция Install-Driver:
  • Попытается остановить службу (выгрузить драйвер), если она уже запущена (без проверки ошибок)
  • Попытается удалить сервис (без проверки ошибок)
  • Скопирует драйвер из текущего каталога в C:\windows\system32\drivers
  • Cоздаст сервис для драйвера
  • И запустит его(загрузит драйвер)
На скриншоте ниже показаны два описанных выше шага:

6.png


После сохранения профиля PowerShell закройте консоль PowerShell и снова откройте ее, чтобы можно было использовать функцию Install-Driver.

Загрузка драйвера
Перейдите в папку, содержащую файл .sys драйвера, который вы хотите установить, в моем случае это wdm-helloworld.sys в Z:\wdm-helloworld\x64\Debug:

7.png


Теперь мы можем установить драйвер, просто вызвав:
Bash:
Install-Driver wdm-helloworld.sys

8.gif


Трассировка исходников
Если у нас есть исходный код драйвера, который мы хотим отладить, мы можем загрузить его исходный код и пройти через него в WinDBG. Загрузите исходный код через Source>Open Source File и повторно загрузите драйвер, используя функцию Install-Driver:

9_1.png

*В оригинале сдесь большая gif, но движок форума отказывается её подгружать
 
Часть 4 - Подписка на уведомления о создании процессов, создании потоков и загрузке изображений из драйвера ядра
Это быстрая лабораторная работа с некоторыми из интересными уведомлениями, на которые могут подписаться драйверы ядра:
  • PsSetCreateProcessNotifyRoutine - уведомляет драйвер о новых / завершенных процессах
  • PsSetCreateProcessNotifyRoutineEx - уведомляет драйвер о создаваемых новых процессах, позволяет убить их, прежде чем они смогут запуститься
  • PsSetCreateThreadNotifyRoutine - уведомляет драйвер о новых / завершенных потоках
  • PsSetLoadImageNotifyRoutine - уведомляет драйвер о загруженных процессами DLL

PsSetCreateProcessNotifyRoutine

PsSetCreateProcessNotifyRoutine принимает два параметра:
C:
NTSTATUS PsSetCreateProcessNotifyRoutine(
  // указатель на функцию, которая будет вызываться при порождении или завершении процесса
  PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
  // указывает, подписаться или отказаться от подписки на это событие
  BOOLEAN                        Remove
);

Ниже приведен фрагмент, который показывает, как процедура sCreateProcessNotifyRoutine (строка 2) регистрируется для уведомлений о новых / завершенных процессах в строке 24:
C:
// обрабатывает входящие уведомления о новых / завершенных процессах
void sCreateProcessNotifyRoutine(HANDLE ppid, HANDLE pid, BOOLEAN create)
{
    if (create)
    {
        PEPROCESS process = NULL;
        PUNICODE_STRING parentProcessName = NULL, processName = NULL;
       
        PsLookupProcessByProcessId(ppid, &process);
        SeLocateProcessImageName(process, &parentProcessName);

        PsLookupProcessByProcessId(pid, &process);
        SeLocateProcessImageName(process, &processName);

        DbgPrint("%d %wZ\n\t\t%d %wZ", ppid, parentProcessName, pid, processName);
    }
    else
    {
        DbgPrint("Process %d lost child %d", ppid, pid);
    }
}

// Зарегистрируйте функцию sCreateProcessNotifyRoutine для получения уведомлений о новых / завершенных процессах
PsSetCreateProcessNotifyRoutine(sCreateProcessNotifyRoutine, FALSE);

Ниже показано, как выполняется процедура sCreateProcessNotifyRoutine, когда Powershell (PID 7176) порождает новый процесс hostname.exe (PID 2892). Кроме того, он показывает, что процесс 7176 (имя хоста) завершен:

1.gif


PsSetLoadImageNotifyRoutine
PsSetLoadImageNotifyRoutine принимает только один параметр - указатель на функцию, которая будет обрабатывать уведомления о библиотеках DLL, которые обрабатываются в загруженной системе:
C:
NTSTATUS PsSetLoadImageNotifyRoutine(
  PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

Ниже показано, что процедура sLoadImageNotifyRoutine будет обрабатывать наши уведомления, зарегистрированные с помощью PsSetLoadImageNotifyRoutine в строке 14:
C:
// обрабатывать входящие уведомления о загрузке модуля
void sLoadImageNotifyRoutine(PUNICODE_STRING imageName,    HANDLE pid, PIMAGE_INFO imageInfo)
{
    UNREFERENCED_PARAMETER(imageInfo);
    PEPROCESS process = NULL;
    PUNICODE_STRING processName = NULL;
    PsLookupProcessByProcessId(pid, &process);
    SeLocateProcessImageName(process, &processName);

    DbgPrint("%wZ (%d) loaded %wZ", processName, pid, imageName);
}

// Зарегистрируйте функцию sLoadImageNotifyRoutine для получения уведомлений о новых библиотеках DLL, загружаемых в процессы
PsSetLoadImageNotifyRoutine(sLoadImageNotifyRoutine);

Тестирование драйвера - как только мы открываем notepad.exe, наш драйвер получает уведомление обо всех модулях, загруженных notepad.exe:

assets%2F-LFEMnER3fywgFHoroYn%2F-M1rc8VBdW_t7Jiuaphe%2F-M1rffRj-dtQ2vI0zywE%2FPsSetLoadImageNotifyRoutine.gif


*К сожалению подгрузить большую gif на форум опять не получилось

PsSetCreateThreadNotifyRoutine
PsSetCreateThreadNotifyRoutine принимает только один параметр - указатель на функцию, которая будет обрабатывать уведомления о новых или убитых потоках во всех системных процессах:
C:
NTSTATUS PsSetCreateThreadNotifyRoutine(
  PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);

Ниже показано, что процедура sCreateThreadNotifyRoutine будет обрабатывать наши уведомления, зарегистрированные с помощью PsSetCreateThreadNotifyRoutine в строке 15:
C:
// обрабатывать входящие уведомления о новых / завершенных процессах
void sCreateThreadNotifyRoutine(HANDLE pid, HANDLE tid, BOOLEAN create)
{
    if (create)
    {
        DbgPrint("%d created thread %d", pid, tid);
    }
    else
    {
        DbgPrint("Thread %d of process %d exited", tid, pid);
    }
}

// Зарегестрируйте sCreateThreadNotifyRoutine для получения уведомлений о создании / завершении потока
PsSetCreateThreadNotifyRoutine(sCreateThreadNotifyRoutine);

Теперь, тестируя драйвер, мы видим, что действительно получаем уведомления о новых и завершенных потоках между процессами в нашей системе:

3.png


PsSetCreateProcessNotifyRoutineEx
PsSetCreateProcessNotifyRoutineEx принимает два аргумента:
C:
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  // указатель на функцию, которая будет вызываться при создании процесса
  PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  // указывает, подписаться или отказаться от подписки на это событие
  BOOLEAN                           Remove
);

Ниже приведен фрагмент, который показывает, как процедура sCreateProcessNotifyRoutineEx (строка 3) регистрируется для уведомлений о новых процессах в строке 19. Процессы с командной строкой, содержащей блокнот, будут завершены путем установки для члена createInfo.reationStatus значения STATUS_ACCESS_DENIED (строка 13)
C:
// обрабатывать входящие уведомления о новых / завершенных процессах и убивать их
// процессы, у которых есть "notepad" в аргументах командной строки
void sCreateProcessNotifyRoutineEx(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo)
{
    UNREFERENCED_PARAMETER(process);
    UNREFERENCED_PARAMETER(pid);
   
    if (createInfo != NULL)
    {
        if (wcsstr(createInfo->CommandLine->Buffer, L"notepad") != NULL)
        {
            DbgPrint("[!] Access to launch notepad.exe was denied!");
            createInfo->CreationStatus = STATUS_ACCESS_DENIED;
        }
    }
}

// subscribe sCreateProcessNotifyRoutineEx to new / terminated process notifications
PsSetCreateProcessNotifyRoutineEx(sCreateProcessNotifyRoutineEx, FALSE);

Если PsSetCreateProcessNotifyRoutineEx не работает в вашем драйвере, вам нужно будет добавить переключатель / целостность проверки в конфигурацию компоновщика.

4.png


Ниже показано, как наш драйвер блокирует попытку запустить notepad.exe:

5.gif


Код
Ниже приведен полный рабочий код драйвера, который регистрирует все процедуры обратного вызова, упомянутые выше:
C:
#include <Ntifs.h>
#include <ntddk.h>
#include <wdm.h>

DRIVER_DISPATCH HandleCustomIOCTL;
#define IOCTL_SPOTLESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x2049, METHOD_BUFFERED, FILE_ANY_ACCESS)
UNICODE_STRING DEVICE_NAME = RTL_CONSTANT_STRING(L"\\Device\\SpotlessDevice");
UNICODE_STRING DEVICE_SYMBOLIC_NAME = RTL_CONSTANT_STRING(L"\\??\\SpotlessDeviceLink");

void sCreateProcessNotifyRoutine(HANDLE ppid, HANDLE pid, BOOLEAN create)
{
    if (create)
    {
        PEPROCESS process = NULL;
        PUNICODE_STRING parentProcessName = NULL, processName = NULL;
       
        PsLookupProcessByProcessId(ppid, &process);
        SeLocateProcessImageName(process, &parentProcessName);

        PsLookupProcessByProcessId(pid, &process);
        SeLocateProcessImageName(process, &processName);

        DbgPrint("%d %wZ\n\t\t%d %wZ", ppid, parentProcessName, pid, processName);
    }
    else
    {
        DbgPrint("Process %d lost child %d", ppid, pid);
    }
}

void sCreateProcessNotifyRoutineEx(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo)
{
    UNREFERENCED_PARAMETER(process);
    UNREFERENCED_PARAMETER(pid);
   
    if (createInfo != NULL)
    {
        if (wcsstr(createInfo->CommandLine->Buffer, L"notepad") != NULL)
        {
            DbgPrint("[!] Access to launch notepad.exe was denied!");
            createInfo->CreationStatus = STATUS_ACCESS_DENIED;
        }
    }
}

void sLoadImageNotifyRoutine(PUNICODE_STRING imageName,    HANDLE pid, PIMAGE_INFO imageInfo)
{
    UNREFERENCED_PARAMETER(imageInfo);
    PEPROCESS process = NULL;
    PUNICODE_STRING processName = NULL;
    PsLookupProcessByProcessId(pid, &process);
    SeLocateProcessImageName(process, &processName);

    DbgPrint("%wZ (%d) loaded %wZ", processName, pid, imageName);
}

void sCreateThreadNotifyRoutine(HANDLE pid, HANDLE tid, BOOLEAN create)
{
    if (create)
    {
        DbgPrint("%d created thread %d", pid, tid);
    }
    else
    {
        DbgPrint("Thread %d of process %d exited", tid, pid);
    }
}

void DriverUnload(PDRIVER_OBJECT dob)
{
    DbgPrint("Driver unloaded, deleting symbolic links and devices");
    IoDeleteDevice(dob->DeviceObject);
    IoDeleteSymbolicLink(&DEVICE_SYMBOLIC_NAME);
    PsSetCreateProcessNotifyRoutine(sCreateProcessNotifyRoutine, TRUE);
    PsRemoveLoadImageNotifyRoutine(sLoadImageNotifyRoutine);
    PsRemoveCreateThreadNotifyRoutine(sCreateThreadNotifyRoutine);
    PsSetCreateProcessNotifyRoutineEx(sCreateProcessNotifyRoutineEx, TRUE);
}

NTSTATUS HandleCustomIOCTL(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    PIO_STACK_LOCATION stackLocation = NULL;
    CHAR *messageFromKernel = "ohai from them kernelz";

    stackLocation = IoGetCurrentIrpStackLocation(Irp);
   
    if (stackLocation->Parameters.DeviceIoControl.IoControlCode == IOCTL_SPOTLESS)
    {
        DbgPrint("IOCTL_SPOTLESS (0x%x) issued", stackLocation->Parameters.DeviceIoControl.IoControlCode);
        DbgPrint("Input received from userland: %s", (char*)Irp->AssociatedIrp.SystemBuffer);
    }

    Irp->IoStatus.Information = strlen(messageFromKernel);
    Irp->IoStatus.Status = STATUS_SUCCESS;
   
    DbgPrint("Sending to userland: %s", messageFromKernel);
    RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, messageFromKernel, strlen(Irp->AssociatedIrp.SystemBuffer));
   
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS MajorFunctions(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);

    PIO_STACK_LOCATION stackLocation = NULL;
    stackLocation = IoGetCurrentIrpStackLocation(Irp);

    switch (stackLocation->MajorFunction)
    {
    case IRP_MJ_CREATE:
        DbgPrint("Handle to symbolink link %wZ opened", DEVICE_SYMBOLIC_NAME);
        break;
    case IRP_MJ_CLOSE:
        DbgPrint("Handle to symbolink link %wZ closed", DEVICE_SYMBOLIC_NAME);
        break;
    default:
        break;
    }
   
    Irp->IoStatus.Information = 0;
    Irp->IoStatus.Status = STATUS_SUCCESS;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
   
    NTSTATUS status    = 0;

    // routine that will execute when our driver is unloaded/service is stopped
    DriverObject->DriverUnload = DriverUnload;
   
    // routine for handling IO requests from userland
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HandleCustomIOCTL;
   
    // routines that will execute once a handle to our device's symbolik link is opened/closed
    DriverObject->MajorFunction[IRP_MJ_CREATE] = MajorFunctions;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = MajorFunctions;
   
    DbgPrint("Driver loaded");

    // subscribe to notifications
    PsSetCreateProcessNotifyRoutine(sCreateProcessNotifyRoutine, FALSE);
    PsSetLoadImageNotifyRoutine(sLoadImageNotifyRoutine);
    PsSetCreateThreadNotifyRoutine(sCreateThreadNotifyRoutine);
    PsSetCreateProcessNotifyRoutineEx(sCreateProcessNotifyRoutineEx, FALSE);
    DbgPrint("Listeners isntalled..");

    IoCreateDevice(DriverObject, 0, &DEVICE_NAME, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DriverObject->DeviceObject);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("Could not create device %wZ", DEVICE_NAME);
    }
    else
    {
        DbgPrint("Device %wZ created", DEVICE_NAME);
    }

    status = IoCreateSymbolicLink(&DEVICE_SYMBOLIC_NAME, &DEVICE_NAME);
    if (NT_SUCCESS(status))
    {
        DbgPrint("Symbolic link %wZ created", DEVICE_SYMBOLIC_NAME);
    }
    else
    {
        DbgPrint("Error creating symbolic link %wZ", DEVICE_SYMBOLIC_NAME);
    }
   
    return STATUS_SUCCESS;
}

Ссылки
 
Часть 5 - Список открытых дескрипторов и поиск адресов объектов ядра
Можно перечислить все открытые дескрипторы (процессы, файлы, мьютексы, ключи, разделы и т. Д.) В системе (права администратора не требуются), что означает, что можно получить виртуальный адрес любого объекта ядра (например, EPROCESS для объект процесса) в пространстве ядра из пользовательского пространства.

Возможность найти виртуальный адрес объекта ядра (например, EPROCESS) полезна при эксплуатации ядра. Например, если вы скомпрометируете машину и обнаружите, что существует уязвимый драйвер, с помощью которого вы можете
читать / записывать память ядра из пользовательского пространства, вы можете использовать его для повышения привилегий, обнаружив объект ядра EPROCESS привилегированного процесса, например winlogon.exe,
украв его токен безопасности и применив его к процессу cmd.exe с низким уровнем привилегий, чтобы получить оболочку с привилегиями SYSTEM.

Список всех открытых дескрипторов в системе получается с помощью NtQuerySystemInformation API и пары недокументированных, но хорошо известных структур SYSTEM_HANDLE_INFORMATION и SYSTEM_HANDLE_TABLE_ENTRY_INFO.

Код
Код ниже извлекает все дескрипторы, открытые процессом SYSTEM (PID 4):
  • Код ниже не обрабатывает ошибки
  • SystemHandleInformationSize - это жестко запрограммированное значение, которое не следует использовать в производственном коде. Вместо этого вам следует:
    • Начать с произвольного размера для SystemHandleInformationSize
    • Вызывать NtQuerySystemInformation в цикле, пока он больше не вернет 0xc0000004 (STATUS_INFO_LENGTH_MISMATCH)​
    • Если возвращается 0xc0000004, увеличьте размер SystemHandleInformation​

C:
#include <iostream>
#include <Windows.h>
#include <winternl.h>

#define SystemHandleInformation 0x10
#define SystemHandleInformationSize 1024 * 1024 * 2

using fNtQuerySystemInformation = NTSTATUS(WINAPI*)(
    ULONG SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
);

// handle information
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
    USHORT UniqueProcessId;
    USHORT CreatorBackTraceIndex;
    UCHAR ObjectTypeIndex;
    UCHAR HandleAttributes;
    USHORT HandleValue;
    PVOID Object;
    ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO;

// handle table information
typedef struct _SYSTEM_HANDLE_INFORMATION
{
    ULONG NumberOfHandles;
    SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;


int main()
{
    ULONG returnLenght = 0;
    fNtQuerySystemInformation NtQuerySystemInformation = (fNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"ntdll"), "NtQuerySystemInformation");
    PSYSTEM_HANDLE_INFORMATION handleTableInformation = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, SystemHandleInformationSize);
    NtQuerySystemInformation(SystemHandleInformation, handleTableInformation, SystemHandleInformationSize, &returnLenght);

    for (int i = 0; i < handleTableInformation->NumberOfHandles; i++)
    {
        SYSTEM_HANDLE_TABLE_ENTRY_INFO handleInfo = (SYSTEM_HANDLE_TABLE_ENTRY_INFO)handleTableInformation->Handles[i];

        if (handleInfo.UniqueProcessId == 4)
        {
            printf_s("Handle 0x%x at 0x%p, PID: %x\n", handleInfo.HandleValue, handleInfo.Object, handleInfo.UniqueProcessId);
        }
        else
        {
            break;
        }
    }

    return 0;
}

Помните: Приведенный выше код можно легко изменить, чтобы найти местоположение объекта в ядре по его дескриптору.

Проверка
Давайте посмотрим, правильно ли перечислены дескрипторы в приведенном выше коде и адреса объектов, на которые эти дескрипторы указывают в памяти ядра.

Если мы скомпилируем и запустим код, мы получим список всех дескрипторов для процесса с PID 4:

1.png


Мы можем перепроверить и убедиться в точности перечисленных дескрипторов с помощью Process Hacker, проверив вкладку «Handlers» процесса SYSTEM (PID 4). Проверим первый дескриптор 0x4:

2.png


Выше показано:
  • Зелёным - handle id(0x4)
  • Синим - process id(4), у которого открыт дескриптор 0x4 (процесс SYSTEM имеет дескриптор самого себя)
  • Красным - расположение объекта (на которое указывает дескриптор) в памяти ядра (0xffff87077c882300)
Мы можем легко проверить объект по адресу 0xffff8f077c882300 в WinDBG:
Bash:
!object 0xffff8f077c882300

Приведенная выше команда указывает, что 0xffff8f077c882300 является допустимым адресом объекта и имеет тип Process:
3.png


Мы можем подтвердить, что 0xffff8f077c882300 является объектом процесса, используя команду !process в WinDBG:
Bash:
!process 0xffff8f077c882300 0

Ниже подтверждается, что это действительно объект процесса:
  • Красным - расположение объекта процесса в памяти ядра (0xffff8f077c882300)
  • Синим - process id (4)
  • Лаймовым - process name (system)
4.png


Наконец, мы можем наложить _EPROCESS поверх ffff8f077c882300 и распечатать UniqueProcessId и ImageFileNames, что еще раз подтвердит, что это процесс SYSTEM с PID 4:
Bash:
dt _eprocess ffff8f077c882300 uniqueprocessid imagefilename

5.png


Ссылки
 
Часть 6 - Отправка команд из вашей пользовательской программы в драйвер ядра с помощью IOCTL
Это быстрое упражнение, демонстрирующее, как:
  • Создайте простой драйвер режима ядра WDM, который может получать и отвечать на настраиваемый код управления вводом / выводом (IOCTL), отправляемый из пользовательской программы.
  • Создайте простую программу пользовательского пространства, которая может отправлять настраиваемый IOCTL в драйвер ядра.
  • Передайте некоторые данные из пользовательской программы в драйвер ядра через DeviceIoConctrol
  • Передайте некоторые данные обратно из ядра в пользовательскую программу
Ниже приведены основные фрагменты кода, из которых будет создан драйвер ядра и программа пользовательского пространства.

Драйвер ядра

Заполнение DriverObject подпрограммами обратного вызова IRP
Внутри функции ввода драйвера мы заполняем наш объект драйвера указателями на важные подпрограммы, которые будут выполняться, например, когда драйвер выгружается, или дескриптор символьной ссылки его устройства получен (IRP_MJ_CREATE) или закрыт (IRP_MJ_CLOSE):

1.png


Это необходимо, потому что эти функции драйвера (обратные вызовы) будут вызываться ОС, когда эти события (например, приложение пользовательского уровня, пытающееся получить дескриптор нашего устройства, выгрузить драйвер или закрыть дескриптор устройства) будут срабатывать. Мы не хотим, чтобы ОС не знала, что делать с нашим драйвером при возникновении этих событий, поэтому мы сообщаем об этом.

Создание устройства и его символической ссылки
Здесь мы создаем устройство (для которого мы пишем драйвер) и его символическую ссылку. Символическая ссылка требуется, когда мы хотим получить доступ к нашему драйверу из пользовательского пространства (открыв дескриптор устройства, вызвав CreateFile) и попросив его выполнить некоторый код в соответствии с нашим настраиваемым IOCTL:

2.png


  • Управляющий код IOCTL - это код, который отправляется драйверу устройства через запрос RP_MJ_DEVICE_CONTROL с использованием DeviceIoControl WinAPI.
  • Управляющий код IOCTL сообщает драйверу, какое действие ему необходимо выполнить.
  • Например, код IOCTL 0x202 (IOCTL_STORAGE_EJECT_MEDIA) может быть отправлен на устройство USB / CDROM, и его драйвер будет выполнять соответствующее действие для данного устройства, то есть открыть лоток для компакт-диска для компакт-диска или извлечь запоминающее устройство USB.

Ниже показано имя устройства и его символическая ссылка, которую мы используем в этом упражнении:

3.png


После создания устройства и его символических ссылок вновь созданное устройство SpotlessDevice теперь отображается внутри WinObj:

4.png


Кроме того, мы видим символическую ссылку Spotless DeviceLink, указывающую на ваше устройство \Device\SpotlessDevice:

5.png


Основные функции
Эта функция будет обрабатывать пакеты IRP, которые запрашивают (CreateFile) или закрывают (CloseHandle) дескриптор нашего устройства \Device\SpotlessDevice через символическую ссылку \\.\SpotlessDeviceLink:

6.png


Ниже показано, как запросы IRP IRP_MJ_CREATE (для получения дескриптора \Device\SpotlessDevice через символическую ссылку) и IRP_MJ_CLOSE (для закрытия дескриптора) выполняются, когда мы дважды щелкаем по SpotlessDevice в WinObj:

7.gif


HandleCustomIOCTL
Эта процедура будет обрабатывать запросы IOCTL, отправленные из нашей пользовательской программы. В этом упражнении, когда он получает код IOCTL для IOCTL_SPOTLESS, он печатает строку, полученную из аргумента командной строки нашей пользовательской программы. Кроме того, он отправит обратно строку для распечатки пользовательской программы:

8.png


Когда IoDeviceControl вызывается в пользовательском пространстве с настраиваемым IOCTL и любыми входными данными, которые мы хотим отправить в ядро, ОС перехватывает этот запрос и упаковывает его в пакет ввода-вывода (IRP), который затем будет передан нашему обратный вызов HandleCustomIOCTL, который мы ранее зарегистрировали в подпрограмме DriverEntry для IRP IRP_MJ_DEVICE_CONTROL.
IRP, помимо прочего, содержит входящий код IOCTL, входные данные, отправленные из запроса пользовательской среды, и буфер, который код драйвера ядра может использовать для отправки ответа обратно программе пользовательской среды.

Определение пользовательского IOCTL
  • Код IOCTL должен быть определен как в драйвере ядра, так и в пользовательской программе.
  • Код IOCTL обычно определяется с помощью макроса CTL_CODE.
  • Microsoft предлагает использовать любой код, начиная с 0x800:
9.png


Программа Userland
Ниже приведен код пользовательского пространства, который получает дескриптор устройства \Device\SpotlessDevice через его символическую ссылку \\.\SpotlessDeviceLink, которую мы создали ранее внутри процедуры DriverEntry драйвера:

10.png


Выдача настраиваемого IOCTL драйверу и отправка ему указателя на строку, которая поступает в качестве аргумента командной строки в нашу программу пользовательского пространства, путем вызова DeviceIoControl:

11.png


Кроме того, приведенный выше код выводит строку, полученную от ядра.

Демо
Ниже показано:
  • Мы выполняем нашу пользовательскую программу со строкой spotless saying ola from userland в качестве аргумента
  • Этот аргумент отправляется драйверу ядра через наш настраиваемый IOCTL IOCTL_SPOTLESS.
  • Ядро отправляет некоторые данные в пользовательскую программу.
  • Программа пользовательского уровня получает текст обратно от ядра и печатает его в DbgView.
12.gif


Код
  • driver.c - это код драйвера, который получает и отвечает на запросы IOCTL, отправленные из пользовательского пространства, и отправляет некоторые данные обратно в пользовательскую программу.
  • userland.cpp - это программа пользовательского уровня, отправляющая IOCTL и получающая данные от драйвера ядра.
C:
#include <wdm.h>

DRIVER_DISPATCH HandleCustomIOCTL;
#define IOCTL_SPOTLESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x2049, METHOD_BUFFERED, FILE_ANY_ACCESS)
UNICODE_STRING DEVICE_NAME = RTL_CONSTANT_STRING(L"\\Device\\SpotlessDevice");
UNICODE_STRING DEVICE_SYMBOLIC_NAME = RTL_CONSTANT_STRING(L"\\??\\SpotlessDeviceLink");

void DriverUnload(PDRIVER_OBJECT dob)
{
    DbgPrint("Driver unloaded, deleting symbolic links and devices");
    IoDeleteDevice(dob->DeviceObject);
    IoDeleteSymbolicLink(&DEVICE_SYMBOLIC_NAME);
}

NTSTATUS HandleCustomIOCTL(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    PIO_STACK_LOCATION stackLocation = NULL;
    CHAR *messageFromKernel = "ohai from them kernelz";

    stackLocation = IoGetCurrentIrpStackLocation(Irp);
    
    if (stackLocation->Parameters.DeviceIoControl.IoControlCode == IOCTL_SPOTLESS)
    {
        DbgPrint("IOCTL_SPOTLESS (0x%x) issued", stackLocation->Parameters.DeviceIoControl.IoControlCode);
        DbgPrint("Input received from userland: %s", (char*)Irp->AssociatedIrp.SystemBuffer);
    }

    Irp->IoStatus.Information = strlen(messageFromKernel);
    Irp->IoStatus.Status = STATUS_SUCCESS;
    
    DbgPrint("Sending to userland: %s", messageFromKernel);
    RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, messageFromKernel, strlen(Irp->AssociatedIrp.SystemBuffer));
    
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS MajorFunctions(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);

    PIO_STACK_LOCATION stackLocation = NULL;
    stackLocation = IoGetCurrentIrpStackLocation(Irp);

    switch (stackLocation->MajorFunction)
    {
    case IRP_MJ_CREATE:
        DbgPrint("Handle to symbolink link %wZ opened", DEVICE_SYMBOLIC_NAME);
        break;
    case IRP_MJ_CLOSE:
        DbgPrint("Handle to symbolink link %wZ closed", DEVICE_SYMBOLIC_NAME);
        break;
    default:
        break;
    }
    
    Irp->IoStatus.Information = 0;
    Irp->IoStatus.Status = STATUS_SUCCESS;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
    
    NTSTATUS status    = 0;

    // routine that will execute when our driver is unloaded/service is stopped
    DriverObject->DriverUnload = DriverUnload;
    
    // routine for handling IO requests from userland
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HandleCustomIOCTL;
    
    // routines that will execute once a handle to our device's symbolik link is opened/closed
    DriverObject->MajorFunction[IRP_MJ_CREATE] = MajorFunctions;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = MajorFunctions;
    
    DbgPrint("Driver loaded");

    IoCreateDevice(DriverObject, 0, &DEVICE_NAME, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DriverObject->DeviceObject);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("Could not create device %wZ", DEVICE_NAME);
    }
    else
    {
        DbgPrint("Device %wZ created", DEVICE_NAME);
    }

    status = IoCreateSymbolicLink(&DEVICE_SYMBOLIC_NAME, &DEVICE_NAME);
    if (NT_SUCCESS(status))
    {
        DbgPrint("Symbolic link %wZ created", DEVICE_SYMBOLIC_NAME);
    }
    else
    {
        DbgPrint("Error creating symbolic link %wZ", DEVICE_SYMBOLIC_NAME);
    }
    
    return STATUS_SUCCESS;
}
C++:
#include <iostream>
#include <Windows.h>

#define IOCTL_SPOTLESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x2049, METHOD_BUFFERED, FILE_ANY_ACCESS)

int main(char argc, char ** argv)
{
    HANDLE device = INVALID_HANDLE_VALUE;
    BOOL status = FALSE;                 
    DWORD bytesReturned = 0;
    CHAR inBuffer[128] = {0};
    CHAR outBuffer[128] = {0};

    RtlCopyMemory(inBuffer, argv[1], strlen(argv[1]));
    
    device = CreateFileW(L"\\\\.\\SpotlessDeviceLink", GENERIC_ALL, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0);
    
    if (device == INVALID_HANDLE_VALUE)
    {
        printf_s("> Could not open device: 0x%x\n", GetLastError());
        return FALSE;
    }

    printf_s("> Issuing IOCTL_SPOTLESS 0x%x\n", IOCTL_SPOTLESS);
    status = DeviceIoControl(device, IOCTL_SPOTLESS, inBuffer, sizeof(inBuffer), outBuffer, sizeof(outBuffer), &bytesReturned, (LPOVERLAPPED)NULL);
    printf_s("> IOCTL_SPOTLESS 0x%x issued\n", IOCTL_SPOTLESS);
    printf_s("> Received from the kernel land: %s. Received buffer size: %d\n", outBuffer, bytesReturned);

    CloseHandle(device);
}

Ссылки
 
Часть 7 - Windows Kernel Drivers 101
В этом документе отражены некоторые концепции, связанные с драйверами ядра и ОС, с которыми я сталкиваюсь, когда изучаю разработку драйверов ядра Windows.
Work in progress

Типы драйверов

Существует много разных типов драйверов, но меня больше всего интересуют Программные драйверы.

Программные драйверы(Software Driver)
  • Не связан ни с одним устройством
  • Полезен для запуска кода в режиме ядра
  • Также может быть драйвером пользовательского режима
  • Драйверы могут быть разработаны с использованием инфраструктуры драйверов режима ядра (KMDF) и модели драйверов Windows (WDM).

KMDF vs WDM

  • WDM очень тесно связан с ОС и напрямую взаимодействует с ней, вызывая системные процедуры обслуживания.
  • KMDF - это фреймворк, который абстрагируется от разработки драйверов и позволяет разработчику сосредоточиться на своем драйвере, а не на тонкостях программирования ОС.
  • KMDF рекомендуется и в большинстве случаев является предпочтительной моделью разработки драйверов по сравнению с WDM.
Менеджер ввода / вывода
  • Интерфейс, обеспечивающий связь между пользовательскими приложениями и драйверами ядра.
  • Создает объект драйвера (DRIVER_OBJECT) для каждого установленного и загруженного драйвера.
  • Определяет набор стандартных обязательных процедур драйвера, которые драйверы должны поддерживать, например DriverEntry
  • Вызывает процедуру драйвера DriverEntry, которая предоставляет адрес драйвера DRIVER_OBJECT
  • Принимает запросы ввода-вывода, которые обычно исходят от приложений пользовательского режима.
  • Создает пакеты IRP для представления запросов ввода-вывода
  • Передает IRP соответствующим драйверам
Заметки без категорий
  • Все драйверы содержат процедуру DriverEntry - аналогичную основной программе исполняемого файла и DllMain библиотеки DLL. Эта процедура вызывается после загрузки и запуска драйвера ОС.
  • Память, выделенная в выгружаемом пуле, может быть выгружена на диск, тогда как память, выделенная из невыгружаемого пула, не может
  • Запросы, отправляемые драйверам, инкапсулируются в пакеты запросов ввода-вывода (IRP).
  • DRIVER_OBJECT представляет собой образ загруженного драйвера режима ядра:
C:
typedef struct _DRIVER_OBJECT {
  CSHORT             Type;
  CSHORT             Size;
  PDEVICE_OBJECT     DeviceObject;
  ULONG              Flags;
  PVOID              DriverStart;
  ULONG              DriverSize;
  PVOID              DriverSection;
  PDRIVER_EXTENSION  DriverExtension;
  UNICODE_STRING     DriverName;
  PUNICODE_STRING    HardwareDatabase;
  PFAST_IO_DISPATCH  FastIoDispatch;
  PDRIVER_INITIALIZE DriverInit;
  PDRIVER_STARTIO    DriverStartIo;
  PDRIVER_UNLOAD     DriverUnload;
  PDRIVER_DISPATCH   MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;
  • DRIVER_OBJECT содержит ссылки на точки входа в стандартные процедуры драйвера (например, выгрузку)
  • Стандартные процедуры драйвера получают пакеты IRP в качестве входных данных, а также указатель на целевой объект устройства.
  • Драйверы должны создать хотя бы один объект устройства (DEVICE_OBJECT) для каждого устройства.
  • Объекты устройства служат целью операций, выполняемых на устройстве.
  • Программные драйверы, которые обрабатывают только запросы ввода-вывода и не передают их на оборудование, по-прежнему должны создавать объект устройства, представляющий цель его операций.
Ссылки
 
Часть 8 - Соглашение о вызовах x64: Стек
Когда вызывается функция в двоичном файле Windows x64, кадр стека используется следующим образом:
  • Первые четыре целочисленных аргумента передаются в регистры RCX, RDX, R8 и R9 соответственно (зеленый)
  • Аргументы 5, 6 и далее помещаются в стек (синий)
  • Адрес возврата к следующей инструкции вызывающего абонента помещается в RSP + 0x0 (желтый)
  • Ниже адреса возврата (RSP + 0x0) 32 байта всегда выделяются для RCD, RDX, R8 и R9, даже если вызываемый объект использует менее 4 аргументов
  • Локальные переменные и энергонезависимые регистры хранятся над адресом возврата (красный)
  • RBP не используется для ссылки на локальные переменные / аргументы функции (за исключением случаев, когда функции используют alloca ()), как это было раньше для X86. За это отвечает RSP, поэтому значение RSP не меняется во всем теле функции (push и pop используются только для эпилога / пролога)
1_2.png


В качестве примера рассмотрим функцию msv1_0.LsaInitializePackage в Ghidra. Ниже показано, как первые четыре аргумента хранятся в ECX (нижняя часть RCX), RDX, R8 и R9:

1_3.png


Ссылки
 
Часть 9 - Таблица дескрипторов системной службы - SSDT
Что такое SSDT
Таблица диспетчеризации системных служб или SSDT - это просто массив адресов к подпрограммам ядра для 32-разрядных операционных систем или массив относительных смещений к тем же подпрограммам для 64-разрядных операционных систем.

SSDT - это первый член структуры памяти ядра таблицы дескрипторов служб, как показано ниже:
C:
typedef struct tagSERVICE_DESCRIPTOR_TABLE {
    SYSTEM_SERVICE_TABLE nt; //effectively a pointer to Service Dispatch Table (SSDT) itself
    SYSTEM_SERVICE_TABLE win32k;
    SYSTEM_SERVICE_TABLE sst3; //pointer to a memory address that contains how many routines are defined in the table
    SYSTEM_SERVICE_TABLE sst4;
} SERVICE_DESCRIPTOR_TABLE;

SSDT использовались антивирусами, а также руткитами, которые хотели скрыть файлы, ключи реестра, сетевые соединения и т. Д. Microsoft представила PatchGuard для систем x64, чтобы бороться с модификациями SSDT из-за BSOD.

На человеческом языке
Когда программа в пользовательском пространстве вызывает функцию, скажем CreateFile, в конечном итоге выполнение кода передается в ntdll!NtCreateFile и через системный вызов в подпрограмму ядра nt!NtCreateFile.

Системный вызов - это просто индекс в Таблице диспетчеризации системных служб (SSDT), который содержит массив указателей для 32-разрядных ОС (или относительные смещения к Таблице диспетчеризации служб для 64-разрядных ОС) на все критические системные API, такие как ZwCreateFile, ZwOpenFile и прочие..

Ниже приведена упрощенная диаграмма, которая показывает, как смещения в SSDT KiServiceTable преобразуются в абсолютные адреса соответствующих подпрограмм ядра:
1.png


По сути, системные вызовы и SSDT (KiServiceTable) работают вместе как мост между вызовами пользовательского API и соответствующими подпрограммами ядра, позволяя ядру знать, какая подпрограмма должна выполняться для данного системного вызова, инициированного в пользовательском пространстве.

Service Descriptor Table
В WinDBG мы можем проверить структуру таблицы дескрипторов сервиса KeServiceDescriptorTable, как показано ниже. Обратите внимание, что первый член распознается как KiServiceTable - это указатель на сам SSDT - таблицу отправки (или просто массив), содержащую все эти указатели / смещения:
Bash:
0: kd> dps nt!keservicedescriptortable L4
fffff801`9210b880  fffff801`9203b470 nt!KiServiceTable
fffff801`9210b888  00000000`00000000
fffff801`9210b890  00000000`000001ce
fffff801`9210b898  fffff801`9203bbac nt!KiArgumentTable

Попробуем распечатать пару значений из SSDT:
Bash:
0: kd> dd /c1 KiServiceTable L2
fffff801`9203b470  fd9007c4
fffff801`9203b474  fcb485c0

Как упоминалось ранее, на x64, с которым я работаю в своей лаборатории, SSDT содержит относительные смещения к процедурам ядра. Чтобы получить абсолютный адрес для заданного смещения, необходимо применить следующую формулу:

RoutineAbsoluteAddress=KiServiceTableAddress+(routineOffset>>>4)

Используя приведенную выше формулу и первое смещение fd9007c4, которое мы получили из KiServiceTable, мы можем определить, что это смещение указывает на nt!NtAccessCheck:
Bash:
0: kd> u KiServiceTable + (0xfd9007c4 >>> 4)
nt!NtAccessCheck:
fffff801`91dcb4ec 4c8bdc          mov     r11,rsp
fffff801`91dcb4ef 4883ec68        sub     rsp,68h
fffff801`91dcb4f3 488b8424a8000000 mov     rax,qword ptr [rsp+0A8h]
fffff801`91dcb4fb 4533d2          xor     r10d,r10d

Мы можем подтвердить это, если попытаемся разобрать nt!NtAccessCheck - адреса подпрограммы (fffff801`91dcb4ec) и первые инструкции (mov r11, rsp) приведенных выше и ниже команд совпадают:
C:
0: kd> u nt!NtAccessCheck L1
nt!NtAccessCheck:
fffff801`91dcb4ec 4c8bdc          mov     r11,rsp

2.png


Если мы вернемся к исходному чертежу того, как смещения SSDT преобразуются в абсолютные адреса, мы можем перерисовать его с конкретными значениями для syscall 0x1:

3.png


Поиск процедуры отправки для заданного системного вызова пользовательского пространства
В качестве простого упражнения, учитывая известный номер системного вызова, мы можем попытаться выяснить, какая процедура ядра будет вызываться после того, как этот системный вызов будет выполнен. Загрузим отладочные символы для модуля ntdll:
Bash:
.reload /f ntdll.dll
lm ntdll

4.png


Теперь давайте найдем системный вызов для ntdll!NtCreateFile:
Bash:
0: kd> u ntdll!ntcreatefile L2

... мы видим системный вызов 0x55:

5.png


Смещения в KiServiceTable имеют размер 4 байта, поэтому мы можем вычислить смещение для syscall 0x55, посмотрев на значение, которое KiServiceTable содержит в позиции 0x55:
Bash:
0: kd> dd /c1 kiservicetable+4*0x55 L1
fffff801`9203b5c4  01fa3007

Как видно из вышеизложенного, смещение для NtCreateFile равно 01fa3007. Используя формулу, описанную ранее для вычисления абсолютного адреса подпрограммы, мы подтверждаем, что рассматриваем подпрограмму ядра nt!TCreateFile, которая будет вызываться после того, как ntdll!NtCreateFile вызовет системный вызов 0x55:
Bash:
0: kd> u kiservicetable + (01fa3007>>>4) L1
nt!NtCreateFile:
fffff801`92235770 4881ec88000000  sub     rsp,88h

Давайте еще раз перерисуем предыдущую диаграмму для системного вызова 0x55 для ntdll!NtCreateFile:

6.png


Поиск адреса всех процедур SSDT
В качестве еще одного упражнения мы могли бы перебрать все элементы в таблице диспетчеризации служб и вывести абсолютные адреса для всех подпрограмм, определенных в таблице диспетчеризации:
Bash:
.foreach /ps 1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable L poi(keservicedescriptortable+0x10) }){ dp kiservicetable + ( offset >>> 4 ) L1 }
7.gif


Красиво, но не очень читабельно. Мы можем немного обновить цикл и распечатать имена API, связанные с этими абсолютными адресами:
Bash:
0: kd> .foreach /ps 1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable L poi(nt!KeServiceDescriptorTable+10)}){ r $t0 = ( offset >>> 4) + nt!KiServiceTable; .printf "%p - %y\n", $t0, $t0 }
fffff80191dcb4ec - nt!NtAccessCheck (fffff801`91dcb4ec)
fffff80191cefccc - nt!NtWorkerFactoryWorkerReady (fffff801`91cefccc)
fffff8019218df1c - nt!NtAcceptConnectPort (fffff801`9218df1c)
fffff801923f8848 - nt!NtMapUserPhysicalPagesScatter (fffff801`923f8848)
fffff801921afc10 - nt!NtWaitForSingleObject (fffff801`921afc10)
fffff80191e54010 - nt!NtCallbackReturn (fffff801`91e54010)
fffff8019213cf60 - nt!NtReadFile (fffff801`9213cf60)
fffff801921b2e80 - nt!NtDeviceIoControlFile (fffff801`921b2e80)
fffff80192212dc0 - nt!NtWriteFile (fffff801`92212dc0)
.....cut for brewity.....

Ссылки
 
Часть 10 - Таблица дескрипторов прерываний - IDT
Основное:
  • Прерывания можно рассматривать как уведомления ЦП о том, что в системе произошло какое-то событие. Классическими примерами прерываний являются аппаратные прерывания, такие как нажатие кнопок мыши или клавиш клавиатуры, активность сетевых пакетов и аппаратные исключения, такие как деление на ноль или точка останова - прерывания 0x00 и 0x03 соответственно
  • Как только ЦП прерывается, он перестает делать то, что делал, и реагирует на новое прерывание.
  • ЦП знает, как реагировать и какие процедуры ядра выполнять для вновь полученного прерывания, просматривая подпрограммы обслуживания прерываний (ISR), которые находятся в таблице дескрипторов прерываний (IDT)
  • IDT - это список записей дескриптора IDT размером 8 или 16 байт в зависимости от архитектуры.
  • Указатель на IDT хранится в регистре IDTR для каждого физического процессора или, другими словами, каждый процессор имеет свой собственный регистр IDTR, указывающий на свою собственную таблицу дескрипторов прерываний.
Смещения для разных снимков экрана и вывода windbg могут отличаться из-за того, что я несколько раз перезагружал отладочную программу за время, пока были сделаны эти заметки.
Примечания основаны на отладке ядра 64-битной Windows, работающего на виртуальной машине с 1 процессором.

Расположение IDT
Мы можем проверить, где находится таблица дескрипторов прерываний в ядре, прочитав регистр IDTR:
Bash:
r idtr

1.png


Как отмечалось ниже, команда !Idt позволяет нам вывести содержимое таблицы дескрипторов прерываний, а также подтверждает, что IDT находится по адресу fffff803`536dda00, как показано ниже:

2.png


Дампинг IDT
Мы можем сбросить IDT и увидеть адреса подпрограмм обслуживания прерываний для данного прерывания. Ниже приведен фрагмент таблицы дескрипторов прерываний:
Bash:
kd> !idt

Dumping IDT: fffff80091456000

00:    fffff8008f37e100 nt!KiDivideErrorFaultShadow
01:    fffff8008f37e180 nt!KiDebugTrapOrFaultShadow    Stack = 0xFFFFF8009145A9E0
02:    fffff8008f37e200 nt!KiNmiInterruptShadow    Stack = 0xFFFFF8009145A7E0
03:    fffff8008f37e280 nt!KiBreakpointTrapShadow
...
90:    fffff8008f37f680 i8042prt!I8042MouseInterruptService (KINTERRUPT ffffd4816353e8c0)
a0:    fffff8008f37f700 i8042prt!I8042KeyboardInterruptService (KINTERRUPT ffffd4816353ea00)
...

Ниже показан дамп IDT и выполнение кода ISR в действии:
  • Таблица IDT выгружается с помощью !Idt
  • Точка входа IRS для прерывания a0 расположена по адресу fffff8008f37f700
    • Это процедура, которая сначала выполняется внутри ядра, когда в ОС регистрируется событие клавиатуры, такое как нажатие клавиши.
    • В конце концов, подпрограмма i8042prt!I8042KeyboardInterruptService (внутри фактического драйвера клавиатуры) срабатывает, как только код на fffff8008f37f700 завершен
  • Установка точки останова на
    i8042prt!I8042KeyboardInterruptService
  • Как только точка останова установлена, в приглашении входа в ОС нажимается клавиша и достигается точка останова, подтверждая, что
    i8042prt!I8042KeyboardInterruptService действительно обрабатывает прерывания клавиатуры

assets%2F-LFEMnER3fywgFHoroYn%2F-Lv7T0maSgKNYOaLVofj%2F-Lv7vKNg9G9YZvVlfvP4%2Fkeyboard-interrupt.gif

*Опять не получилось подгрузить большую gif на форум

Ниже представлена сильно упрощенная диаграмма, иллюстрирующая все происходящие выше события:
  • Происходит прерывание клавиатуры 0xa0
  • Выполняется поиск таблицы IDT с индексом 0x0a (адрес IDT + 0xa0 * 0x10), точка входа ISR определяется, и код переходит к ней.
  • После некоторых обручей код в конечном итоге перенаправляется на драйвер клавиатуры, где прерывание обрабатывается в i8042prt!I8042KeyboardInterruptService

3.png


IDT Entry
IDT состоит из записей IDT _KIDTENTRY64, которые представляют собой структуру памяти ядра и определяются следующим образом:
Bash:
kd> dt nt!_KIDTENTRY64
   +0x000 OffsetLow        : Uint2B
   +0x002 Selector         : Uint2B
   +0x004 IstIndex         : Pos 0, 3 Bits
   +0x004 Reserved0        : Pos 3, 5 Bits
   +0x004 Type             : Pos 8, 5 Bits
   +0x004 Dpl              : Pos 13, 2 Bits
   +0x004 Present          : Pos 15, 1 Bit
   +0x006 OffsetMiddle     : Uint2B
   +0x008 OffsetHigh       : Uint4B
   +0x00c Reserved1        : Uint4B
   +0x000 Alignment        : Uint8B

Члены OffsetLow, OffsetMiddle и OffsetHigh со смещениями 0x000, 0x006 и 0x008 составляют виртуальный адрес в ядре, и это место, куда выполнение кода будет передано ЦП после того, как произойдет конкретное прерывание - другими словами - это служба прерывания Точка входа в процедуру (ISR).

Запись IDT для прерывания клавиатуры 0xa0

В качестве примера давайте проверим запись IDT на предмет прерывания клавиатуры, которая находится по индексу a0 в таблице IDT, как было обнаружено ранее:
Bash:
!idt a0

4.png


Ранее мы также узнали, что IDT находится по адресу fffff803536dd000:
Bash:
kd> r idtr
idtr=fffff803536dd000

Мы можем получить местоположение записи a0 IDT, добавив 0xa0 * 0x10 (индекс прерывания a0 умноженный на 0x10, поскольку размер записи дескриптора составляет 16 байт) к адресу таблицы IDT fffff803536dd000, что дает нам fffff803`536dda00:
Bash:
kd> dq idtr + (0xa0*0x10) L2
fffff803`536dda00  51568e00`0010e700 00000000`fffff803

С помощью приведенной выше информации мы можем наложить запись дескриптора прерывания a0 на _KIDTENTRY64 и проверить содержимое записи IDT a0:
Bash:
kd> dt _kidtentry64 (idtr + (0xa0*0x10))
ntdll!_KIDTENTRY64
   +0x000 OffsetLow        : 0xe700
   +0x002 Selector         : 0x10
   +0x004 IstIndex         : 0y000
   +0x004 Reserved0        : 0y00000 (0)
   +0x004 Type             : 0y01110 (0xe)
   +0x004 Dpl              : 0y00
   +0x004 Present          : 0y1
   +0x006 OffsetMiddle     : 0x5156
   +0x008 OffsetHigh       : 0xfffff803
   +0x00c Reserved1        : 0
   +0x000 Alignment        : 0x51568e00`0010e700

ISR для прерывания клавиатуры a0
Основываясь на приведенной выше записи IDT для прерывания клавиатуры, нижеследующее подтверждает, что комбинация смещения (высокий | средний | низкий) формирует виртуальный адрес точки входа процедуры обработки прерываний (ISR) - код, который будет выполняться, когда Прерывание a0 запускается с клавиатуры:

5.png


Ниже показаны инструкции по адресу fffff803`5156e700 (точка входа ISR), которые должны выполняться ЦП после срабатывания прерывания a0:
  • FFFFFFFFFFFFFFA0 будет помещен в стек
  • Произойдет переход на fffff803`5156ea40

6.png


... и, в конце концов, i8042prt!I8042KeyboardInterruptService будет задействован, и ниже это подтверждается - во-первых, точка останова достигнута для fffff803`5156e700 и
i8042prt!I8042KeyboardInterruptService сработает сразу после:

7.png


_KINTERRUPT
_KINTERRUPT - это структура памяти ядра, которая содержит информацию о прерывании. Ключевым членом этой структуры для этой лабораторной работы является член, расположенный по смещению 0x18 - это указатель на ServiceRoutine - подпрограмму (внутри связанного драйвера), которая фактически отвечает за обработку прерывания:
Bash:
dt nt!_KINTERRUPT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x008 InterruptListEntry : _LIST_ENTRY
   +0x018 ServiceRoutine   : Ptr64     unsigned char
   ...
   +0x0f8 Padding          : [8] UChar

В качестве примера - из ранее мы знаем, что ISR для прерываний клавиатуры находится по адресу ffffd4816353ea00, поэтому мы можем проверить структуру _KINTERRUPT нашего прерывания, наложив на нее содержимое памяти по адресу ffffd4816353ea00:
Bash:
dt nt!_KINTERRUPT ffffd4816353ea00

Это позволяет нам подтвердить, что ServiceRoutine снова правильно указывает на i8042prt!I8042KeyboardInterruptService внутри драйвера клавиатуры:

8.png


Поиск _KINTERRUPT
Чтобы вручную найти местоположение _KINTERRUPT для данного прерывания, нам необходимо использовать следующие ячейки и структуры памяти.
Область управления процессом или PCR (структура памяти _KPCR в ядре) хранит информацию о данном процессоре:
Bash:
kd> dt _KPCR
ntdll!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 GdtBase          : Ptr64 _KGDTENTRY64
   +0x008 TssBase          : Ptr64 _KTSS64
   +0x010 UserRsp          : Uint8B
   +0x018 Self             : Ptr64 _KPCR
   +0x020 CurrentPrcb      : Ptr64 _KPRCB
   +0x028 LockArray        : Ptr64 _KSPIN_LOCK_QUEUE
   +0x030 Used_Self        : Ptr64 Void
   +0x038 IdtBase          : Ptr64 _KIDTENTRY64
   +0x040 Unused           : [2] Uint8B
   +0x050 Irql             : UChar
   +0x051 SecondLevelCacheAssociativity : UChar
   +0x052 ObsoleteNumber   : UChar
   +0x053 Fill0            : UChar
   +0x054 Unused0          : [3] Uint4B
   +0x060 MajorVersion     : Uint2B
   +0x062 MinorVersion     : Uint2B
   +0x064 StallScaleFactor : Uint4B
   +0x068 Unused1          : [3] Ptr64 Void
   +0x080 KernelReserved   : [15] Uint4B
   +0x0bc SecondLevelCacheSize : Uint4B
   +0x0c0 HalReserved      : [16] Uint4B
   +0x100 Unused2          : Uint4B
   +0x108 KdVersionBlock   : Ptr64 Void
   +0x110 Unused3          : Ptr64 Void
   +0x118 PcrAlign1        : [24] Uint4B
   +0x180 Prcb             : _KPRCB

Местоположение _KPCR можно найти так:
Bash:
kd> ? @$pcr
Evaluate expression: -8781847822336 = fffff803`51148000

kd> !pcr
KPCR for Processor 0 at fffff80351148000:
    Major 1 Minor 1
    NtTib.ExceptionList: fffff803536dffb0
        NtTib.StackBase: fffff803536de000
       NtTib.StackLimit: 0000000000000000
...snip...

Внутри _KPCR по смещению 0x180 есть член, который указывает на структуру памяти блока управления процессом _KPRCB, которая содержит информацию о состоянии процессора.

Ключевым элементом, который нас интересует при попытке найти место в памяти _KINTERRUPT для данного прерывания, является InterruptObject, поскольку он содержит список указателей на список объектов _KINTERRUPT. InterrupObject расположен по смещению 0x2e80, как показано ниже:
Bash:
kd> dt _KPRCB
ntdll!_KPRCB
   +0x000 MxCsr            : Uint4B
   +0x004 LegacyNumber     : UChar
   +0x005 ReservedMustBeZero : UChar
   ....
   +0x2e80 InterruptObject  : [256] Ptr64 Void //256 pointers max as noted earlier
   ....

Обладая вышеуказанными знаниями, теперь мы можем найти расположение _KINTERRUPT для прерывания клавиатуры a0:
Bash:
dt @$pcr nt!_KPCR Prcb.InterruptObject[a0]

Ниже подтверждается, что _KINTERRUPT для прерывания a0, которое мы обнаружили вручную, совпадает с заданным командой! Idt:

10.png


Ссылки
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх