Injecting into a process via Ring 0 in C#

Tr0jan_Horse

Moderator
Staff member
MODERATOR
ULTIMATE
PREMIUM
MEMBER
Joined
Oct 23, 2024
Messages
304
Reaction score
8,789
Deposit
0$
1750635058547.png
Hi. I've never written anything like this, but I read the chat in Telezhka and decided to touch on this topic. I'll try to keep it short, the topic is serious and huge. For those who are waiting for copy-paste to work and everything to start - I'll say right away - try and maybe you'll get lucky. So here we go:

Introduction​

Injecting code into a process via Ring 0 (kernel mode) allows you to gain privileged access to system resources and processes. This method is often used to bypass standard operating system security mechanisms. In this article, we will take a detailed look at how to implement code injection into a process via Ring 0 in the C# programming language.

Warning​

This material is provided for educational purposes only. Any unauthorized use of such methods may be illegal and unethical. Use this knowledge responsibly and only within the framework of legitimate research and testing.

Basic concepts​

Ring 0​

Ring 0 is the processor privilege level in which the operating system kernel runs. Programs running in Ring 0 have full access to hardware resources and can execute any instructions without restrictions.

Code injection​

Code injection is the process of introducing third-party code into a running process with the intent of executing that code within the context of that process. This can be used to modify the behavior of a process, perform debugging tasks, or bypass security mechanisms.

Preparing the environment​

To perform injection through Ring 0 we will need the following tools:

  1. Visual Studio is an integrated development environment (IDE) for C#.
  2. Windows Driver Kit (WDK) is a set of tools for developing Windows drivers.
  3. C# Runtime Compiler - for compiling and executing code on the fly.

Implementation steps​

Step 1: Create a driver​

The first step is to create a driver that will work in Ring 0. This can be done using the WDK and the C++ programming language.

Step 1.1: Create a driver project​

  1. Install the WDK.
  2. Create a new driver project in Visual Studio:
    • Open Visual Studio and select "Create a new project".
    • Select the "Empty WDM Driver" or "Kernel Mode Driver, Windows" template.

Step 1.2: Writing the Driver Code​

Create a main driver file, such as Driver.c, and add the following code:

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;
}


Step by step what is happening here:
  • #include <ntddk.h>:
    • We include a header file for developing Windows drivers. This header contains all the necessary definitions and declarations for writing drivers.
  • NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath):
    • We define the DriverEntry function, which is the entry point of the driver. This is the function that is called when the driver is loaded.
    • PDRIVER_OBJECT DriverObject: A pointer to a driver object that provides driver-specific functions and data.
    • PUNICODE_STRING RegistryPath: Pointer to a string containing the path to the registry key that stores driver settings.
  • UNREFERENCED_PARAMETER(DriverObject); и UNREFERENCED_PARAMETER(RegistryPath);:
    • The UNREFERENCED_PARAMETER macro prevents compiler warnings about unused parameters. These parameters are not used in this function, so we mark them as unused.
  • DbgPrint("Driver Loaded\n");:
    • The DbgPrint function prints a debug message to the system debug output. Here we indicate that the driver has been loaded.
  • return STATUS_SUCCESS;:
    • Return STATUS_SUCCESS to indicate that the driver was successfully loaded and initialized.
  • VOID DriverUnload(PDRIVER_OBJECT DriverObject):
    • We define the DriverUnload function, which is called when the driver is unloaded.
    • PDRIVER_OBJECT DriverObject: A pointer to the driver object. This parameter is not used in this function, so it is marked as unused.
  • DbgPrint("Driver Unloaded\n");:
    • We print a debug message to indicate that the driver has been unloaded.
  • extern "C" NTSTATUS NTAPI DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath):
    • Exported entry point of the driver. We use extern "C" to prevent name mangling during compilation.
    • NTAPI defines the function calling convention used in Windows.
  • DriverObject->DriverUnload = DriverUnload;:
    • We set the driver unload function. This is necessary so that the operating system knows which function to call when unloading the driver.
  • return STATUS_SUCCESS;:
    • Return STATUS_SUCCESS to indicate that the driver was successfully initialized.
So our driver is ready, but we will have to come back to it to finish it.

Step 2: Subscribe and install the driver​

To install a driver, you need to sign it, as Windows requires signed drivers. This can be done using a self-signed certificate.

Step 2.1: Create a self-signed certificate​

  1. Open PowerShell as administrator.
  2. Create a certificate using the command:


    Code:
    New-SelfSignedCertificate -Type CodeSigning -Subject "CN=TestDriver" -CertStoreLocation "Cert:\LocalMachine\My"
