Aight, fam. I'm going to start getting into some deep and dirty topics, so if things start to go over your head, don't freak out. Use this as a basis to jump to resources that will guide you in your cyber journeys. Note that this is just basic kernel development. I'm not going into hooking communications, root kits, or code injections, so cool it there, hot shot. We're just going to describe some theory, get setup for development, and pull basic system information with a test driver. Also note, you should have a familiarity with Windows Internals. If you need a class on Windows Internals hit me up because I made a sweet course for it. Brian, how do we contact you? Oops, sounds like you need to figure it out if we aren't already connected.

So what is the kernel?

We can think of the kernel as the link between software and hardware. It allows for control and dispatching of system calls made by the software to the hardware. A lot of this control relates to thread scheduling, interrupts, exception dispatching, and the implementation of mutexes and semaphores. The kernel acts a mini operating system (OS) underneath what the user sees (Windows, Mac, Linux, etc.). Why can't software or users directly interact with the hardware? Well it's just too easy to mess up a call to the hardware and brick the machine or cause a catastrophic malfunction. No seriously, if an improper command is sent to the hardware, it's extremely possible to render the entire machine useless afterwards. The kernel executes this link by executing device drivers, which are much like a program only that it runs in kernel mode and is a loadable kernel module.

Kernel mode vs User mode

"A processor in a computer running Windows has two different modes: user mode and kernel mode. The processor switches between the two modes depending on what type of code is running on the processor. Applications run in user mode, and core operating system components run in kernel mode.[1]"

I love that shallow HAL[1]

Key differences are found with the virtual address spaces. In user-mode processes, a virtual address space is private and dedicated solely to one process. This prevents applications from having the ability to access another processes memory and making unnecessary modifications as well as separating them from operating system. Kernel mode applications, on the other hand, share a single address space, which is where we can easily brick our machine. Since memory down here is shared, it allows for the possibility of overwriting to the OS's or another application's address space.

Before we go any further, we need to go deeper into virtual memory. The range of an address space starts at zero (first 64 KB reserved) and runs to the max which is determined by whether or not the OS is 32-bit or 64 bit (for Windows anyway). In 32-bit processes on 32-bit Windows, the max runs to 2 GB. This can go up to 3 GB if a flag is set in the header. In 64-bit processes on 64-bit Windows, for Windows 8 and before, max is 8 TB. For Windows 8.1 and newer, max is 128 TB. For 32-bit processes on 64-bit Windows, the max is 4 GBs if a flag is set, otherwise it's 2 GB. This whole paragraph was extracted from [2] Pavel Yosifovich's book, "Windows Kernel Programming." Great book for starters and where a lot of the information in this article will be derived from.

Let's take a step back and look into some Computer Science (CS) theory. In operating systems, we typically follow a classic theoretical protection ring that assists in protecting data within an OS. It looks like the image below.

Hertzsprung/Wikipedia

As shown, we can see where privilege lies for each different class. Why do we have two device drivers? Check the image from MSDN above (in case you missed it) to see that there can be device drivers that run in user mode as well as kernel mode. That's fine and dandy for your average introductory, college level operating systems class, but we need to take this a step further.

Why does it matter to programmers, cyber defenders, and attackers? Spoken in true Army fashion, it is the "key-terrain" of an operating system. Since it has the most privilege, it allows all three of those roles to complete tasks like detecting when certain events occur or intercepting operations that the OS is conducting then make modifications. It's where we run rootkits, bootkits, and other serious tools to as an attacker for nefarious reasons. Attackers remain fairly close to undetectable as we're operating under the OS. Not totally hidden, but you can certainly do a good bit of damage at kernel level while having minimal logging.

Now lets set up a test environment. Easy, make a Windows 10 virtual machine (VM). One option is remote deployment/debugging of kernel modules on remote machines with visual studio 2017 or 2019. If you'd rather try it on a live machine, I'd recommend waiting until you know your driver is functional. A VM is low-risk and allows us to brick it by accident without needing to remove a CMOS or something. For remote deployment, check MSDN here. For those in the back that like it a bit saucy on 64-bit, you can start by opening a privileged terminal and entering "bcdedit /set testsigning on".  This'll allow drivers signed with test by visual studio to be run. You can choose to sign with production as well, but we're not ready for that yet. This is probably a good time to talk about driver signing since it's rather important for deployment of real drivers. As kernel drivers have such powerful abilities that could cause Blue Screen Of Death (BSOD) or alter system stability, Microsoft requires drivers to be signed with certificates from a certificate authority (CA). Any drivers that are unsigned are told, "You are the weakest link. Goodbye." in a shrill voice like no other. These certificates only guarantee that the driver hasn't been modified since leaving the driver's publisher, not whether it works or not[2]. There are unique ways to get around this, but, once again, we're chilling today on the basics.

