Recently-ish (~2020), Microsoft changed the way the kernel image is mapped and also some implementation details of hal.dll. The kernel changes have caused existing methods of finding the base of the kernel via shellcode or a leak and arbitrary read to crash. This obviously isn't great, so I decided to figure out a way around the issue to support some code I've been writing in my free time (maybe more on that later).

Our discussion is going to start at Windows 10 1903 and then move up through Windows 10 21H2. These changes are also still present in Windows 11.

What's the point(er)?

Finding the base of the kernel is important for kernel exploits and kernel shellcode. If you can find the base of the kernel you can look up functions inside of it via the export table in its PE header. Various functions inside of the kernel allow you to allocate memory, start threads, and resolve other kernel module bases via the PsLoadedModuleList. Without being able to utilize kernel routines and symbols, you're pretty limited in what you can do if you're executing in kernel. Hopefully this clarifies why this post is even necessary.

Literature Review: Existing Methods

In order to understand where I am going with all of this, we first need to look at what techniques are already out there. This is split up into three parts: how to get to the base of the kernel, obtaining ("leaking") a kernel address to be used to find the base, and how to do version detection in kernel.

Getting to Kernel Base

Two of these methods rely on having some kind of memory leak of a kernel address, one does not. They really all have the same goal: to locate the base of the kernel.

All of these techniques apply to any PE file, not just the kernel.

NtQuerySystemInformation

The easiest and most version independent way to get the base of the kernel and all other kernel modules as via NtQuerySystemInformation using the SystemModuleInformation (0xB) member of the SYSTEM_INFORMATION_CLASS enumeration. When queried (with an appropriate buffer size), the function will return a filled out SYSTEM_MODULE_INFORMATION structure that contains a DWORD for the number of modules present and then an anysize array of SYSTEM_MODULE structures representing the modules. Here's some C code that uses it to query driver names and bases. You can actually get the base addresses and names of every kernel module via some documented APIs too: EnumDeviceDrivers and GetDeviceDriverBaseNameA from the PSAPI can be used together in order to accomplish that. On the backend they use NtQuerySystemInformation with the SystemModuleInformation class. FYI, psapi is just a small stub around the API set DLL api-ms-win-core-psapi-l1-1-0.dll, which ends up forwarding to kernelbase.dll in all versions.

kernelbase!EnumDeviceDrivers
A portion of kernelbase!EnumDeviceDrivers showing a call to NtQuerySystemInformation

GetDeviceDriverBaseNameA calls the unexported kernel32!FindDeviceDriver function, which again calls NtQuerySystemInformation with the SystemModuleInformation class.

Scan Backwards

In the event we cannot get any information from user-mode or we are in a low-integrity process, then the scanback technique can be used. Basically, we need a memory leak or reliable way of getting a kernel address to get in the "ballpark" of the kernel image. See the next section on "leaking" kernel addresses for more details on that. Once we have an address somewhere in the kernel, we can scan backwards one page (0x1000 bytes) at a time until we get to the PE header of the kernel image. This trick relies on two major assumptions:

  1. PE images are page aligned
  2. The memory space between the leaked address and the base of the kernel is contiguously mapped

We will see later that #2 isn't true on newer versions of Windows.

Every PE file starts with the bytes MZ (0x5a4d). To see if we have reached the beginning of the PE file, we can check to see if the page starts with MZ. If it does not, continue scanning back, if it does, then you have (probably) found the base of the image. I recommend doing a little bit more validation than that, such as seeing if the suspected base address + IMAGE_DOS_HEADER.e_lfanew contains the bytes PE (0x4550).

If you're interested in a code implementation of this technique, here's some code from zerosum0x0.

Relative Virtual Address (RVA)

The lamest of the kernel base finding methods is just to hard code the Relative Virtual Address (RVA) of the leaked symbol into your shellcode or exploit. This requires knowing the exact version(s) your code will be running on ahead of time and also requires version detection to support multiple versions of the kernel.

A slight variation on this method is to use an exported symbol from the leaked module to calculate its base. You can open the image file in user-mode and then look up the exported symbol to get its offset from the base address. This can be accomplished with LoadLibraryA and GetProcAddress. You can also do manual PE parsing. However, loading something like the kernel image into a user-mode process is pretty suspicious. You'll also need a way to pass the calculated RVA into your exploit or shellcode.

"Leaking" Kernel Addresses

To get a kernel address from an exploit you usually have to have a memory leak (information disclosure). When you're already executing via shellcode you have more options, but you still need to find a pointer into the kernel or another module to utilize the techniques above.

KPCR

Each logical processor on a Windows system has an associated structure called the Kernel Processor Control Region (KPCR). The KPCR is a massive structure, coming in at 0xC000 bytes as of the Windows 11 Beta. The first 0x180 bytes are almost entirely consistent across versions. At offset 0x180 lies the nested Kernel Processor Region Control Block (KPRCB) structure, which is very large and the reason that the KPCR is as large as it is. Members are added when major features (like KVAS) are added to the OS.

On 64-bit Windows, the GS segment register points to the KPCR for that processor. The swapgs instruction at kernel entry points (such as the system call handler, KiSystemCall64[Shadow], and Interrupt Service Routines (ISRs)) causes the processor to swap the contents of Model Specific Register (MSR) 0xC0000101 (GSBASE) with MSR 0xC0000102 (KERNEL_GSBASE). GSBASE is also the contents of the GS segment register. On 32-bit, 0x30 is explicitly loaded into FS at kernel entry points, and the GDT entry at offset 0x30 defines the base as the address of the KPCR for that processor.

