Windows Kernel Debugging

This article is looking at debugging the Windows kernel over a network. We’re going to use our debug environment to hide running processes and elevate privileges, two tasks that would be useful for a rootkit to perform.

You will need one machine to run WinDBG on, and another as the debugging target. Ideally, the target will be a virtual machine. I’m using Windows 11 as the debugging host, and Windows Server 2022 as the target.

On the debugging (Windows 11) machine, install WinDBG and Visual Studio 2022. Next, add the following Visual Studio Components.

  • MSVC v143 – VS 2022 C++ ARM64/ARM64EC Spectre-mitigated libs (Latest)
  • MSVC v143 – VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)
  • C++ ATL for latest v143 build tools with Spectre Mitigations (ARM64/ARM64EC)
  • C++ ATL for latest v143 build tools with Spectre Mitigations (x86 & x64)
  • C++ MFC for latest v143 build tools with Spectre Mitigations (ARM64/ARM64EC)
  • C++ MFC for latest v143 build tools with Spectre Mitigations (x86 & x64)
  • Windows Driver Kit

Download and install the Windows SDK to the same machine.

https://developer.microsoft.com/en-gb/windows/downloads/windows-sdk

From the debugging machine, copy of the following files to the target Virtual Machine we aim to debug.

C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\kdnet.exe
C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\VerifiedNICList.xml

Run the kdnet executable on the target, specifying the debugging machines IP address and a port number (in this instance 50000).

C:\KdNet>kdnet.exe 192.168.1.81 50000

Enabling network debugging on Intel(R) PRO/1000 MT Desktop Adapter.
Manage-bde.exe not present.  Bitlocker presumed disabled.

To debug this machine, run the following command on your debugger host machine.
windbg -k net:port=50000,key=3129p24sd9mlr.e735r5vy22a4.176gduo22v4yt.101ic7t5rgws8

Then reboot this machine by running shutdown -r -t 0 from this command prompt.

In WinDBG on the debugging machine, select File > Attach the Kernel. Specify the connection string.

Once connected, run shutdown -r -t 0 on the guest system. You should see a connection coming back to WinDBG as per the below output.

************* Preparing the environment for Debugger Extensions Gallery repositories **************
   ExtensionRepository : Implicit
   UseExperimentalFeatureForNugetShare : true
   AllowNugetExeUpdate : true
   NonInteractiveNuget : true
   AllowNugetMSCredentialProviderInstall : true
   AllowParallelInitializationOfLocalRepositories : true
   EnableRedirectToChakraJsProvider : false

   -- Configuring repositories
      ----> Repository : LocalInstalled, Enabled: true
      ----> Repository : UserExtensions, Enabled: true

>>>>>>>>>>>>> Preparing the environment for Debugger Extensions Gallery repositories completed, duration 0.000 seconds

************* Waiting for Debugger Extensions Gallery to Initialize **************

>>>>>>>>>>>>> Waiting for Debugger Extensions Gallery to Initialize completed, duration 0.047 seconds
   ----> Repository : UserExtensions, Enabled: true, Packages count: 0
   ----> Repository : LocalInstalled, Enabled: true, Packages count: 45

Microsoft (R) Windows Debugger Version 10.0.27920.1001 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.

