Статья Инжектирование в процесс через Ring 0 на C#

1.png

Привет. Никогда не писал ничего подобного, но тут почитал чат в Тележке и решил затронуть данную тему. Постараюсь уложиться кратенько, тема серьезная и огромная. Для тех кто ждет что копипаст сработает и все заведется - скажу сразу - пробуйте и может повезет. Итак поехали:

Введение​

Инжектирование кода в процесс через Ring 0 (kernel mode) позволяет получить привилегированный доступ к системным ресурсам и процессам. Этот метод часто используется для обхода стандартных механизмов безопасности операционной системы. В этой статье подробно рассмотрим, как реализовать инжектирование кода в процесс через Ring 0 на языке программирования C#.

Предупреждение​

Данный материал представлен исключительно в образовательных целях. Любое несанкционированное использование подобных методов может быть незаконным и неэтичным. Используйте эти знания ответственно и только в рамках законных исследований и тестирования.

Основные понятия​

Ring 0​

Ring 0 — это уровень привилегий процессора, в котором работает ядро операционной системы. Программы, выполняющиеся в Ring 0, имеют полный доступ к аппаратным ресурсам и могут выполнять любые инструкции без ограничений.

Инжектирование кода​

Инжектирование кода — это процесс внедрения стороннего кода в работающий процесс с целью выполнения этого кода в контексте данного процесса. Это может быть использовано для модификации поведения процесса, выполнения отладочных задач или обхода механизмов безопасности.

Подготовка окружения​

Для выполнения инжектирования через Ring 0 нам понадобятся следующие инструменты:

  1. Visual Studio — интегрированная среда разработки (IDE) для C#.
  2. Windows Driver Kit (WDK) — набор инструментов для разработки драйверов Windows.
  3. C# Runtime Compiler — для компиляции и выполнения кода на лету.

Шаги по реализации​

Шаг 1: Создание драйвера​

Первым шагом является создание драйвера, который будет работать в Ring 0. Это можно сделать с использованием WDK и языка программирования C++.

Шаг 1.1: Создание проекта драйвера​

  1. Установите WDK.
  2. Создайте новый проект драйвера в Visual Studio:
    • Откройте Visual Studio и выберите "Создать новый проект".
    • Выберите шаблон "Empty WDM Driver" или "Kernel Mode Driver, Windows".

Шаг 1.2: Написание кода драйвера​

Создайте основной файл драйвера, например Driver.c и добавьте следующий код:
C++:
#include <ntddk.h> // Подключаем заголовочный файл для разработки драйверов Windows (WDK).

// Точка входа драйвера
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    // Эти параметры не используются в этой функции, поэтому макрос UNREFERENCED_PARAMETER предотвращает предупреждения компилятора.
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
   
    // Выводим сообщение в отладочный вывод, чтобы указать, что драйвер был загружен.
    DbgPrint("Driver Loaded\n");
   
    // Возвращаем успешный статус, чтобы указать, что драйвер был успешно инициализирован.
    return STATUS_SUCCESS;
}

// Функция выгрузки драйвера
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    // Этот параметр не используется в этой функции, поэтому макрос UNREFERENCED_PARAMETER предотвращает предупреждения компилятора.
    UNREFERENCED_PARAMETER(DriverObject);
   
    // Выводим сообщение в отладочный вывод, чтобы указать, что драйвер был выгружен.
    DbgPrint("Driver Unloaded\n");
}