nt!KiKernelSysretExit
swapgs at the 64-bit kernel entrypoint
nt!KiKernelSysretExit
Moving 0x30 into FS at the 32-bit kernel entrypoint

Both the upper members of the KPCR and the KPRCB have pointers into the kernel and other modules that might be of use to use while trying to calculate where exactly the kernel is located. The issue with the KPRCB is that fields change frequently, so the offset to a particular field of interest would be very version dependent.

Interrupt Descriptor Table

One classic and consistent place to find reliable pointers into the kernel in the KPCR is in the Interrupt Descriptor Table (IDT). The KPCR has a pointer to the IDT at offset 0x38, the IdtBase field. Dumping out quad words (with symbols) at that address gives some pointers into the kernel!

0: kd> dqs poi(@$pcr+38)+4
fffff802`35d8b004  fffff802`39448e00 nt!KiDebugServiceTrap+0x40
fffff802`35d8b00c  00102a40`00000000
fffff802`35d8b014  fffff802`39448e04 nt!KiDebugServiceTrap+0x44
fffff802`35d8b01c  00103040`00000000
fffff802`35d8b024  fffff802`39448e03 nt!KiDebugServiceTrap+0x43
fffff802`35d8b02c  001035c0`00000000
fffff802`35d8b034  fffff802`3944ee00 nt! ?? ::FNODOBFM::`string'+0x10
fffff802`35d8b03c  00103900`00000000
fffff802`35d8b044  fffff802`3944ee00 nt! ?? ::FNODOBFM::`string'+0x10
fffff802`35d8b04c  00103c40`00000000
fffff802`35d8b054  fffff802`39448e00 nt!KiDebugServiceTrap+0x40
fffff802`35d8b05c  00104180`00000000
fffff802`35d8b064  fffff802`39448e00 nt!KiDebugServiceTrap+0x40
fffff802`35d8b06c  00104680`00000000
fffff802`35d8b074  fffff802`39448e00 nt!KiDebugServiceTrap+0x40
fffff802`35d8b07c  00104a40`00000000

If you look a bit lower in the code from zerosum0x0 that I linked earlier you can see this is exactly the method being used to get a kernel address.

KTHREAD Pointers

One of the fields in the KPRCB that is consistent across versions of the kernel is the CurrentThread field at offset 8. This would be at the KPCR at offset 0x188 (x64). In fact, you'll see this offset repeatedly in the kernel, as this is what the kernel uses to get a pointer to the current thread running on the processor.

nt!KiKernelSysretExit
Here's an example from KiKernelSysretExit, which might look familiar from my KVAS post

If we dump pointers with symbols (dps) at the current thread over the size of KTHREAD, we can see many pointers into the kernel!

