Home KernelSharp - Write Kernel Drivers in C#
Post
Cancel

KernelSharp - Write Kernel Drivers in C#

Introduction

In the past, people asked me if it was possible to write a kernel-mode Driver for Windows in C#.
Now, you might be thinking that something like this would be totally foolish and serve no purpose.
Let me tell you one thing: You are totally right about that.
But after getting asked this question multiple times, I have decided to make it possible.

.NET in Kernel-mode?

There are several reasons, why the .NET Runtime is not really able to run in kernel-mode, in fact, there are too many to list them all.
The biggest reason, besides its memory management, is the Just-in-time compiler. The runtime compiles the code from MSIL to native code while the program runs. There are a lot of ways in which this could go wrong in kernel mode. As an example, you would never be able to make an HVCI-compatible driver because the native code would have to be written to the heap.
However, this is where NativeAOT comes into play.

NativeAOT

NativeAOT is an experimental fork of the .NET Runtime. At the time of writing it is planned to be moved into mainline development in .NET 7. NativeAOT allows us to compile our C# programs to a native binary ahead of time.
It works by taking the intermediate code, transpiling it to native code, and outputting an object file, which can be linked afterward. This compiler is included in the Microsoft.DotNet.ILCompiler package. Follow the NativeAOT quick start guide if you want to create your own project.

Developing a Driver

If you want to follow along, feel free to clone the KernelSharp repository, because it basically is ready to compile already.

Since we will be going down into kernel mode, we cannot have .NET libraries with us. Simple classes like System.String already require dynamic memory allocation and other routines that are not available in kernel mode in the same way. This is why we have to strip all standard libraries with <NoStdLib>true</NoStdLib> in the .csproj file. In order to still have the basic datatypes with their basic functionality, we have to define them ourselves. Thankfully this part has been done by Michal Strehovsky for EFI boot applications already. I also recommend enabling unsafe code, because at some point we will be forced to work with pointers.

With all of that setup, we can write a simple driver.

1
2
3
4
static uint DriverEntry()
{
    return 0x0; //STATUS_SUCCESS
}

However, currently, there is no way to call any kernel-mode API. This part is crucial, otherwise, we would be pretty limited.
Normally we would use the DllImport attribute to import native functions from other modules into our C# code, however, that is not available here. We have to use the RuntimeImport attribute, which we once again have to implement it first since all standard libraries have been stripped. After implementing the MethodImpl attribute as well, we can now import functions from ntoskrnl.

1
2
3
[MethodImpl(MethodImplOptions.InternalCall)]
[RuntimeImport("ntoskrnl.exe", "ExAllocatePool")]
public static extern PVOID ExAllocatePool(PoolType poolType, ulong size);

The code above is from my libraries WDK class. I have imported a lot of useful ntoskrnl functions there and defined some datatypes like PVOID and PEPROCESS. I even implemented __readcr3 using a dirty workaround since the compiler does not support intrinsics yet.

1
2
3
4
5
6
7
public static ulong __readcr3()
{
    void* buffer = stackalloc byte[0x5C0];
    var sat = KeSaveStateForHibernate(buffer); // saves CPU context into buffer
    ulong cr3 = *(ulong*)((ulong)buffer + 0x10);
    return cr3;
}

Compiling the Driver

The build script in my repository basically only does 3 simple things:

  • Compile all C# classes to MSIL
  • Compile MSIL binary to a native object file using the experimental compiler
  • Link the object file together with ntoskrnl.lib to a .sys file

Limitations

While creating this project, I have come across a few limitations.

  • No support for variadic arguments
    By default, C# already only supports passing variadic arguments to native functions using the undocumented __arglist. In this case, it seems not to be possible at all, which can make calling functions like DbgPrint a bit annoying, since you can only pass a fixed amount of arguments.
  • Strings always get stored as Wide-strings by the compiler
    This is annoying when you need to pass a normal char* to a function. In those cases you have to convert the string to a char* first. However, Michal Strehovsky pointed out that there is a String Literal Generator for UTF-8 strings on Twitter DMs.
  • Global strings misplaced
    I am not entirely sure if this was not an edge case, but when referencing a globally defined string in my code the driver would bugcheck, because the compiler somehow did not place the string at the correct place in memory. I haven’t debugged this any further though and it works when writing the strings inside of the method bodies.

Conclusion

This is a very experimental project and I don’t recommend doing this. The only purpose of this project is to prove that kernel drivers are possible in C# and that C# native compilation is around the corner.
If you want to see an example of this library being used, check out my KernelBypassSharp repository.

This post is licensed under CC BY 4.0 by the author.