Driver - Fundamentals of kernel programming

Write in front

  this series is written word by word, including examples and experimental screenshots. Due to the complexity of the system kernel, there may be errors or incompleteness. If there are errors, criticism and correction are welcome. This tutorial will be updated for a long time. If you have any good suggestions, welcome feedback. Code words are not easy. If this article is helpful to you, if you have spare money, you can reward and support my creation. If you want to reprint, please attach my reprint information to the back of the article and state my personal information and my blog address, but you must notify me in advance.

If you look from the middle, please read it carefully Yu Xia's view of Win system kernel -- a brief introduction , easy to learn this tutorial.

  before reading this tutorial, ask a question, do you know the purpose of learning driver? Is your development environment ready? Have you learned the content of the previous section? If you don't, don't continue. Please re learn the tutorial content of the previous driver to continue.

🔒 Gorgeous dividing line 🔒

Use of kernel API

  in application layer programming, we can use various API functions provided by WINDOWS, as long as we import the header file windows.h. However, during kernel programming, Microsoft provides a special API for kernel programs, which can be used as long as the corresponding header file is included in the program, such as: #include < ntddk. H >, provided that you have WDK installed.
  what if you encounter a function that you can't or don't know how to use the function? When programming in the application layer, we use MSDN to understand the details of functions. When programming in the kernel, we should use WDK's own help documents.
   however, the WDK documentation only contains functions exported by the kernel module, and functions not exported cannot be used directly. If you want to use an unexported function, you can use it as long as you define a function pointer and provide the correct function address for the function pointer. There are two ways to get the address of the exported function: signature search and parsing the kernel PDB file. The one and only one encoding as like as two peas is the same as the hard encoding. For the last method, let's think about why WinDbg is so powerful. Why can WinDbg easily analyze some structures or function names? The essential reason is that it has a symbolic file and can parse it, that is, PDB file. That is why we need to equip it with symbol file path before.

Drive basic data type

   during kernel programming, it is strongly recommended that you follow the coding habits of WDK and do not write: unsigned long length;, It is suggested to write as follows: ULONG length.
  the following are WDK habits and our regular habits:

WDK habits SDK habits
ULONG unsigned long
PULONG unsigned long*
UCHAR unsigned char
PUCHAR unsigned char*
UINT unsigned int
PUNIT unsigned int*
VOID void
PVOID void*

Function return value

  the return values of most kernel functions are of NTSTATUS type, such as:

NTSTATUS PsCreateSystemThread();
NTSTATUS ZwOpenProcess();
NTSTATUS ZwOpenEvent();

   this value can describe the result of function execution, such as:

#define STATUS_SUCCESS 0x00000000 / / success
#define STATUS_INVALID_PARAMETER 0xC000000D / / invalid parameter
#define STATUS_BUFFER_OVERFLOW 0x80000005 / / insufficient buffer length

  when you call the kernel function, if the returned result is not STATUS_SUCCESS indicates that there is a problem in the function execution. The specific problem can be viewed in the ntstatus.h file.

Kernel exception handling

  in the kernel, a small error may lead to a blue screen, such as reading and writing an invalid memory address. In order to make your kernel program more robust, it is strongly recommended that you use exception handling when writing kernel programs to reduce the possibility of blue screen. But the error is big. The blue screen is still the blue screen.
  Windows provides a structured exception handling mechanism, which is supported by general compilers, as follows:

    //Possible error codes