Export the certificate to a PFX file:


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

Step 2.2: Signing the Driver​

  1. Install signtool.exe from the Windows SDK.
  2. Sign the driver using the command:

Bash:

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


I won't describe how to use signtool, where to get it, etc. - the forum search works)

Step 3: Create a C# Application​

Now let's create a C# application that will interact with our driver. We will use P/Invoke to call functions from the driver.

P/Invoke (Platform Invocation Services) is a mechanism in .NET that allows you to call functions from unmanaged libraries, such as dynamic-link libraries (DLLs) written in C or C++. It is a powerful tool that allows C# developers to use existing code written in other languages and interact with low-level system APIs.

Basic principles of P/Invoke​

To use P/Invoke, you need to:
  1. Define a function signature in C#.
  2. Import a function from an unmanaged library.
  3. Call a function in C# code.

Step 3.1: Create a C# Project​

  1. Open Visual Studio and create a new C# Console Application project.
  2. Add the following code to 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."); // Ошибка выполнения команды
}
}
}


we created an application on sharps that uses our driver - the most interesting part remains))

Step 4: Injecting the code​

Let's create an injection mechanism that will transmit code to the target process via Ring 0. One way is to use Asynchronous Procedure Call (APC) or Direct Kernel Object Manipulation (DKOM).

Step 4.1: Using APC for Injection​

Code injection via APC​

This code shows how to create and initialize an APC that will execute the specified function (MyInjectedFunction) in the context of the target thread. The code is intended to be added to our driver to demonstrate code injection via APC.

In the driver, add a function to execute APC in the context of the target process.
Let's rewrite our driver like this:


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");
}


It wasn't that much code)
It remains to figure out how, where and which button to press) But in fact, everything is not difficult:

To complete and test the driver and C# application you have written, follow the steps below. This will help you ensure that the code works correctly and is safe.

Step 5: Finishing and Testing​

5.1. Compiling and building the driver​

  1. Installing Windows Driver Kit (WDK) and Visual Studio :
    • Make sure you have the latest versions of Visual Studio and the Windows Driver Kit (WDK) installed.
  2. Creating a driver project :
    • Open Visual Studio.
    • Select File > New > Project.
    • In the Create Project window, select Empty WDM Driver or Kernel Mode Driver, Windows.
    • Name the project and specify the path to save it.
  3. Adding driver code :
    • In the created project, add a new file Driver.c.
    • Paste into it all the driver code given earlier.
  4. Compiling the driver :
    • In Visual Studio, select Build > Build Solution.
    • Make sure the driver compiles without errors. The compiled driver file (with the .sys extension) will be located in the x64\Debug or x64\Release folder in the project directory, depending on the build settings.
How to sign the driver is written above

5. Installing and running the driver​

  1. Starting the test system :
    • It is recommended to use a virtual machine (VM) or an isolated system to test the driver. For example, you can use Hyper-V or VMware to create a Windows virtual machine.
  2. Setting up a test system to load unsigned drivers :
    • Enable test signature mode:

    • Code:
      bcdedit /set testsigning on
    • Reboot the system.
  3. Installing the driver :
    • Copy the compiled and signed driver file (.sys) to the test system.
    • Open Command Prompt as administrator.
    • Install the driver using sc.exe:

    • Bash:
      sc create MyDriver type= kernel start= demand binPath= "C:\Path\To\Your\Driver.sys"
      sc start MyDriver
  4. Checking driver installation :
    • Verify that the driver has been successfully installed and started by checking the message in the debug output (for example, using DbgView).

5.4 Compiling and running a C# application​

  1. Creating a C# project :
    • Open Visual Studio.
    • Select File > New > Project.
    • Select the Console App (.NET Framework) template and create a project.
  2. Adding C# code :
    • Paste the C# application code provided earlier into Program.cs.
  3. Compiling and running the application :
    • Select Build > Build Solution.
    • Launch the application by clicking the Start button or pressing F5.
  4. Checking the application operation :
    • Make sure that the application successfully connects to the driver and sends the command. Check the debug output to confirm that the injected function is executed.

5.5. Disabling test signature mode​

Once testing is complete, disable test signature mode:


Bash:
bcdedit /set testsigning off

Conclusion​

Ring 0 injection is a complex and potentially dangerous technique that requires a deep understanding of the operating system architecture and how drivers work. Hopefully, this article has helped you understand the basic steps and principles of implementing Ring 0 injection in C#. Remember the importance of using such knowledge ethically and always act within the law.
 
Top Bottom