DEV Community

Igor Proskurin
Igor Proskurin

Posted on

Using GNU toolchain for Windows kernel-mode drivers

For a long time, I was curious about using GNU toolchain on Windows platforms, especially when it boils down to kernel-mode driver development. I like GNU toolchain (binutils, gcc, libstdc++). I use it for embedded development, but compiling, and especially linking binaries with GNU ld linker, has always been tricky. Why is it important? Microsoft Visual Studio is an excellent tool, but it is kind of obscure what is happening behind the scenes. Having free and open-source software toolchain even on a proprietary platform is a good thing (and avoid all the bloat from Visual Studio).

This time, it took me a while to figure out how to link a trivial kernel-mode driver, and this brief post is to document these findings.

Compiling GCC from sources for the mingw-w64 target is a topic for a different post (binutils is easy, though). So for these experiments I just used MSYS2 with UCRT64 environment. This painlessly bring you startup libraries (CRT), C standard library (mingw-w64), and compiler support libraries (libgcc and friends) together with libstdc++ if you use C++.

Running kernel-mode drivers on Windows requires a bunch of tricks (disabling secure boot, enabling test signing, and installing sysinternals). If you are not familiar with it, read a few introductory chapters from Windows Kernel Programming, or message me in the comments, and I will update this post with some details.

Driver Hello World

This is a typical "Hello World!" example for Windows kernel-mode drivers:

#include <ntddk.h>

static void test_driver_unload(PDRIVER_OBJECT driverObject);

NTSTATUS 
DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath)
{
    UNREFERENCED_PARAMETER(registryPath);
    DbgPrint("Sample driver initialized successfully\n");

    driverObject->DriverUnload = test_driver_unload;

    return STATUS_SUCCESS;
}

void
test_driver_unload(PDRIVER_OBJECT driverObject)
{
    UNREFERENCED_PARAMETER(driverObject);
    DbgPrint("Driver unload called\n");
}
Enter fullscreen mode Exit fullscreen mode

For tracing purposes, DbgPrint is provided by ntoskrnl.exe. DriverEntry is our entry point to the driver that is called by the kernel. test_driver_unload is our custom callback that should be called when the module is unloaded.

Using GNU ld for linking

Compiling into object code is easy, but linking is tricky. For reference of possible options, I used an example from ReactOS project. ReactOS is the only working (sort of) full reverse-engineered NT operating system kernel. It is a cool project. ReactOS GitHub repo has a collection of build options for kernel-mode drivers that can be carved out of CMake files.

Unfortunately, with options from ReactOs, I could not make my example fully working 64-bit Windows 11 -- driver loaded but refused to unload. The next repo that helped me with a list of options was Mingw64 Driver Plus Plus.

Finally, this combination of options provided a loadable and unloadable image:

gcc -std=gnu99 -Wall -Wextra -pedantic -shared -fPIC\
   -O0 -municode -nostartfiles -nostdlib -nodefaultlibs\
   -I/ucrt64/include/ddk -Wl,-subsystem,native \
   -Wl,--exclude-all-symbols,-file-alignment=0x200,-section-alignment=0x1000 \
   -Wl,-entry,DriverEntry -Wl,-image-base,0x140000000\
   -Wl,--dynamicbase -Wl,--nxcompat -Wl,--gc-sections\
   -Wl,--stack,0x100000\
   -o test_driver.sys test_driver.c -lntoskrnl
Enter fullscreen mode Exit fullscreen mode

First time, I was also using -Wl,--wdmdriver options, but it was not helping. This mess can be organized in a Makefile:

TARGET = test_driver
OPT = -O0
CSTANDARD = -std=gnu99
DDK_INCLUDE_PATH = /ucrt64/include/ddk

CC = gcc
LD = ld
RM = rm -f
STRIP = strip
OBJDUMP = objdump -x

CFLAGS += -Wall
CFLAGS += -Wextra
CFLAGS += -pedantic
CFLAGS += -municode
CFLAGS += $(CTANDARD)
CFLAGS += $(OPT)

# Do not link startup, compiler support
# or C standard libraries.
CFLAGS += -nostartfiles
CFLAGS += -nostdlib
CFLAGS += -nodefaultlibs

CFLAGS += -fPIC
CFLAGS += -shared

# Include path to ntddk.h or wdm.h.
CFLAGS += -I$(DDK_INCLUDE_PATH)

LDFLAGS  = --exclude-all-symbols
LDFLAGS += --gc-sections
LDFLAGS += --dynamicbase
LDFLAGS += --nxcompat

LDFLAGS += -subsystem=native
LDFLAGS += -file-alignment=0x200
LDFLAGS += -section-alignment=0x1000
LDFLAGS += -image-base=0x140000000
LDFLAGS += --stack=0x100000

LDFLAGS +=-entry=DriverEntry

SRC = $(TARGET).c

OBJ = $(SRC:%.c=%.o)

LIBS  = -lntoskrnl
LIBS += -lhal

all: sys strip dump

dump: strip
    $(OBJDUMP) $(TARGET).sys

strip: sys
    $(STRIP) $(TARGET).sys

sys: $(TARGET).sys

.SECONDARY: $(TARGET).sys
.PRECIOUS: $(OBJ)

$(TARGET).sys: $(OBJ) 
    $(LD) $(LDFLAGS) -o $@ $(OBJ) $(LIBS)

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    $(RM) $(TARGET).sys
    $(RM) $(SRC:%.c=%.o)

.PHONY: all sys clean strip dump
Enter fullscreen mode Exit fullscreen mode

This Makefile can be found in my GitHub repo here.

Loading and unloading

On Windows 11, the driver also requires signing (using a Visual Studio tool). This is easy:

PS C:\Users\Igor> signtool sign /v /fd sha256 /n WDKTestCert test_driver.sys
The following certificate was selected:
    Issued to: WDKTestCert Igor,133660689149334675
    Issued by: WDKTestCert Igor,133660689149334675
    Expires:   Thu Jul 20 16:00:00 2034
    SHA1 hash: E76CFF2C68E75A85631906D4BC2F55A6D7B32597

Done Adding Additional Store
Successfully signed: test_driver.sys

Number of files successfully Signed: 1
Number of warnings: 0
Number of errors: 0
Enter fullscreen mode Exit fullscreen mode

After that, we can use Service Control utility (it is also easy to write a custom loader based on Service Manager API):

sc create test_driver type= kernel binPath= C:\Users\Igor\test_driver.sys
Enter fullscreen mode Exit fullscreen mode

Which creates a record in \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\test_driver. After that, the driver can be started and stopped (and deleted if necessary):

PS C:\Users\Igor> sc start test_driver

SERVICE_NAME: test_driver
        TYPE               : 1  KERNEL_DRIVER
        STATE              : 4  RUNNING
                                (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x0
        PID                : 0
        FLAGS              :

PS C:\Users\Igor> sc stop test_driver

SERVICE_NAME: test_driver
        TYPE               : 1  KERNEL_DRIVER
        STATE              : 1  STOPPED
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x0
Enter fullscreen mode Exit fullscreen mode

The debug output (has to be enabled in the registry) shows tracing messages.

Summary

It is possible to link a simple Windows kernel-mode driver using GNU toolchain (mingw32-w64). For reference, source code for this example can be found in my GitHub repo here.

Top comments (0)