Pointers in KTHREAD (system thread)
0: kd> dps @$thread L@@C++(sizeof(nt!_KTHREAD)/8)
fffff802`39d4abc0  00000000`00200006
fffff802`39d4abc8  fffff802`39d4abc8 nt!KiInitialThread+0x8
fffff802`39d4abd0  fffff802`39d4abc8 nt!KiInitialThread+0x8
fffff802`39d4abd8  00000000`00000000
fffff802`39d4abe0  00000000`0791ddc0
fffff802`39d4abe8  fffff802`35d97c70
fffff802`39d4abf0  fffff802`35d92000
fffff802`39d4abf8  fffff802`35d98000
fffff802`39d4ac00  00000000`00000000
fffff802`39d4ac08  000000d2`4507715b
fffff802`39d4ac10  00000000`ffffffff
fffff802`39d4ac18  fffff802`35d97c00
fffff802`39d4ac20  fffff802`35d97cc0
fffff802`39d4ac28  00000000`00000000
fffff802`39d4ac30  00000409`00000100
fffff802`39d4ac38  00080000`00020044
fffff802`39d4ac40  00000000`00000000
fffff802`39d4ac48  00000000`00000000
fffff802`39d4ac50  00000000`00000000
fffff802`39d4ac58  fffff802`39d4ac58 nt!KiInitialThread+0x98
fffff802`39d4ac60  fffff802`39d4ac58 nt!KiInitialThread+0x98
fffff802`39d4ac68  fffff802`39d4ac68 nt!KiInitialThread+0xa8
fffff802`39d4ac70  fffff802`39d4ac68 nt!KiInitialThread+0xa8
fffff802`39d4ac78  ffffe70e`4e4a5040
fffff802`39d4ac80  00000000`00000000
fffff802`39d4ac88  00000000`00000000
fffff802`39d4ac90  00000000`00000000
fffff802`39d4ac98  00000000`00000000
fffff802`39d4aca0  00000000`00000000
fffff802`39d4aca8  00000000`00000000
fffff802`39d4acb0  00000000`00000000
fffff802`39d4acb8  00000000`00000000
fffff802`39d4acc0  00000000`00000008
fffff802`39d4acc8  fffff802`39d4ad90 nt!KiInitialThread+0x1d0
fffff802`39d4acd0  fffff802`39d4ad90 nt!KiInitialThread+0x1d0
fffff802`39d4acd8  00000000`00000000
fffff802`39d4ace0  00000000`00000000
fffff802`39d4ace8  00000000`00000000
fffff802`39d4acf0  6851f04c`965c27f1
fffff802`39d4acf8  00000000`00000000
fffff802`39d4ad00  00000000`00000000
fffff802`39d4ad08  00000000`00000000
fffff802`39d4ad10  00038a7a`00000401
fffff802`39d4ad18  fffff802`39d4abc0 nt!KiInitialThread
fffff802`39d4ad20  ffffe70e`506fcd88
fffff802`39d4ad28  00000000`00000000
fffff802`39d4ad30  00000000`00000000
fffff802`39d4ad38  00000000`00000000
fffff802`39d4ad40  00020002`00000000
fffff802`39d4ad48  fffff802`39d4abc0 nt!KiInitialThread
fffff802`39d4ad50  00000000`00000000
fffff802`39d4ad58  00000000`00000000
fffff802`39d4ad60  00000000`00000000
fffff802`39d4ad68  00000000`00000000
fffff802`39d4ad70  00014f81`00000000
fffff802`39d4ad78  fffff802`39d4abc0 nt!KiInitialThread
fffff802`39d4ad80  00000000`00000000
fffff802`39d4ad88  00000000`00000000
fffff802`39d4ad90  fffff802`39d4acc8 nt!KiInitialThread+0x108
fffff802`39d4ad98  fffff802`39d4acc8 nt!KiInitialThread+0x108
fffff802`39d4ada0  00000000`01020401
fffff802`39d4ada8  fffff802`39d4abc0 nt!KiInitialThread
fffff802`39d4adb0  00000000`00000000
fffff802`39d4adb8  00000000`00000000
fffff802`39d4adc0  00000000`00000000
fffff802`39d4adc8  00000000`00000000
fffff802`39d4add0  00000000`00000000
fffff802`39d4add8  00000000`00000000
fffff802`39d4ade0  fffff802`39d47ac0 nt!KiInitialProcess
fffff802`39d4ade8  fffff802`39d1db90 nt!KiBootProcessorIdleThreadUserAffinity
fffff802`39d4adf0  00000000`00000000
fffff802`39d4adf8  00000000`00000014
fffff802`39d4ae00  fffff802`39d21cc0 nt!KiBootProcessorIdleThreadAffinity
fffff802`39d4ae08  00000000`00010000
fffff802`39d4ae10  00000000`00000004
fffff802`39d4ae18  fffff802`39d4ae18 nt!KiInitialThread+0x258
fffff802`39d4ae20  fffff802`39d4ae18 nt!KiInitialThread+0x258
fffff802`39d4ae28  fffff802`39d4ae28 nt!KiInitialThread+0x268
fffff802`39d4ae30  fffff802`39d4ae28 nt!KiInitialThread+0x268
fffff802`39d4ae38  fffff802`39d47ac0 nt!KiInitialProcess
fffff802`39d4ae40  00000000`19000000
fffff802`39d4ae48  00006804`7f580012
fffff802`39d4ae50  fffff802`39d4abc0 nt!KiInitialThread
fffff802`39d4ae58  00000000`00000000
fffff802`39d4ae60  00000000`00000000
fffff802`39d4ae68  fffff802`393b2170 nt!EmpCheckErrataList
fffff802`39d4ae70  fffff802`393b2170 nt!EmpCheckErrataList
fffff802`39d4ae78  fffff802`39337ac0 nt!KiSchedulerApc
fffff802`39d4ae80  fffff802`39d4abc0 nt!KiInitialThread
fffff802`39d4ae88  00000000`00000000
fffff802`39d4ae90  00000000`00000000
fffff802`39d4ae98  00000000`00000000
fffff802`39d4aea0  00000001`00060000
fffff802`39d4aea8  fffff802`39d4aea8 nt!KiInitialThread+0x2e8
fffff802`39d4aeb0  fffff802`39d4aea8 nt!KiInitialThread+0x2e8
fffff802`39d4aeb8  ffffe70e`4e535378
fffff802`39d4aec0  fffff802`39d47af0 nt!KiInitialProcess+0x30
fffff802`39d4aec8  fffff802`39d4aec8 nt!KiInitialThread+0x308
fffff802`39d4aed0  fffff802`39d4aec8 nt!KiInitialThread+0x308
fffff802`39d4aed8  00000000`0000003f
...

Now for consistency's sake, I'm going to explicitly dump out the same information from a user-mode thread, cmd.exe in this case.

Pointers in KTHREAD (user thread)
0:kd> dps ffffe70e57dee0c0 L@@C++(sizeof(nt!_KTHREAD)/8)
ffffe70e`57dee0c0  00000000`00a00006
ffffe70e`57dee0c8  ffffe70e`57dee0c8
ffffe70e`57dee0d0  ffffe70e`57dee0c8
...
ffffe70e`57dee350  ffffe70e`57dee0c0
ffffe70e`57dee358  ffffe70e`552eaf50
ffffe70e`57dee360  ffffe70e`57dee158
ffffe70e`57dee368  fffff802`393b2170 nt!EmpCheckErrataList
ffffe70e`57dee370  fffff802`393b2170 nt!EmpCheckErrataList
ffffe70e`57dee378  fffff802`39337ac0 nt!KiSchedulerApc
ffffe70e`57dee380  ffffe70e`57dee0c0
ffffe70e`57dee388  00000000`00000000
...

