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 likeDbgPrint
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 normalchar*
to a function. In those cases you have to convert the string to achar*
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.