In this two part blog post series we present KTIMER hijacking, a novel post-exploitation technique that delays the execution of kernel-mode payloads. This first part will focus on Windows 11 timer internals and deferred procedure calls and how we can hijack KTIMER and KDCP objects to delay the execution of a function pointer. The second part focusses on implementing these findings in a proof of concept, illustrating the delay in execution of a kernel-mode payload.

Introduction

We assume that an attacker has an arbitrary read/write primitive (ARW) in the kernel, which is usually the first objective in Windows Kernel Exploitation. The next step is often to “upgrade” this ARW to arbitrary code execution. Similarly to getting code execution by e.g. hijacking a kernel mode routine, the presented technique results in arbitrary code execution, but in a delayed manner (and periodic if necessary). This means that a “ticking time bomb” could live in the Windows kernel whilst the kernel exploit process is already terminated, making it harder to detect. Additionally, no Page Table Entries (PTE) have to be modified as the KDPC and KTIMER objects are already writeable, making it abide by the rules that HVCI has set.

We use the most recent Windows 11 22H2 (22621.2134, August 2023) at the time of our research as our target machine with default exploit mitigations in place but disabling HVCI if enabled. This is because HVCI will activate kCFG which will disallow the execution the hijacked DeferredRoutine (this will become more clear later on). We’ll make sure that the rest of the exploit is HVCI compliant, with an eventual kCFG bypass, KTIMER hijacking is also possible on machines that have HVCI enabled.

For this first part, we illustrate a proof of concept by reading and writing kernel data in a debugger, in the second part we implement this in a proof of concept taking all aspects of exploit development into account (stackpivotting, executing kernel mode routines and restoring the execution flow).

As far as I am aware, this research has not been carried out before, making KTIMER hijacking a novel post-exploitation technique.

Let’s start!

Timer Table

Each processor has its own timer table for which it does its own timer expiration routine. These KTIMER_TABLE structures can be requested using the WinDbg !timer command in a (local) kernel debugger which can be seen in the following screenshot. We see the KTIMER_TABLE for processor 0 (yellow) and notice two arrays, one for high precision timers (type 0) and one for the standard timers (type 1). High precision timers are used for applications that require very fast response times, such as multimedia applications. In the table we see the addresses of the KTIMER objects, fire times and the Deferred Procedure Call (DPC) or threads to execute at this fire time. I’ll explain DPCs later, first we explain where these timer tables are stored in the kernel.

KTIMER_TABLE

The Kernel Processor Control Region (KPCR) is a per-processor structure which contains information about the processor. It contains the Kernel Processor Control Block (KPRCB) structure which holds most of what the kernel needs for managing the processor and its resources. It is the KPRCB that contains a pointer to the processor’s KTIMER_TABLE. See following screenshot where we traverse the KPCR and KPRCB to find the KTIMER_TABLE at KPCR+0x180+0x3c00. At offset 0x200 in the KTIMER_TABLE two arrays are defined with each 256 KTIMER_TABLE_ENTRY elements. The first array corresponds to the high precision timers, and the second with the standard timers.

@$pcr->Prcb.TimerTable.TimerEntries

Timer Entries

Let’s take a step back to the output of the !timer command. In the table, three interesting timers are always present, namely the timer that checks for Daylight Savings Time time-zone changes, the timer that processes the passing of the year and the timer that processes the passing of the century as can be seen in the next screenshot. We’ll use this distinct timer (green) that processes the DPC (orange) calling the nt!ExpCenturyDpcRoutine (pink), it should be present at index 73 in the array for standard timers.

ExpCenturyDpcRoutine timer entry

Refer to the next screenshot. Inspecting TimerEntries[1][73] we notice that the KTIMER_TABLE_ENTRY holds a linked list at offset 0x8 s.t. it can hold multiple timers represented by the KTIMER structure. The KTIMER itself holds the LIST_ENTRY at offset 0x20 in the TimerListEntry member. We can use the WinDbg command !list to display the linked list and dump the KTIMER objects in the list. For this, we need to specify the name of the datastructure holding the forward link using -t, namely nt!_KTIMER.TimerListEntry.Flink. We specify the command to execute using -x, here @$extret is the variable that holds the current object in the list. In this case we print the address using ? and dump the KTIMER object. Next, we specify the address of the first KTIMER object, this is the Flink from the KTIMER_TABLE_ENTRY (cyan) minus 0x20, the offset of the LIST_ENTRY in the KTIMER structure. Notice that we indeed located the KTIMER object for the DPC routine handling the passing of the century (green), but the pointer to the DPC (red) is not the same as we previously identified (it should be orange). In fact, the pointer doesn’t even point to valid memory. We need to figure out how the kernel uses this value to find the actual DPC. This can be achieved with an access breakpoint, to explain this we first need to explain how timers expire.

ExpCenturyDpcRoutine timer entry

Timer Expiration