// Экспортируемая точка входа драйвера
extern "C" NTSTATUS NTAPI DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    // Устанавливаем функцию выгрузки драйвера, чтобы ОС знала, какую функцию вызывать при выгрузке драйвера.
    DriverObject->DriverUnload = DriverUnload;
   
    // Возвращаем успешный статус, чтобы указать, что драйвер был успешно инициализирован.
    return STATUS_SUCCESS;
}
По шагам что тут происходит:
  • #include <ntddk.h>:
    • Подключаем заголовочный файл для разработки драйверов Windows. Этот заголовок содержит все необходимые определения и объявления для написания драйверов.
  • NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath):
    • Определяем функцию DriverEntry, которая является точкой входа драйвера. Это функция, которая вызывается при загрузке драйвера.
    • PDRIVER_OBJECT DriverObject: указатель на объект драйвера, который предоставляет функции и данные, относящиеся к драйверу.
    • PUNICODE_STRING RegistryPath: указатель на строку, содержащую путь к разделу реестра, который хранит параметры драйвера.
  • UNREFERENCED_PARAMETER(DriverObject); и UNREFERENCED_PARAMETER(RegistryPath);:
    • Макрос UNREFERENCED_PARAMETER предотвращает предупреждения компилятора о неиспользуемых параметрах. Эти параметры не используются в данной функции, поэтому мы помечаем их как неиспользуемые.
  • DbgPrint("Driver Loaded\n");:
    • Функция DbgPrint выводит отладочное сообщение в системный отладочный вывод. Здесь мы указываем, что драйвер был загружен.
  • return STATUS_SUCCESS;:
    • Возвращаем значение STATUS_SUCCESS, чтобы указать, что драйвер был успешно загружен и инициализирован.
  • VOID DriverUnload(PDRIVER_OBJECT DriverObject):
    • Определяем функцию DriverUnload, которая вызывается при выгрузке драйвера.
    • PDRIVER_OBJECT DriverObject: указатель на объект драйвера. Этот параметр не используется в данной функции, поэтому он помечается как неиспользуемый.
  • DbgPrint("Driver Unloaded\n");:
    • Выводим отладочное сообщение, чтобы указать, что драйвер был выгружен.
  • extern "C" NTSTATUS NTAPI DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath):
    • Экспортируемая точка входа драйвера. Используем extern "C", чтобы предотвратить искажение имен (name mangling) при компиляции.
    • NTAPI определяет соглашение о вызове функции, используемое в Windows.
  • DriverObject->DriverUnload = DriverUnload;:
    • Устанавливаем функцию выгрузки драйвера. Это необходимо, чтобы операционная система знала, какую функцию вызывать при выгрузке драйвера.
  • return STATUS_SUCCESS;:
    • Возвращаем значение STATUS_SUCCESS, чтобы указать, что драйвер был успешно инициализирован.
Итак наш драйвер готов, но нам придется к нему еще вернуться дабы дописать

Шаг 2: Подписка и установка драйвера​

Для установки драйвера его нужно подписать, так как Windows требуют подписанные драйверы. Это можно сделать с использованием self-signed сертификата.

Шаг 2.1: Создание self-signed сертификата​

  1. Откройте PowerShell от имени администратора.
  2. Создайте сертификат с помощью команды:

Bash:
New-SelfSignedCertificate -Type CodeSigning -Subject "CN=TestDriver" -CertStoreLocation "Cert:\LocalMachine\My"

Экспортируйте сертификат в PFX файл:

Bash:
Export-PfxCertificate -Cert "Cert:\LocalMachine\My\<серийный номер сертификата>" -FilePath "C:\Path\To\Your\Certificate.pfx" -Password (ConvertTo-SecureString -String "YourPassword" -Force -AsPlainText)

Шаг 2.2: Подписание драйвера​

  1. Установите signtool.exe из Windows SDK.
  2. Подпишите драйвер с помощью команды:

Bash:
signtool sign /f "C:\Path\To\Your\Certificate.pfx" /p YourPassword /d "Your Driver" /v "C:\Path\To\Your\Driver.sys"

Как использовать signtool где брать и прочее описывать не буду - поиск на форуме работает)

Шаг 3: Создание C# приложения​

Теперь создадим приложение на C#, которое будет взаимодействовать с нашим драйвером. Мы будем использовать P/Invoke для вызова функций из драйвера.

P/Invoke (Platform Invocation Services) — это механизм в .NET, позволяющий вызывать функции из неуправляемых библиотек, таких как динамические библиотеки (DLL), написанные на языках C или C++. Это мощный инструмент, который позволяет разработчикам C# использовать существующий код, написанный на других языках, и взаимодействовать с низкоуровневыми системными API.

Основные принципы P/Invoke​

Чтобы использовать P/Invoke, необходимо:
  1. Определить сигнатуру функции в C#.
  2. Импортировать функцию из неуправляемой библиотеки.
  3. Вызвать функцию в коде C#.

Шаг 3.1: Создание проекта C#​

  1. Откройте Visual Studio и создайте новый проект консольного приложения на C#.
  2. Добавьте следующий код в Program.cs:
C#:
using System;
using System.Runtime.InteropServices;

class Program
{
    // Импорт функции CreateFile из библиотеки kernel32.dll для открытия связи с драйвером
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr CreateFile(
        string lpFileName,        // Имя файла или устройства
        uint dwDesiredAccess,     // Тип доступа к файлу или устройству (например, чтение или запись)
        uint dwShareMode,         // Тип совместного доступа (например, возможность совместного чтения)
        IntPtr lpSecurityAttributes, // Указатель на структуру SECURITY_ATTRIBUTES, определяющую безопасность объекта
        uint dwCreationDisposition, // Действие, которое нужно выполнить, если файл или устройство существует или не существует
        uint dwFlagsAndAttributes,  // Атрибуты и флаги файла или устройства
        IntPtr hTemplateFile);      // Объект файла, шаблон для создания нового файла