Using NET for debugging
Opened WinSock 2.0
Using IPv4 only.
Waiting to reconnect...
Connected to target 192.168.1.196 on port 50000 on local IP 192.168.1.81.
You can get the target MAC address by running .kdtargetmac command.
Connected to Windows 10 20348 x64 target at (Thu Sep 11 18:11:40.852 2025 (UTC + 1:00)), ptr64 TRUE
Kernel Debugger connection established.
Symbol search path is: srv*
Executable search path is: 
Windows 10 Kernel Version 20348 MP (4 procs) Free x64
Product: Server, suite: TerminalServer SingleUserTS
Edition build lab: 20348.2849.amd64fre.fe_release_svc_prod1.241101-1732
Kernel base = 0xfffff801`0de00000 PsLoadedModuleList = 0xfffff801`0ea33ad0
Debug session time: Thu Sep 11 15:11:40.887 2025 (UTC + 1:00)
System Uptime: 0 days 1:02:29.657
Shutdown occurred at (Thu Sep 11 18:11:48.743 2025 (UTC + 1:00))...unloading all symbol tables.
Using NET for debugging
Opened WinSock 2.0
Using IPv4 only.
Waiting to reconnect...
Connected to target 192.168.1.196 on port 50000 on local IP 192.168.1.81.
You can get the target MAC address by running .kdtargetmac command.
Connected to target 192.168.1.196 on port 50000 on local IP 192.168.1.81.
You can get the target MAC address by running .kdtargetmac command.
Connected to target 192.168.1.196 on port 50000 on local IP 192.168.1.81.
You can get the target MAC address by running .kdtargetmac command.
Connected to Windows 10 20348 x64 target at (Thu Sep 11 18:11:56.124 2025 (UTC + 1:00)), ptr64 TRUE
Kernel Debugger connection established.

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*
Symbol search path is: srv*
Executable search path is: 
Windows 10 Kernel Version 20348 MP (1 procs) Free x64
Edition build lab: 20348.2849.amd64fre.fe_release_svc_prod1.241101-1732
Kernel base = 0xfffff806`1f600000 PsLoadedModuleList = 0xfffff806`20233ad0
System Uptime: 0 days 0:00:03.618
nt!DebugService2+0x5:
fffff806`1fa38605 cc              int     3
kd> !process 0 0 system
NULL value in PsActiveProcess List
kd> g

With a connection established, we can start manipulating Kernel memory in the remote machine. Note, that normally technologies such as PatchGuard would prevent this.


Hiding Processes

In the Kernel, processes are tracked through the EPROCESS data structures. The field ActiveProcessLinks in an EPROCESS object is used to track running processes.

You can examine the ActiveProcessLinks objects structure using the Display Type (dt) command in WinDBG.

0: kd> dt nt!_eprocess ActiveProcessLinks
   +0x448 ActiveProcessLinks : _LIST_ENTRY
0: kd> dt nt!_LIST_ENTRY
   +0x000 Flink            : Ptr64 _LIST_ENTRY
   +0x008 Blink            : Ptr64 _LIST_ENTRY

From the above output, we can see that each entry is of type _LIST_ENTRY. Each _LIST_ENTRY includes forward (FLINK) and backward (BLINK) pointers. These are pointers to the next and previous AcitveProcessLink entries. In addition, we know that ActiveProcessLinks is at the relative virtual offset of 0x448 from the base address of the EPROCESS entry.

typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink;  // points to the next entry in the list
  struct _LIST_ENTRY *Blink;  // points to the previous entry in the list
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

As such, the ActiveProcessLinks entries form a doubly-linked list, with each entry containing pointers to the previous and next ActiveProcessLinks entry.

We can examine the process entries for cmd.exe using the dt command.

0: kd> !process 0 0 cmd.exe
PROCESS ffffe10eb43573c0
    SessionId: 1  Cid: 14d0    Peb: d185fe5000  ParentCid: 07b8
    DirBase: 6b07a000  ObjectTable: ffff8c0b66bf9b80  HandleCount:  84.
    Image: cmd.exe
    
0: kd> dt nt!_LIST_ENTRYffffe10eb43573c0+0x448
Symbol nt!_LIST_ENTRYffffe10eb43573c0+0x448 not found.
0: kd> dt nt!_LIST_ENTRY ffffe10eb43573c0+0x448
 [ 0xffffe10e`b435a788 - 0xffffe10e`b42ef748 ]
   +0x000 Flink            : 0xffffe10e`b435a788 _LIST_ENTRY [ 0xffffe10e`aec924c8 - 0xffffe10e`b4357808 ]
   +0x008 Blink            : 0xffffe10e`b42ef748 _LIST_ENTRY [ 0xffffe10e`b4357808 - 0xffffe10e`b427b688 ]

FLINK = 0xffffe10eb435a788
BLINK = 0xffffe10eb42ef748

To hide a process from the system’s process list (effectively making it invisible to the operating system), the FLINK and BLINK pointers of the surrounding ActiveProcessLinks entries must be updated to point to each other, bypassing the target process.

Let’s look at hiding the cmd.exe process. In the below screenshot, we can clearly see cmd.exe is running and is listed as a process in task manager.

We can remove cmd.exe from the process listing by using the Enter Quadword “eq” command. eq uses the destination as the first parameter (as a pointer) and the second parameter as the value to be written to memory.

First set the FLINK of the previous process to point to the next process, skipping over cmd.exe. As previously seen, our FLINK was set to 0xffffe10eb435a788 and our BLINK was set to 0xffffe10eb42ef748.

0: kd> eq 0xffffe10e`b42ef748 0xffffe10e`b435a788

Next, point the BLINK of the next process to point to the previous process. The Relative Virtual Address (RVA) of 0x8 will be the next process, since the entries are 8 bytes long.

eq 0xffffe10e`b435a788+0x8 0xffffe10e`b42ef748

Finally, we can nullify the BLINK/FLINK entries of our cmd.exe process. This might not be strictly necessary.

eq ffffe10e`b43573c0+0x448 0
eq ffffe10e`b43573c0+0x448+0x8 0

Once execution continues, you should find cmd.exe is no longer listed as a running process!


Access Token Manipulation

With access to the Kernel, you can change permissions on any process to that of your choosing. In this example, we’re going to look at elevating a command prompt to “NT AUTHORITY\SYSTEM”.

To do this, we can copy the access token of a privileged process to our command prompt EPROCESS.

An access token is an object that describes the security context of a process.

First, get a handle to the kernel process “system”. This is essentially the running kernel.

0: kd> !process 0 0 system
PROCESS ffffd08f796cf040
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001aa000  ObjectTable: ffff8489d764be80  HandleCount: 2055.
    Image: System

Inspect the Token member of the EPROCESS data structure using the Display Type (dt) command.

dt nt!_eprocess ffffd08f796cf040 Token
   +0x4b8 Token : _EX_FAST_REF

Examine the current Token value.

0: kd> dt _ex_fast_ref 0xffffd08f796cf040+0x4b8
nt!_EX_FAST_REF
   +0x000 Object           : 0xffff8489`d76208de Void
   +0x000 RefCnt           : 0y1110
   +0x000 Value            : 0xffff8489`d76208de

Based on this output, we know that the SYSTEM access token is 0xffff8489d76208de. We can copy this Token to another process to elevate it’s privileges to SYSTEM.

Let’s get a pointer to the cmd.exe Token offset (0x4b8).

0: kd> !process 0 0 cmd.exe
PROCESS ffffd08f7e48a0c0
    SessionId: 1  Cid: 0dbc    Peb: 8607ae3000  ParentCid: 10e8
    DirBase: 30fe6000  ObjectTable: ffff8489dcbf24c0  HandleCount:  76.
    Image: cmd.exe
0: kd> dt nt!_eprocess ffffd08f7e48a0c0 Token
   +0x4b8 Token : _EX_FAST_REF
0: kd> dt _ex_fast_ref 0xffffd08f7e48a0c0+0x4b8
nt!_EX_FAST_REF
   +0x000 Object           : 0xffff8489`de09f779 Void
   +0x000 RefCnt           : 0y1001
   +0x000 Value            : 0xffff8489`de09f779

Then use the enter quadword “eq” command to overwrite the cmd.exe Token with our system Token value (0xffff8489`d76208de).

0: kd> eq ffffd08f7e48a0c0+0x4b8 0xffff8489`d76208de

After continuing execution, we can see our cmd.exe process has elevated to “NT AUTHORITY\SYSTEM”.


In Conclusion

As mentioned earlier, modifying Kernel data structures isn’t possible during normal system operation. However, understanding the basics of these data structures is valuable for future projects, such as rootkits and malicious drivers.