__except(filter_value) {
    //Code to execute when an error occurs

   when an exception occurs, it can be determined according to the filter_value to determine if the program should be executed, when filter_ The value of value is:
one ️⃣ EXCEPTION_EXECUTE_HANDLER(1): the code enters the except block
two ️⃣ EXCEPTION_CONTINUE_SEARCH(0): do not handle exceptions, which are handled by the calling function of the upper layer
three ️⃣ EXCEPTION_CONTINUE_EXECUTION(-1): go back and continue to execute the code at the error

Common kernel memory functions

  the use of memory mainly includes application, setting, copying and release. We are writing 3-ring applications and functions corresponding to the kernel, for example, see the help documents of MSDN and WDK for specific use:

General procedure In kernel
malloc ExAllocatePool2
memset RtlFillMemory
memcpy RtlMoveMemory
free ExFreePool

   of course, there are many kernel functions corresponding to malloc, but many of them have been discarded. The function description is as follows:

  ExAllocatePool is obsolete and has been deprecated in Windows 10, version 2004. It has been replaced by ExAllocatePool2. For more information, see Updating deprecated > ExAllocatePool calls to ExAllocatePool2 and ExAllocatePool3.
  When developing drivers for version of Windows prior to Windows 10, version 2004, use ExAllocatePoolZero.

Kernel string

    when writing 3-ring programs, we often use CHAR(char)/WCHAR(wchar_t) to represent house string and wide string respectively, and 0 to represent the end. But in the kernel, we often use: ANSI_STRING/UNICODE_STRING to represent a string and a wide string, respectively. Their structure is as follows:
  ANSI_STRING string:

typedef struct _STRING
    USHORT Length;
    USHORT MaximumLength;
    PCHAR Buffer;


typedef struct _UNICODE_STRING
    USHORT Length;
    USHORT MaxmumLength;
    PWSTR Buffer;

  why does the kernel use such a string? Mainly for safety reasons. When we first learned C language, we often printed strings such as hot because it printed strings that didn't end with 0. If this problem occurs in the kernel, it is easy to cause a blue screen. Therefore, the modified structure is used to ensure safety. Of course, there are special functions for processing such strings in the kernel, which I will continue to introduce next.

Kernel string common functions

   the common functions of strings are nothing more than creation, copy, comparison, conversion and so on. Their functions are as follows. Please check the help document of WDK for details:

RtlInitAnsiString RtlInitUnicodeString
RtlCopyString RtlCopyUnicodeString
RtlCompareString RtlCompareUnicoodeString
RtlAnsiStringToUnicodeString RtlUnicodeStringToAnsiString

Code detail analysis

    in the last tutorial, we used a piece of code to test whether the driver can be loaded and executed. Let's analyze it. The code used last time is as follows:

#include <ntddk.h>

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
    DbgPrint("Chapter Driver By WingSummer,Unloaded Successfully!");

    DbgPrint("Chapter Driver By WingSummer,Loaded Successfully!");
    DriverObject->DriverUnload = UnloadDriver;

    return STATUS_SUCCESS;


   DriverEntry is the entry of the driver. If the driver is loaded successfully, call the DllMain function just as Dll is loaded successfully.


  refers to the driver_ Pointer to the object structure. After a driver file is loaded, its complete information will be returned to us. Let's take a look at driver_ What is stored in the object structure? The following is the definition in the header file:

typedef struct _DRIVER_OBJECT {
    CSHORT Type;
    CSHORT Size;

    PDEVICE_OBJECT DeviceObject;
    ULONG Flags;

    PVOID DriverStart;
    ULONG DriverSize;
    PVOID DriverSection;
    PDRIVER_EXTENSION DriverExtension;

    UNICODE_STRING DriverName;
    PUNICODE_STRING HardwareDatabase;
    PFAST_IO_DISPATCH FastIoDispatch;

    PDRIVER_STARTIO DriverStartIo;
    PDRIVER_UNLOAD DriverUnload;


  since it is the basis of explanation, we will choose the most important ones to explain. However, in order to facilitate learning drive, we have made minor modifications to the above code:

#include <ntddk.h>

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
    DbgPrint("Chapter Driver By WingSummer,Unloaded Successfully!");

    DbgPrint("Chapter Driver By WingSummer,Loaded Successfully!");

    DriverObject->DriverUnload = UnloadDriver;
    DbgPrint("addr: %p", DriverObject);

    return STATUS_SUCCESS;

  then compile and let the virtual machine load the driver. As shown in the figure below, then we get its first address:

  then let's dt talk again:

kd> dt _DRIVER_OBJECT 89B7FA20
   +0x000 Type             : 0n4
   +0x002 Size             : 0n168
   +0x004 DeviceObject     : (null)
   +0x008 Flags            : 0x12
   +0x00c DriverStart      : 0xbab50000 Void
   +0x010 DriverSize       : 0x6000
   +0x014 DriverSection    : 0x89936678 Void
   +0x018 DriverExtension  : 0x89b7fac8 _DRIVER_EXTENSION
   +0x01c DriverName       : _UNICODE_STRING "\Driver\HelloDriver"
   +0x028 FastIoDispatch   : (null)
   +0x02c DriverInit       : 0xbab54000     long  HelloDriver!GsDriverEntry+0
   +0x030 DriverStartIo    : (null)
   +0x034 DriverUnload     : 0xbab51040     void  HelloDriver!UnloadDriver+0
   +0x038 MajorFunction    : [28] 0x804f454a     long  nt!IopInvalidDeviceRequest+0


   the starting address of the drive object after loading.


   the memory size after the driver object is loaded.


  it is an LDR that stores all currently loaded driver information_ DATA_ TABLE_ Bidirectional circular linked list of entry structure. Through this thing, we can string them all. Through this, we can also traverse them. Let's take a look through WinDbg. Let dt's talk about the DriverSection of the driver written by ourselves:

kd> dt _LDR_DATA_TABLE_ENTRY 0x89936678
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x80554fc0 - 0x89b80d58 ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xffffffff - 0xffffffff ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x630069 - 0x0 ]
   +0x018 DllBase          : 0xbab50000 Void
   +0x01c EntryPoint       : 0xbab54000 Void
   +0x020 SizeOfImage      : 0x6000
   +0x024 FullDllName      : _UNICODE_STRING "\??\C:\Documents and Settings\wingsummer\desktop\HelloDriver.sys"
   +0x02c BaseDllName      : _UNICODE_STRING "HelloDriver.sys"
   +0x034 Flags            : 0x9104000
   +0x038 LoadCount        : 1
   +0x03a TlsIndex         : 0x49
   +0x03c HashLinks        : _LIST_ENTRY [ 0xffffffff - 0x1055c ]
   +0x03c SectionPointer   : 0xffffffff Void
   +0x040 CheckSum         : 0x1055c
   +0x044 TimeDateStamp    : 0xfffffffe
   +0x044 LoadedImports    : 0xfffffffe Void
   +0x048 EntryPointActivationContext : (null)
   +0x04c PatchInformation : 0x00650048 Void

  then we move on to dt the next member:

kd> dt _LDR_DATA_TABLE_ENTRY 0x89b80d58
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x89936678 - 0x89b45e98 ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xb8183850 - 0x1 ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0xe - 0x0 ]
   +0x018 DllBase          : 0xb817e000 Void
   +0x01c EntryPoint       : 0xb81a6105 Void
   +0x020 SizeOfImage      : 0x2b000
   +0x024 FullDllName      : _UNICODE_STRING "\SystemRoot\system32\drivers\kmixer.sys"
   +0x02c BaseDllName      : _UNICODE_STRING "kmixer.sys"
   +0x034 Flags            : 0x9104000
   +0x038 LoadCount        : 1
   +0x03a TlsIndex         : 0x74
   +0x03c HashLinks        : _LIST_ENTRY [ 0xffffffff - 0x2f580 ]
   +0x03c SectionPointer   : 0xffffffff Void
   +0x040 CheckSum         : 0x2f580
   +0x044 TimeDateStamp    : 0xe1786190
   +0x044 LoadedImports    : 0xe1786190 Void
   +0x048 EntryPointActivationContext : (null)
   +0x04c PatchInformation : 0x006d006b Void

  it can be seen that we can traverse the driver information through this linked list.


  indicates the name of the driver object, which is a_ UNICODE_ The structure of string.


   the unloading address of the driver object. If it exists, it will be called. Its definition:

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)


  the remaining members who are not introduced will continue to explore on their own if they are interested.


   the full name of IRQL is Interrupt Request Level, that is, the priority of interrupt execution. It is a set of priority schemes defined by Windows. It has nothing to do with CPU. The higher the value, the higher the permission. Interrupt includes hard interrupt and soft interrupt. Hard interrupt is generated by hardware, while soft interrupt is completely virtual. The processor executes thread code on an IRQL, and each processor's IRQL determines how it handles interrupts and which interrupts it is allowed to receive. On the same processor, threads can only be interrupted by higher-level IRQL threads. Each processor has its own interrupt IRQL. There are four common IRQL levels: Passive, APC, Dispatch, and DIRQL. PASSIVE_LEVEL is the lowest level. There are no masked interrupts. The thread executes user mode and can access paging memory. APC_LEVEL only APC level interrupts are masked and can access paging memory. When an APC occurs, the processor is promoted to the APC level and other APCs are shielded. DISPATCH_LEVEL can mask DPC (delayed process) and lower interrupts, and cannot access paging memory. Because only paging memory can be processed, the number of API s that can be accessed at this level is greatly reduced. For our kernel security, it is enough to understand these. The following is the schematic diagram of IRQL:

  pay special attention to IRQL when writing kernel programs. There are a lot of blue screens.

This section exercises

The answers in this section will be explained in the next section. Be sure to read the next explanation after completing this section. Don't be lazy. Experiment is a shortcut to learn this tutorial.

   as the saying goes, you can only speak without practicing the fake skill. The following are the relevant exercises in this section. If you don't do well in the exercise, don't look at the next tutorial. If you don't do the exercise, it's easy to get mixed up. At first, you still understand, and then you really don't understand at all. There are not many exercises in this section. Please complete it with quality and quantity.

one ️⃣ Write a driver, apply for a piece of memory, and store all the data of GDT table in memory. Then it is displayed in DebugView, and finally the memory is released.

two ️⃣ Write the driver to realize the following functions:
<1> Initialize a string;
<2> Copy a string;
<3> Compare whether two strings are equal;
<4> ANSI_ String and UNICODE_STRING string conversion;

three ️⃣ Question: why DISPATCH_LEVEL cannot access paging memory.


  drivers -- kernel space and kernel modules

Posted by on Wed, 03 Nov 2021 03:10:21 -0700