Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

Traverse the DPC timer in the Windows system

2022-12-17 @ UTC+0

Humans are creatures that hate to wait. Even if the hardware response is slow by 100 milliseconds, human blood pressure may rise linearly. As a technology that appeared with the CPU in ancient times, the interrupt carries the response actions of various hardware in the system. Therefore, the response speed of the interrupt request has become a matter of “life and death” for the operating system - the interrupt handler should be as fast as possible, preferably not to let people feel waiting. Imagine the feeling of pressing a key and only seeing feedback on the screen after 1s, it definitely won’t be a good experience.

DPC Timer

Although it is envisaged that the interrupt request handler (ISR, Interrupt Service Routines) should be as fast as possible, it is inevitable to perform some time-consuming operations in the actual practice process. At this time, the conflict occurs. Fortunately, time-consuming events can generally be “delayed” for processing. At this time, the DPC (Deferred Procedure Calls) timer was born. The ISR can add a callback function to the DPC queue when needed. When the system switches from DIRQL to PASSIVE_LEVEL, the system will execute the callback function inserted into the DPC queue by the ISR, thereby completing some time-consuming operations (reference 1).

This article only explains a few key data structures of the DPC timer, and this article only contains a few key codes. If you want to traverse all DPC timers, there is some extra workload, and the code is not given in this article.

APC Queue

By the way, corresponding to DPC, there is also an APC (Asynchronous Procedure Calls) queue. This queue is thread-related, and there are two types of kernel-mode APC and user-mode APC. A thread must be in an Alertable State to execute the APC queue. The most famous “thread” is the Idle thread. Although its name is the idle thread (the operating system does nothing when it does a loop in this thread, so it often sees this thread occupying 99% of the CPU), it actually does some non-idle things. One of them is to execute the functions in the APC queue. This mechanism is often targeted by viruses to do some bad things. We will elaborate on this when we encounter suitable samples later.

KPRCB Structure

The header of the DPC queue is located in the TimerTable. The TimerTable is a member of the KPRCB structure. Its position under different operating systems is as follows:

Windows 70x19600x2200
Windows 80x22600x2e00
Windows 8.10x2260 0x2e00
Windows 100x22600x3600
Windows 11-0x3C00

For the convenience of the following description, it is assumed that the following data structure is defined, and the offset needs to be corrected according to the system version when used:

  1. typedef struct _KPRCB  
  2. {  
  3. #ifdef _WIN64  
  4.     UCHAR Reserved1[x64_offset_in_above_table];  
  5. #else  
  6.     UCHAR Reserved1[x86_offset_in_above_table];  
  7. #endif  
  8.     KTIMER_TABLE TimerTable;  
  9. } KPRCB, *PKPRCB;  

And KTIMER_TABLE is the data structure we are concerned about and related to DPC.


Microsoft is always doing weird things…

The structure of Windows 11 is different from that of Windows 7 to Windows 10. The main differences are: 

  1. The TimerEntries member has changed from a one-dimensional array to a two-dimensional array.
  2. Windows 11 has added a KTIMER_TABLE_STATE type member TableState