With a Windows 10 target machine, we need to add a couple keys to the registry in order to get our output to generate. Using RegEdit, add a key named Debug Print Filter under the HKLM\SYSTEM\CurrentControlSet\Control\Session Manager. In that key, add a DWORD value named DEFAULT and set it to 8. Then restart the machine to load everything[2]. (SIDENOTE: this is coming straight from Pavel Yosifovich's book, "Windows Kernel Programming"). If you haven't installed, or created a symlink, to the sysinternal suite, you need to do that at this point. We need to use DbgView later. If you don't know what the Sysinternals suite is, then you need to go back to windows internals.

Sweet, we're pretty much good to go, so let's head back over to our development machine. In Visual studio create an empty WDM Driver and name it what ever you'd like and add a cpp file to it. Most of Windows programming is primarily done in C++. As usual in all low level topics, step 1 learn C. C++ is pretty quick to pickup though if you haven't had much experience with it.  Follow along with the code below:

#include <ntddk.h>

//Typically used to free up space previously allocated to variables in 
//other functions 
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {

	UNREFERENCED_PARAMETER(DriverObject);

	KdPrint(("Sample driver Unload called\n"));
}

//really just prints the data stored in osVersion Info
void getMachineInfo(OSVERSIONINFOEXW osVersionInfo) {

	KdPrint(("Printing major, minor, Build Number, and Product type"));
	KdPrint(("Major version: %u\n", osVersionInfo.dwMajorVersion));
	KdPrint(("Minor version: %u\n", osVersionInfo.dwMinorVersion));
	KdPrint(("Build Number: %u\n", osVersionInfo.dwBuildNumber));
	KdPrint(("Product type: %u\n", osVersionInfo.wProductType));

}

extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {

	//https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexw
	OSVERSIONINFOEXW osVersionInfo;

	//status is the status we're returning since return type is NTSTATUS
	NTSTATUS status = STATUS_SUCCESS;

	//Suppresses warnings by removing arguments name
	UNREFERENCED_PARAMETER(RegistryPath);

	DriverObject->DriverUnload = SampleUnload;

	osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEXW);
	//BAM RtlGetVersion get's system info and stores it into osVersionInfo (must typecast to POSVERSIONINFOW)
    	status = RtlGetVersion((POSVERSIONINFOW)&osVersionInfo);

	//Test that execution of RtlGetVersion worked
	NT_ASSERT(NT_SUCCESS(status));

	getMachineInfo(osVersionInfo);

	KdPrint(("Sample driver initialized successfully\n"));

	return STATUS_SUCCESS;
}

Before we finish it, right click your projects name, select properties, and change your platform (at the top) to All Platforms. Looks good, lets build it and ship it! To get it on to the target machine, I just threw up a Python webserver and pulled it down from the target machine. Other options include shared clipboard (if local VM), SCP, share drive, etc. We just want to send SuperSweetDriver.sys over to the target. Use the .sys found in the project's X64 folder since we know our target is x64.

Once on the target machine, open up dbgview from the sysinternals suite with admin privileges and check capture kernel, then uncheck Capture Win32. When you're good to go, enter the same commands as below.

Running driver

Once it starts running, let's check DbgView.

Everything Logged

Nice so this tells us that Windows 10 with build 18363 is being used. 18363 is was released in either May or November of 2019, so this is an older machine that may be vulnerable to an older exploit. The product type indicates it's a desktop OS. Some decent information from a quick pull that wouldn't usually be logged. Congrats on your first kernel driver! Now run "sc stop supersweetdriver" before you forget to unload the driver.

Once again, this was pretty much all pulled from Pavel Yosifovich's "Windows Kernel Programming." I highly recommend it as a nice kick-start to the basics of kernel development.

Pretty quick article this time. I didn't touch on messing with threads, hooking, remotely debugging, injections, networking, the list goes on as things start to get complicated really fast. This is the "hello world" for kernel debugging to hopefully show people it's not all that bad (kind of). Kernel development is a lot less forgiving than traditional user-mode application development. Your mistakes have a bit more weight, which may cost significant amounts of time if things go wrong. However, the reward is quite fruitful. Kernel development can serve as a step towards being able to build rootkits or really solid defensive tools that track events happening below Windows. Both of which are so fetch right now. Stay golden, pony boy.

[1] https://docs.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode#:~:text=A%20processor%20in%20a%20computer,components%20run%20in%20kernel%20mode.

[2] Windows kernel programming, Pavel Yosifovich - https://www.amazon.com/Windows-Kernel-Programming-Pavel-Yosifovich-ebook/dp/B07TJT1GTF