    // Импорт функции DeviceIoControl из библиотеки kernel32.dll для отправки команд драйверу
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool DeviceIoControl(
        IntPtr hDevice,          // Дескриптор устройства
        uint dwIoControlCode,    // Управляющий код ввода-вывода
        IntPtr lpInBuffer,       // Указатель на входной буфер
        uint nInBufferSize,      // Размер входного буфера
        IntPtr lpOutBuffer,      // Указатель на выходной буфер
        uint nOutBufferSize,     // Размер выходного буфера
        ref uint lpBytesReturned,// Указатель на переменную, которая получает размер возвращаемых данных
        IntPtr lpOverlapped);    // Указатель на структуру OVERLAPPED для асинхронных операций

    static void Main(string[] args)
    {
        // Открытие связи с драйвером с помощью функции CreateFile
        IntPtr hDevice = CreateFile(
            "\\\\.\\MyDriver", // Имя устройства
            0xC0000000,        // Тип доступа (чтение и запись)
            0,                 // Совместный доступ (0 - без совместного доступа)
            IntPtr.Zero,       // Атрибуты безопасности (по умолчанию)
            3,                 // Действие открытия (3 - открыть существующий файл)
            0,                 // Атрибуты файла (0 - без дополнительных атрибутов)
            IntPtr.Zero);      // Шаблонный файл (не используется)

        // Проверка успешного открытия устройства
        if (hDevice.ToInt32() != -1)
        {
            Console.WriteLine("Driver loaded successfully."); // Успешная загрузка драйвера
        }
        else
        {
            Console.WriteLine("Failed to load driver."); // Ошибка загрузки драйвера
            return;
        }

        // Переменная для хранения количества возвращаемых байтов
        uint bytesReturned = 0;

        // Отправка команды драйверу с помощью функции DeviceIoControl
        if (DeviceIoControl(
            hDevice,           // Дескриптор устройства
            0x222000,          // Управляющий код ввода-вывода
            IntPtr.Zero,       // Входной буфер (не используется)
            0,                 // Размер входного буфера (0 - отсутствует)
            IntPtr.Zero,       // Выходной буфер (не используется)
            0,                 // Размер выходного буфера (0 - отсутствует)
            ref bytesReturned, // Количество возвращаемых байтов
            IntPtr.Zero))      // Структура OVERLAPPED (не используется)
        {
            Console.WriteLine("DeviceIoControl succeeded."); // Успешное выполнение команды
        }
        else
        {
            Console.WriteLine("DeviceIoControl failed."); // Ошибка выполнения команды
        }
    }
}
мы создали приложение на шарпах которое использует наш драйвер - осталось самое интересное))

Шаг 4: Инжектирование кода​

Создадим механизм инжектирования, который будет передавать код в целевой процесс через Ring 0. Один из способов — использование Asynchronous Procedure Call (APC) или Direct Kernel Object Manipulation (DKOM).

Шаг 4.1: Использование APC для инжектирования​

Инжектирование кода через APC​

Этот код показывает, как создать и инициализировать APC, который выполнит указанную функцию (MyInjectedFunction) в контексте целевого потока. Код предназначен для добавления в наш драйвер, чтобы продемонстрировать инжектирование кода через APC.

В драйвере добавьте функцию для выполнения APC в контексте целевого процесса.
Перепишем наш дравер вот так:

C++:
#include <ntddk.h>

// Прототипы функций
VOID ApcInject(
    PKAPC Apc,
    PKNORMAL_ROUTINE* NormalRoutine,
    PVOID* NormalContext,
    PVOID* SystemArgument1,
    PVOID* SystemArgument2);

VOID QueueApc(PKTHREAD Thread);

// Точка входа драйвера
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    DbgPrint("Driver Loaded\n");

    // Установка функции выгрузки драйвера
    DriverObject->DriverUnload = DriverUnload;

    // Пример вызова инжектирования в контексте текущего потока
    PKTHREAD CurrentThread = KeGetCurrentThread();
    QueueApc(CurrentThread);

    return STATUS_SUCCESS;
}

// Функция выгрузки драйвера
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    DbgPrint("Driver Unloaded\n");
}

// Реализация функции APC
VOID ApcInject(
    PKAPC Apc,
    PKNORMAL_ROUTINE* NormalRoutine,
    PVOID* NormalContext,
    PVOID* SystemArgument1,
    PVOID* SystemArgument2)
{
    UNREFERENCED_PARAMETER(Apc);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);

    // Указываем функцию, которая будет выполнена
    if (NormalRoutine)
    {
        *NormalRoutine = (PKNORMAL_ROUTINE)MyInjectedFunction; // Указатель на вашу функцию
    }
}