The output was shortened in places that did not have kernel pointers. Notice there are only three kernel pointers in this thread! The two different functions and their offsets into KTHREAD are consistent between the system thread and the user thread. If you check any thread, you will find that these pointers are present. What are these three fields? The offset into KTHREAD to the first nt!EmpCheckErrataList pointer is 0x2a8 (0xffffe70e57dee368-0xffffe70e57dee0c0). Dumping out KTHREAD gives the answer!

0: kd> dt -v -r1 _KTHREAD @$thread
nt!_KTHREAD
struct _KTHREAD, 225 elements, 0x480 bytes
   +0x000 Header           : struct _DISPATCHER_HEADER, 59 elements, 0x18 bytes
...
   +0x288 SchedulerApc     : struct _KAPC, 19 elements, 0x58 bytes
      +0x000 Type             : 0x12 ''
      +0x001 AllFlags         : 0 ''
      +0x001 CallbackDataContext : Bitfield 0y0
      +0x001 Unused           : Bitfield 0y0000000 (0)
      +0x002 Size             : 0x58 'X'
      +0x003 SpareByte1       : 0x7f ''
      +0x004 SpareLong0       : 0x6804
      +0x008 Thread           : 0xfffff802`39d4abc0 struct _KTHREAD, 225 elements, 0x480 bytes
      +0x010 ApcListEntry     : struct _LIST_ENTRY, 2 elements, 0x10 bytes
 [ 0x00000000`00000000 - 0x00000000`00000000 ]
      +0x020 KernelRoutine    : 0xfffff802`393b2170        void  nt!EmpCheckErrataList+0
      +0x028 RundownRoutine   : 0xfffff802`393b2170        void  nt!EmpCheckErrataList+0
      +0x030 NormalRoutine    : 0xfffff802`39337ac0        void  nt!KiSchedulerApc+0
      +0x020 Reserved         : [3] 0xfffff802`393b2170 Void
      +0x038 NormalContext    : 0xfffff802`39d4abc0 Void
      +0x040 SystemArgument1  : (null) 
      +0x048 SystemArgument2  : (null) 
      +0x050 ApcStateIndex    : 0 ''
      +0x051 ApcMode          : 0 ''
      +0x052 Inserted         : 0 ''
   +0x288 SchedulerApcFill1 : [3]  "???"
   +0x28b QuantumReset     : 0x7f ''
   +0x288 SchedulerApcFill2 : [4]  "???"
   +0x28c KernelTime       : 0x6804
   +0x288 SchedulerApcFill3 : [64]  "???"
   +0x2c8 WaitPrcb         : (null) 
   +0x288 SchedulerApcFill4 : [72]  "???"
...
The dt WinDbg command has a lot of useful options. -v and -r (used above) show sizes for fields and recurse through nested structures, respectively. Check out the docs for more options and info!

The fields are the KernelRoutine, RundownRoutine, and NormalRoutine function pointers in the SchedulerApc member of KTHREAD. These offsets have been consistent since Windows 8 RTM where the name of the field was changed from SuspendApc to SchedulerApc. Unfortunately, these function pointers seem to have been removed from Windows 21H1, probably to prevent this kind of disclosure. Of course you can just go back to the old versions to get the true use, since they are still present in newer Windows versions.

It's worth noting that I'm not the first one to discover this. Pages 20 and 21 of Morten Schenk's 2017 BlackHat briefing paper show that if you have a pointer to KTHREAD, then you can reliably get pointers into the kernel (hence why this is in the literature review section).

LSTAR MSR

When a syscall instruction is executed, the processor jumps to the address contained in the LSTAR Model Specific Register (MSR) (0xC0000082) after transitioning into kernel mode. This is not Windows specific behavior, as it is defined in the Intel Manual (Volume 2B, Chapter 4.3, SYSCALL). The system call handlers are unsurprisingly located in the kernel image, so if you can execute a rdmsr, you can get a pointer into the kernel. Of course this technique is only useful for shellcode or if you are somehow already executing in kernel.

With the introduction of KVAS, all of the kernel entry points were moved into a section in the kernel called KVASCODE. This section is present in both the user-mode and kernel-mode copies of the page tables. In kernels that have KVAS support up to Windows 10 19H2 the KVASCODE section directly borders the .text section, so if you are able to get an address of a kernel entry point (such as the one in the LSTAR MSR), then you can use it as a starting point for a scanback.

Passing in from Userland

Of course, one foolproof technique you can use to get the base of the kernel into your kernel mode payload is pass the address in from user-mode. This is assuming medium integrity execution in user-mode and will not help when you're dealing with a fully remote exploit.

Other Leaks

Talking about how more specific kernel memory leaks work is outside the scope of this post, but I will say that Microsoft very frequently patches kernel information disclosure bugs, so perhaps you can use my post about patch extraction and patch diffing to find and play with one :).

Version Detection in Kernel

Version detection can be accomplished by looking at the NtMajorVersion, NtMinorVersion, NtBuildNumber, and NtProductType fields of KUSER_SHARED_DATA, which is always located in the kernel at 0xFFDF0000 (32-bit) or 0xFFFFF78000000000 (64-bit). Microsoft recently randomized the writable version of this structure and a read-only mapping is located at the old static address. Information on that can be found on the MSRC blog and in this post by Connor McGarr.

Funny enough the NtMajorVersion is still 10 on Windows 11

What Has Changed?