As soon as timers are expired, the clock Interrupt Service Routine (ISR) sets the TimerRequest flag in the KPRCB so that the DPC draining mechanism knows DPCs are in queue to be executed. The DueTime in the KTIMER objects is the InterruptTime at which the corresponding KDPC.DeferredRoutine wants to be executed. We can calculate what exact date and time this is by subtracting the current InterruptTime from the SystemTime and adding the DueTime. Both the interrupt time and system time are present in the KUSER_SHARED_DATA, an object that the kernel places at a pre-set address for sharing with user-mode. For our build, KUSER_SHARED_DATA is still located at 0xfffff78000000000. See the following screenshot. Note that we have set both our debuggee as well as our debugger to UTC to stay away from conversions (TimeZoneBias is 0). We subtract the interrupt time from the system time and add the DueTime from our target KTIMER (green). The value is passed to .formats to see that the time is indeed at the passing of the century. Now we can figure out how the DPC in the KTIMER object is decrypted.

InterruptTime, SystemTime and DueTime

Reversing KTIMER DPCs

In order to figure out how the kernel decrypts the KTIMER.Dpc we need to know when it accesses this value. We set an access breakpoint ba of an 8-byte read r8 on a KTIMER object (blue, that is about to expire) at offset 0x30, this is the encrypted DPC. Continuing execution, we break at nt!KiTimerWaitTest+0x1d, which is actually one instruction further than where the read occurred. Disassembling backwards we see that the instruction at nt+0x345909 was responsible for the dereference.

Setting an access breakpoint on the encrypted DPC

Reversing this function in IDA we notice a decryption routine before the DPC is placed in rsi and used later in a call to KiInsertQueueDpc. The following screenshot contains annotations from the decryption routine. As can be seen, nt!KiWaitNever, nt!KiWaitAlways and a pointer to the KTIMER is used with various operations (xor, left rotation and byteswap).

Reversing the decryption algorithm of the DPC

Let’s manually carry out this decryption in the debugger for our target KTIMER. The following screenshot once again dumps the target KTIMER. We list the values that are used in the decryption, nt!KiWaitNever (light pink) and nt!KiWaitAlways (light red). First, we xor the encrypted DPC (red) with nt!KiWaitNever (light pink). Next, we rotate left with 0x7b bytes with a gnarly looking expression, before xor’ing the result with the address of the KTIMER. We manually do the byteswap and xor that with nt!KiWaitAlways (light red). The result is the DPC that we initially found in the timer table (orange)!

Now, let’s focus some more on DPCs before trying to hijack and delay the execution flow.

Decrypting the DPC

Deferred Procedure Calls

A Deferred Procedure Call (DPC) is an object that contains a function that will be deferred at Interrupt Request Level (IRQL) DISPATCH_LEVEL/DPC_LEVEL (level 2). These IRQLs define the hardware priority at which a processor operates at any given time. For example, the CLOCK_LEVEL is one of the highest because it needs to handle processor ticks. So, when the IRQL level drops to level 2, DPCs in the DPC queue will be executed, emptying the queue as it proceeds. Because the DPCs are deferred, they may not execute at the exact DueTime, the processor first has to execute all higher level interrupts. Microsoft mentions that the members of the KDPC structure may not be set directly. What if we do set them directly? What if we could set the DeferredRoutine to any function pointer and set the corresponding KTIMER.DueTime to a specific InterruptTime of our choosing? If possible we could be able to delay the execution of kernel mode payloads.

Proof of Concept

We have shown how KTIMER objects can be traversed from the KPCR and how the encrypted DPC values can be decrypted. We have shown how the timers are processed and at which DueTime the DPCs fire. Also, both the KTIMER objects and and KDPC objects are writeable from the kernel as can be seen from the following screenshot. All that’s left is trying it out in the debugger.

PTE for KTIMER and KDPC

In the following screenshot we illustrate that KTIMER hijacking is feasible as a way to execute arbitrary function pointers. Although we manually modify values in the debugger, these values should be modifiable by a kernel exploit having an arbitrary read/write. We change the KDPC.DeferredRoutine to a bogus nop; ret; gadget before changing the KTIMER.DueTime to the current value of the KUSER_SHARED_DATA.InterruptTime s.t. it is directly placed in the DPC queue. Of course, the DueTime can be any value representing a date and time. We set a breakpoint on the bogus gadget before continuing execution. When the IRQL drops to DISPATCH_LEVEL the breakpoint is hit and we can execute the nop!

KTIMER hijacking proof of concept

A Note on PatchGuard

PatchGuard is used by Windows to monitor the system for changes in the kernel, by the kernel. Because of this, PatchGuard is uniquely undocumented and implemented very vaguely and obscure as attackers may disable it when they come to know the internals.

In the proof of concept we did not have to deal with PatchGuard as it is disabled when enabling (local) kernel debugging. That said, PatchGuard DPCs are also triggered using timers, so it would be interesting to see whether PatchGuard actually monitors the timer table entries.

Thanks

In the second part of the series we will implement the results of this research in a proof of concept, taking all aspects of exploit development into account. We’ll show how to bring your own vulnerable driver (BYOVD), use an arbitrary read/write to get a specific KTIMER, decrypt the KTIMER.Dpc, do the KTIMER hijack and execute an arbitrary kernel API. We also test our proof of concept against a build with PatchGuard enabled, running it for a longer time to see if we can draw some conclusions.

I’d like to thank Yarden Shafir (@yarden_shafir) for pointing out this research topic during her Windows Internals course. If you like a crash course in Windows Internals it is worth checking out.

Thanks for taking the time to read this post. If you have any questions or remarks, reach out to me on X or Discord.