// Реализация функции инжектирования
VOID QueueApc(PKTHREAD Thread)
{
    // Создаем и инициализируем объект APC
    PKAPC Apc = (PKAPC)ExAllocatePool(NonPagedPool, sizeof(KAPC));
    if (Apc)
    {
        KeInitializeApc(Apc, Thread, OriginalApcEnvironment, ApcInject, NULL, NULL, KernelMode, NULL);

        // Вставляем APC в очередь APC целевого потока
        if (!KeInsertQueueApc(Apc, NULL, NULL, 0))
        {
            // Если вставка в очередь не удалась, освобождаем память
            ExFreePool(Apc);
        }
    }
}

// Пример инжектируемой функции
VOID MyInjectedFunction()
{
    DbgPrint("Injected Function Executed\n");
}

Не так уж много кода и получилось)
Осталось разобраться как куда и на какую кнопку нажать) Но на самом деле все не сложно:

Для завершения и тестирования написанного драйвера и C# приложения, следуйте приведенным ниже шагам. Это поможет вам убедиться в правильности работы кода и его безопасности.

Шаг 5: Завершение и тестирование​

5.1. Компиляция и сборка драйвера​

  1. Установка Windows Driver Kit (WDK) и Visual Studio:
    • Убедитесь, что у вас установлены последние версии Visual Studio и Windows Driver Kit (WDK).
  2. Создание проекта драйвера:
    • Откройте Visual Studio.
    • Выберите File > New > Project.
    • В окне создания проекта выберите Empty WDM Driver или Kernel Mode Driver, Windows.
    • Назовите проект и укажите путь для его сохранения.
  3. Добавление кода драйвера:
    • В созданном проекте добавьте новый файл Driver.c.
    • Вставьте в него весь код драйвера, приведенный ранее.
  4. Компиляция драйвера:
    • В Visual Studio выберите Build > Build Solution.
    • Убедитесь, что драйвер скомпилирован без ошибок. Скомпилированный файл драйвера (с расширением .sys) будет находиться в папке x64\Debug или x64\Release в каталоге проекта, в зависимости от настроек сборки.
Как подписать драйвер написал выше

5. Установка и запуск драйвера​

  1. Запуск тестовой системы:
    • Для тестирования драйвера рекомендуется использовать виртуальную машину (VM) или изолированную систему. Например, можно использовать Hyper-V или VMware для создания виртуальной машины с Windows.
  2. Настройка тестовой системы для загрузки неподписанных драйверов:
    • Включите режим тестовой подписи:
    • Bash:
      bcdedit /set testsigning on
    • Перезагрузите систему.
  3. Установка драйвера:
    • Скопируйте скомпилированный и подписанный файл драйвера (.sys) на тестовую систему.
    • Откройте Command Prompt от имени администратора.
    • Установите драйвер с помощью sc.exe:
    • Bash:
      sc create MyDriver type= kernel start= demand binPath= "C:\Path\To\Your\Driver.sys"
      sc start MyDriver
  4. Проверка установки драйвера:
    • Убедитесь, что драйвер успешно установлен и запущен, проверив сообщение в отладочном выводе (например, с помощью DbgView).

5.4. Компиляция и запуск C# приложения​

  1. Создание проекта C#:
    • Откройте Visual Studio.
    • Выберите File > New > Project.
    • Выберите шаблон Console App (.NET Framework) и создайте проект.
  2. Добавление кода C#:
    • Вставьте приведенный ранее код C# приложения в Program.cs.
  3. Компиляция и запуск приложения:
    • Выберите Build > Build Solution.
    • Запустите приложение, нажав кнопку Start или клавишу F5.
  4. Проверка работы приложения:
    • Убедитесь, что приложение успешно подключается к драйверу и отправляет команду. Проверьте отладочный вывод для подтверждения выполнения инжектированной функции.

5.5. Отключение режима тестовой подписи​

После завершения тестирования отключите режим тестовой подписи:
Bash:
bcdedit /set testsigning off

Заключение​

Инжектирование кода через Ring 0 — это сложная и потенциально опасная техника, требующая глубокого понимания архитектуры операционной системы и принципов работы драйверов. Надеюсь, данный материал помог вам понять основные шаги и принципы реализации инжектирования через Ring 0 на C#. Помните о важности этического использования подобных знаний и всегда действуйте в рамках закона.
 
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!