Now that we are all up to speed on what techniques are already out there, we need to take a look at what Microsoft has changed in the most recent versions of Windows that get in the way of some of these techniques and then how to work around these changes to make sure exploitation and/or execution can keep working on 20H1 and higher.

Kernel Mapping and Fake Headers

In kernel versions prior to 20H1, the .text section of the kernel binary bordered the top of the image. This means that it also bordered the PE header for the image. This fact is why it is possible to use the scanback technique from a pointer into the .text section. In kernel versions 20H1 and up, the .text section no longer borders the PE header. In fact, no code sections at all border the PE header. The .rdata (read-only data), .pdata (exception data), and .idata (import data) sections now border the PE header. Between .idata and the next readable section, PROTDATA lies a few unmapped pages and then the text section at 0x200000 bytes offset from the base of the PE. Fortunately, .text and KVASCODE are contiguous with the sections in between them.

19H2 kernel memory segments
The image starts with .text and it borders the top of the image
20H2 kernel memory segments
The .text section and the base of the image are now non-contiguous

For the sake of validation, let's see if those pages are actually unmapped or if something is there. To do so, let's load up our trusty kernel debugger.

I'm just going to go back by a few thousand bytes fromt the kernel's text section into that gap and look over what is there, if anything.

0: kd> dc nt+200000-5000 L500
fffff806`6e3fb000  00000000 00000000 00002b00 72657355  .........+..User
fffff806`6e3fb010  68636143 746e4565 78457972 65726970  CacheEntryExpire
fffff806`6e3fb020  65754464 6f4c6f54 64656b63 73736553  dDueToLockedSess
fffff806`6e3fb030  006e6f69 00030b06 00000000 00000000  ion.............
fffff806`6e3fb040  55000032 43726573 65686361 72746e45  2..UserCacheEntr
fffff806`6e3fb050  70784579 64657269 54657544 536f4e6f  yExpiredDueToNoS
fffff806`6e3fb060  69737365 73416e6f 69636f73 6f697461  essionAssociatio
fffff806`6e3fb070  0b06006e 00000005 00000000 00720000  n.............r.
fffff806`6e3fb080  65735500 63614372 6e456568 53797274  .UserCacheEntryS
fffff806`6e3fb090  65746174 65706f00 69746172 6f436e6f  tate.operationCo
... boring, boring
fffff806`6e3fc000  00905a4d 00000003 00000004 0000ffff  MZ..............
fffff806`6e3fc010  000000b8 00000000 00000040 00000000  ........@.......
fffff806`6e3fc020  00000000 00000000 00000000 00000000  ................
fffff806`6e3fc030  00000000 00000000 00000000 000000e8  ................
fffff806`6e3fc040  0eba1f0e cd09b400 4c01b821 685421cd  ........!..L.!Th
fffff806`6e3fc050  70207369 72676f72 63206d61 6f6e6e61  is program canno
fffff806`6e3fc060  65622074 6e757220 206e6920 20534f44  t be run in DOS 
fffff806`6e3fc070  65646f6d 0a0d0d2e 00000024 00000000  mode....$.......

Well that looks interesting. It's a PE header... but to what?

0: kd> !dh fffff806`6e3fc000

File Type: DLL
FILE HEADER VALUES
     14C machine (i386)
       6 number of sections
2AB009D1 time date stamp Thu Sep 10 22:52:01 1992

       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
    2102 characteristics
            Executable
            32 bit word machine
            DLL

OPTIONAL HEADER VALUES
     10B magic #
   14.20 linker version
   1A800 size of code
    4600 size of initialized data
       0 size of uninitialized data
    7370 address of entry point
    1000 base of code
         ----- new -----
0000000076570000 image base
    1000 section alignment
     200 file alignment
       3 subsystem (Windows CUI)
   10.00 operating system version
   10.00 image version
   10.00 subsystem version
   23000 size of image
     400 size of headers
   233EC checksum
0000000000040000 size of stack reserve
0000000000001000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
    4540  DLL characteristics
            Dynamic base
            NX compatible
            No structured exception handler
            Guard
   11D80 [    99D3] address [size] of Export Directory
   1D364 [     154] address [size] of Import Directory
   20000 [     3D8] address [size] of Resource Directory
       0 [       0] address [size] of Exception Directory
   1EE00 [    2690] address [size] of Security Directory
   21000 [    1304] address [size] of Base Relocation Directory
    28E0 [      54] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
    1000 [      AC] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
   1D000 [     360] address [size] of Import Address Table Directory
    E0AC [     320] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory


SECTION HEADER #1
   .text name
   1A753 virtual size
    1000 virtual address
   1A800 size of raw data
     400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read


Debug Directories(3)
    Type       Size     Address  Pointer
    (   96)   60f01       d640f    a340f
    (1342988301)    300b       c1d01    b741d
    (4028183069)c015e017       a2619  10f0114

SECTION HEADER #2
   .data name
     4F4 virtual size
   1C000 virtual address
     200 size of raw data
   1AC00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #3
  .idata name
    1D9A virtual size
   1D000 virtual address
    1E00 size of raw data
   1AE00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #4
  .didat name
     8C4 virtual size
   1F000 virtual address
     A00 size of raw data
   1CC00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #5
   .rsrc name
     3D8 virtual size
   20000 virtual address
     400 size of raw data
   1D600 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #6
  .reloc name
    1304 virtual size
   21000 virtual address
    1400 size of raw data
   1DA00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

Everything seems to parse out OK, but there is some minor issues... For starters the machine type for this "DLL" is i386, which seems unlikely to be true since this is a 64-bit kernel. Another discrepancy is the debug directory, which seems to be completely bogus. It seems like there are a bunch of fake, mostly complete DOS/PE headers in that gap for some reason. The following command will find them all and dump their headers for closer inspection:

0: kd> .foreach (addr { s -[1]b nt L200000 4d 5a 90 00 03 }) { .echo ${addr}; dc ${addr} L20; !dh ${addr}; .echo }
NT header scan output
0xfffff806`6e200000
fffff806`6e200000  00905a4d 00000003 00000004 0000ffff  MZ..............
fffff806`6e200010  000000b8 00000000 00000040 00000000  ........@.......
fffff806`6e200020  00000000 00000000 00000000 00000000  ................
fffff806`6e200030  00000000 00000000 00000000 00000118  ................
fffff806`6e200040  0eba1f0e cd09b400 4c01b821 685421cd  ........!..L.!Th
fffff806`6e200050  70207369 72676f72 63206d61 6f6e6e61  is program canno
fffff806`6e200060  65622074 6e757220 206e6920 20534f44  t be run in DOS 
fffff806`6e200070  65646f6d 0a0d0d2e 00000024 00000000  mode....$.......

