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.
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.
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.
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.
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.
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 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).
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.
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.
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
!
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.