Therefore, it is defined as follows:

  1. #define TIMER_TABLE_MAX 256  
  3. typedef struct _KTIMER_TABLE_STATE  
  4. {  
  5.     UINT8 LastTimerExpiration[2];  
  6.     UINT8 LastTimerHand;  
  9. typedef struct _KTIMER_TABLE_ENTRY  
  10. {  
  11.     ULONG_PTR Lock;   
  12.     LIST_ENTRY Entry;   
  13.     ULARGE_INTEGER Time;   
  16. typedef struct _KTIMER_TABLE_WINDOWS7  
  17. {  
  18. #ifdef _WIN64  
  19.     KTIMER *TimerExpiry[64];   
  20. #else  
  21.     KTIMER *TimerExpiry[16];   
  22. #endif  
  23.     KTIMER_TABLE_ENTRY TimerEntries[TIMER_TABLE_MAX];   
  26. typedef struct _KTIMER_TABLE_WINDOWS11  
  27. {  
  28.     KTIMER *TimerExpiry[64];  
  29.     KTIMER_TABLE_ENTRY TimerEntries[2][TIMER_TABLE_MAX];  
  30.     KTIMER_TABLE_STATE TableState;  

Fortunately, the KTIMER_TABLE_ENTRY structure has not changed, otherwise another layer of adaptation will be needed…

KTIMER Structure

After being able to traverse TimerEntries, there is one last step, to establish a connection between KTIMER_TABLE_ENTRY and KTIMER, which is actually the following relationship:

The corresponding C language traversal is as follows:

  1. ULONG i = 0, j = 0;  
  2. ULONG NumberOfProcessor = KeQueryMaximumProcessorCount();  
  4. for (i = 0; i < NumberOfProcessor; ++i)  
  5. {  
  6.     PKPRCB Prcb = _GetPRCBEachProcessor(i);  
  7.     PKTIMER_TABLE TimerTable = Prcb->TimerTable;  
  9.     for (j = 0; j < TIMER_TABLE_MAX; ++j)  
  10.     {  
  11.         PKTIMER_TABLE_ENTRY Node = (PUCHAR)(TimerTable->TimerEntries) + j * sizeof(KTIMER_TABLE_ENTRY);  
  12.         PLIST_ENTRY Work = Node->Entry.Blink;  
  14.         for (Work = Node->Entry.Blink; Work != &Node->Entry.Blink; Work = Work->Blink)  
  15.         {  
  16.             PKTIMER Timer = CONTAINING_RECORD(Work, KTIMER, TimerListEntry);  
  17.         }  
  18.     }  
  19. }  

Note: The above code is only for Windows 7 to Windows 10, not suitable for Windows 11. Because the TimerEntries of Windows 11 is a two-dimensional array (see the structure definition in the above text), the traversal code of Windows 11 needs to consider this issue.

Encryption Problem of DPC

Structure Microsoft is always doing weird things… 

For some reason, starting from x64, Microsoft encrypted the DeferredRoutine function address pointed to by the Dpc member of the KTIMER structure. The following code is taken from Windows 10 x64:

  1. __int64 __fastcall KiInsertQueueDpc(_KDPC *Dpc, __int64 a2, __int64 a3, volatile signed __int32 *a4, char a5)  
  2. {  
  3.   …… 
  4.   v8 = 0;  
  5.   v10 = (DWORD1(PerfGlobalGroupMask) & 0x40000) != 0;  
  6.   ……  
  8.   if ( _InterlockedCompareExchange64((volatile signed __int64 *)&Dpc->DpcData, IsrDpcStats, 0i64) )  
  9.   {  
  10.     DpcQueueDepth = 0;  
  11.   }  
  12.   else  
  13.   {  
  14.     ……  
  15.     v8 = 1;  
  16.     ……  
  17.   }  
  18.   ……  
  19.   if ( v8 )  
  20.   {  
  21.     if ( v10 )  
  22.     {  
  23.       EtwTraceDpcEnqueueEvent(  
  24.         0xF3DD7277  
  25.       * (KiWaitNever ^ __ROR8__(  
  26.                          (unsigned __int64)Dpc->DeferredRoutine ^ _byteswap_uint64((unsigned __int64)Dpc ^ KiWaitAlways),  
  27.                          KiWaitNever)),  
  28.         Dpc->DeferredRoutine,  
  29.         DpcQueueDepth,  
  30.         DpcCount,  
  31.         v15,  
  32.         Dpc->Importance);  
  33.       v15 = v31;  
  34.     }  
  35.     ……  
  36.   }  
  37.   ……  
  38. }  

It can be seen that when log tracking is turned on, EtwTraceDpcEnqueueEvent will decrypt Dpc->DeferredRoutine, and its decryption algorithm is equivalent to the following C code:

  1. PKDPC Dpc = NULL;  
  2. ULONG_PTR ULongDpc = 0;  
  3. ULongDpc  = (ULONG_PTR)Timer->Dpc;  
  4. ULongDpc ^= *(PULONG_PTR)KiWaitNever;  
  5. ULongDpc  = _rotl64(ULongDpc, *(PUCHAR)KiWaitNever);  
  6. ULongDpc ^= (ULONG_PTR)Timer;  
  7. ULongDpc  = _byteswap_uint64(ULongDpc);  
  8. ULongDpc ^= *(PULONG_PTR)KiWaitAlways;  
  9. if (MmIsAddressValid(ULongDpc))  
  10. {  
  11.     Dpc   = ULongDpc;  
  12. }  

The KiWaitNever and KiWaitAlways in the above text are global variables in the kernel, which can be obtained by observing the KeSetTimerEx function. This article will not elaborate.


To prove that the above results are correct, verify it:

End of work~


  1. Introduction to DPC Objects

  • DPC Timer
  • APC Queue
  • KPRCB Structure
  • KTIMER_TABLE Structure
  • KTIMER Structure
  • Encryption Problem of DPC
  • Results
  • References
  • CopyRight (c) 2020 - 2025

    Designed by Hacksign