File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
    8664 machine (X64)
      21 number of sections
73F1C0C4 time date stamp Fri Aug 22 23:49:24 2031

       0 file pointer to symbol table
       0 number of symbols
      F0 size of optional header
      22 characteristics
            Executable
            App can handle >2gb addresses

OPTIONAL HEADER VALUES
     20B magic #
   14.20 linker version
  8B5600 size of code
  1B7E00 size of initialized data
  495000 size of uninitialized data
  98D010 address of entry point
    1000 base of code
         ----- new -----
fffff8066e200000 image base
    1000 section alignment
     200 file alignment
       1 subsystem (Native)
   10.00 operating system version
   10.00 image version
   10.00 subsystem version
 1046000 size of image
     800 size of headers
  A65799 checksum
0000000000080000 size of stack reserve
0000000000002000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
    4160  DLL characteristics
            High entropy VA supported
            Dynamic base
            NX compatible
            Guard
  134000 [   18C86] address [size] of Export Directory
  131630 [     168] address [size] of Import Directory
 1000000 [   3B23C] address [size] of Resource Directory
   C9000 [   67A7C] address [size] of Exception Directory
  A56600 [    2540] address [size] of Security Directory
 103C000 [    50B4] address [size] of Base Relocation Directory
   108E0 [      54] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
    5B30 [     118] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
  131000 [     620] address [size] of Import Address Table Directory
       0 [       0] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory


SECTION HEADER #1
  .rdata name
   C7940 virtual size
    1000 virtual address
   C7A00 size of raw data
     800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
48000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Only


Debug Directories(3)
    Type       Size     Address  Pointer
    cv           25       406e0    3fee0    Format: RSDS, guid, 1, ntkrnlmp.pdb
    (   13)    1568       40708    3ff08
    (   16)      24       41cc4    414c4

SECTION HEADER #2
  .pdata name
   67A7C virtual size
   C9000 virtual address
   67C00 size of raw data
   C8200 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
48000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Only

SECTION HEADER #3
  .idata name
    20C2 virtual size
  131000 virtual address
    2200 size of raw data
  12FE00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
48000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Only

SECTION HEADER #4
  .edata name
   18C86 virtual size
  134000 virtual address
   18E00 size of raw data
  132000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #5
PROTDATA name
       1 virtual size
  14D000 virtual address
     200 size of raw data
  14AE00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
48000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Only

SECTION HEADER #6
   GFIDS name
    8BFC virtual size
  14E000 virtual address
    8C00 size of raw data
  14B000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

SECTION HEADER #7
    Pad1 name
   A9000 virtual size
  157000 virtual address
       0 size of raw data
       0 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000080 flags
         Uninitialized Data
         Discardable
         (no align specified)
         Read Only

SECTION HEADER #8
   .text name
  3C6F59 virtual size
  200000 virtual address
  3C7000 size of raw data
  153C00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
68000020 flags
         Code
         Not Paged
         (no align specified)
         Execute Read

SECTION HEADER #9
    PAGE name
  3C5716 virtual size
  5C7000 virtual address
  3C5800 size of raw data
  51AC00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read

SECTION HEADER #A
  PAGELK name
   24E74 virtual size
  98D000 virtual address
   25000 size of raw data
  8E0400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read

SECTION HEADER #B
POOLCODE name
     48B virtual size
  9B2000 virtual address
     600 size of raw data
  905400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
68000020 flags
         Code
         Not Paged
         (no align specified)
         Execute Read

SECTION HEADER #C
  PAGEKD name
    5B92 virtual size
  9B3000 virtual address
    5C00 size of raw data
  905A00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read

SECTION HEADER #D
PAGEVRFY name
   320EC virtual size
  9B9000 virtual address
   32200 size of raw data
  90B600 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read

SECTION HEADER #E
PAGEHDLS name
    25D6 virtual size
  9EC000 virtual address
    2600 size of raw data
  93D800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read

