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
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.
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
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
DpcRoutine (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.
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.
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
KPRCB to find the
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.
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.
Refer to the next screenshot. Inspecting
TimerEntries 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 itself holds the
LIST_ENTRY at offset 0x20 in the
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
We specify the command to execute using
@$extret is the variable that holds the current object in the list.
In this case we print the address using
? and dump the
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
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.
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.
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
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
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
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.
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.
Reversing this function in IDA we notice a decryption routine before the
DPC is placed in
rsi and used later in a call to
The following screenshot contains annotations from the decryption routine.
As can be seen,
nt!KiWaitAlways and a pointer to the
KTIMER is used with various operations (xor, left rotation and byteswap).
Let’s manually carry out this decryption in the debugger for our target
The following screenshot once again dumps the target
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
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.
Deferred Procedure Calls
A Deferred Procedure Call (DPC) is an object that contains a function that will be deferred at Interrupt Request Level (IRQL)
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.
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
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.
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.