SECTION HEADER #F
PAGEBGFX name
    69EA virtual size
  9EF000 virtual address
    6A00 size of raw data
  93FE00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read

SECTION HEADER #10
INITKDBG name
   195BA virtual size
  9F6000 virtual address
   19600 size of raw data
  946800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
68000020 flags
         Code
         Not Paged
         (no align specified)
         Execute Read

SECTION HEADER #11
TRACESUP name
    175B virtual size
  A10000 virtual address
    1800 size of raw data
  95FE00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
68000020 flags
         Code
         Not Paged
         (no align specified)
         Execute Read

SECTION HEADER #12
KVASCODE name
    23DE virtual size
  A12000 virtual address
    2400 size of raw data
  961600 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
68000020 flags
         Code
         Not Paged
         (no align specified)
         Execute Read

SECTION HEADER #13
  RETPOL name
     740 virtual size
  A15000 virtual address
     800 size of raw data
  963A00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
68000020 flags
         Code
         Not Paged
         (no align specified)
         Execute Read

SECTION HEADER #14
  MINIEX name
    25AE virtual size
  A16000 virtual address
    2600 size of raw data
  964200 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
62000020 flags
         Code
         Discardable
         (no align specified)
         Execute Read

SECTION HEADER #15
    INIT name
   8AA98 virtual size
  A19000 virtual address
   8AC00 size of raw data
  966800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
62000020 flags
         Code
         Discardable
         (no align specified)
         Execute Read

SECTION HEADER #16
    Pad2 name
  15C000 virtual size
  AA4000 virtual address
       0 size of raw data
       0 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
62000080 flags
         Uninitialized Data
         Discardable
         (no align specified)
         Execute Read

SECTION HEADER #17
   .data name
   FA018 virtual size
  C00000 virtual address
   13000 size of raw data
  9F1400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C8000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Write

SECTION HEADER #18
ALMOSTRO name
   272E0 virtual size
  CFB000 virtual address
    1400 size of raw data
  A04400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C8000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Write

SECTION HEADER #19
CACHEALI name
    92C0 virtual size
  D23000 virtual address
     200 size of raw data
  A05800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C8000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Write

SECTION HEADER #1A
PAGEDATA name
   12150 virtual size
  D2D000 virtual address
    1800 size of raw data
  A05A00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #1B
PAGEVRFD name
   15D00 virtual size
  D40000 virtual address
    8000 size of raw data
  A07200 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #1C
INITDATA name
   17C44 virtual size
  D56000 virtual address
     800 size of raw data
  A0F200 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C2000020 flags
         Code
         Discardable
         (no align specified)
         Read Write

SECTION HEADER #1D
    Pad3 name
   92000 virtual size
  D6E000 virtual address
       0 size of raw data
       0 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C2000080 flags
         Uninitialized Data
         Discardable
         (no align specified)
         Read Write

SECTION HEADER #1E
   CFGRO name
    1CC8 virtual size
  E00000 virtual address
    1E00 size of raw data
  A0FA00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C8000040 flags
         Initialized Data
         Not Paged
         (no align specified)
         Read Write

SECTION HEADER #1F
    Pad4 name
  1FE000 virtual size
  E02000 virtual address
       0 size of raw data
       0 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
CA000080 flags
         Uninitialized Data
         Discardable
         Not Paged
         (no align specified)
         Read Write

SECTION HEADER #20
   .rsrc name
   3B23C virtual size
 1000000 virtual address
   3B400 size of raw data
  A11800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

SECTION HEADER #21
  .reloc name
    9964 virtual size
 103C000 virtual address
    9A00 size of raw data
  A4CC00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

... many more headers

0xfffff806`6e3fc000
fffff806`6e3fc000  00905a4d 00000003 00000004 0000ffff  MZ..............
fffff806`6e3fc010  000000b8 00000000 00000040 00000000  ........@.......
fffff806`6e3fc020  00000000 00000000 00000000 00000000  ................
fffff806`6e3fc030  00000000 00000000 00000000 000000e8  ................
fffff806`6e3fc040  0eba1f0e cd09b400 4c01b821 685421cd  ........!..L.!Th
fffff806`6e3fc050  70207369 72676f72 63206d61 6f6e6e61  is program canno
fffff806`6e3fc060  65622074 6e757220 206e6920 20534f44  t be run in DOS 
fffff806`6e3fc070  65646f6d 0a0d0d2e 00000024 00000000  mode....$.......

File Type: DLL
FILE HEADER VALUES
     14C machine (i386)
       6 number of sections
2AB009D1 time date stamp Thu Sep 10 22:52:01 1992

       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
    2102 characteristics
            Executable
            32 bit word machine
            DLL

OPTIONAL HEADER VALUES
     10B magic #
   14.20 linker version
   1A800 size of code
    4600 size of initialized data
       0 size of uninitialized data
    7370 address of entry point
    1000 base of code
         ----- new -----
0000000076570000 image base
    1000 section alignment
     200 file alignment
       3 subsystem (Windows CUI)
   10.00 operating system version
   10.00 image version
   10.00 subsystem version
   23000 size of image
     400 size of headers
   233EC checksum
0000000000040000 size of stack reserve
0000000000001000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
    4540  DLL characteristics
            Dynamic base
            NX compatible
            No structured exception handler
            Guard
   11D80 [    99D3] address [size] of Export Directory
   1D364 [     154] address [size] of Import Directory
   20000 [     3D8] address [size] of Resource Directory
       0 [       0] address [size] of Exception Directory
   1EE00 [    2690] address [size] of Security Directory
   21000 [    1304] address [size] of Base Relocation Directory
    28E0 [      54] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
    1000 [      AC] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
   1D000 [     360] address [size] of Import Address Table Directory
    E0AC [     320] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory


SECTION HEADER #1
   .text name
   1A753 virtual size
    1000 virtual address
   1A800 size of raw data
     400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read


Debug Directories(3)
    Type       Size     Address  Pointer
    (   96)   60f01       d640f    a340f
    (1342988301)    300b       c1d01    b741d
    (4028183069)c015e017       a2619  10f0114

SECTION HEADER #2
   .data name
     4F4 virtual size
   1C000 virtual address
     200 size of raw data
   1AC00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #3
  .idata name
    1D9A virtual size
   1D000 virtual address
    1E00 size of raw data
   1AE00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #4
  .didat name
     8C4 virtual size
   1F000 virtual address
     A00 size of raw data
   1CC00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #5
   .rsrc name
     3D8 virtual size
   20000 virtual address
     400 size of raw data
   1D600 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #6
  .reloc name
    1304 virtual size
   21000 virtual address
    1400 size of raw data
   1DA00 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

The first one is the header dump for the kernel. Note the valid debug directory. If you want the full output you can get that here.

Some of these headers are less valid than they appear. The last header tells us that the code section starts at an offset of 0x1000 bytes, as is common for PE files. Investigating that memory location yields not code, but ASCII data.

0: kd> db fffff806`6e3f0000+1000
fffff806`6e3f1000  29 0a 2d 2d 0a 0a 50 6f-73 74 20 61 20 6d 65 73  ).--..Post a mes
fffff806`6e3f1010  73 61 67 65 20 74 6f 20-63 6f 6d 70 6c 65 74 69  sage to completi
fffff806`6e3f1020  6f 6e 20 70 6f 72 74 2e-00 00 00 00 00 00 00 00  on port.........
fffff806`6e3f1030  52 65 61 64 46 69 6c 65-28 24 73 65 6c 66 2c 20  ReadFile($self, 
fffff806`6e3f1040  68 61 6e 64 6c 65 2c 20-73 69 7a 65 2c 20 2f 29  handle, size, /)
fffff806`6e3f1050  0a 2d 2d 0a 0a 53 74 61-72 74 20 6f 76 65 72 6c  .--..Start overl
fffff806`6e3f1060  61 70 70 65 64 20 72 65-61 64 2e 00 00 00 00 00  apped read......
fffff806`6e3f1070  4f 76 65 72 6c 61 70 70-65 64 28 65 76 65 6e 74  Overlapped(event

It is possible that these DLLs/drivers were really here at some point but they are gone now and may have been replaced by other data. Regardless, what is left will mess up our page-at-a-time scanback technique to find the base of the kernel.

hal.dll

Another interesting change in the kernel in 20H1+ is that the Hardware Abstraction Layer (HAL) has moved into the kernel image itself and no longer lives inside of hal.dll. If you open up hal.dll in a disassembler, you will notice that it actually does not even have a .text section. It is just a forwarding DLL that forwards exports into the kernel. The forwarding is done to not break backwards compatibility with drivers and components that expect to import HAL functionality from hal.dll and not ntoskrnl.exe.

hal.dll
hal.dll has no code! It does still have the Hal* exports.

Fixing Scanback

Since the new version of the kernel has the .text section starting at 0x200000 we can adjust our scanback to the following algorithm:

const KUSER_SHARED_DATA: usize = 0xFFFFF78000000000;
const KUSER_NT_MAJOR_VERSION_OFFSET: usize = 0x26C;
const KUSER_NT_BUILD_NUMBER_OFFSET: usize = 0x260;
let major_version: *const u32 = (KUSER_SHARED_DATA + KUSER_NT_MAJOR_VERSION_OFFSET) as _;
let build_number: *const u32 = (KUSER_SHARED_DATA + KUSER_NT_BUILD_NUMBER_OFFSET) as _;
let step = if unsafe { *major_version >= 10 && *build_number > 19000 } {
    0x200000
} else {
    0x1000
}
let mut cursor = (leaked_addr as usize & !(step-1)) as *const u16;
unsafe {
    while *cursor != 0x5a4d {
        cursor = cursor.sub(step);
    }
}
let kernel_base = cursor as usize;

Obviously, this code has to be version dependent so we can still use the KUSER_SHARED_DATA version detection method to decide which step amount to use. The algorithm is the same as before, but instead of rounding down to the nearest page and then scanning backward by page size, we use 0x200000. This technique actually also works on 19H1, since the kernel is mapped with large pages (yes entirely RWX in 19H1) and large pages happen to be 0x200000 bytes in size.

Another alternative is to parse each header and try to figure out which one is ntoskrnl.exe. I've tried two alternatives that work: checking the number of sections or looking up the PDB path via the DEBUG data directory.

If Microsoft decides to change the .text section offset or puts unmapped regions between sections this will need to be re-written.

Wrap Up

I hope that this post has been informative! I thought there was going to be more in the solutions section than literature review, but I think this ended up being a good round up of info regardless. It's been something I've wanted to post for a while but finally took the time to write it up properly.

Anyway, have a good day and remember to ask yourself... ~~did you set it to wumbo?~~