Watch & Learn

Debugwar Blog

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

遍历Windows系统DPC定时器

2022-12-17 14:04:21

人是讨厌等待的动物,哪怕硬件响应慢个100毫秒,人类的血压都有可能会直线上升。而中断作为上古时期便随CPU一起出现的技术,承载着系统各种硬件本身的响应动作,因此中断请求的响应速度变成了操作系统“性命攸关”的东西——中断处理程序要尽可能的快,最好不要让人有等待的感觉,想象一下按下一个按键1s中后才会在屏幕上看到反馈的感觉,一定不会是什么好的体验。

DPC定时器

虽然设想上中断请求处理程序(ISR, Interrupt Service Routines)应该尽可能的快,但在实际实践过程中难免会进行一些耗时的操作,那么这个时候冲突便发生了,好在耗时的事件一般都可以”延迟“处理,这时候DPC(Deferred Procedure Calls)定时器便应运而生了。ISR可以在需要时向DPC队列中加入一个回调函数,在系统从DIRQL向PASSIVE_LEVEL切换时,系统会执行ISR插入到DPC队列中的回调函数,从而完成一些耗时操作参考1

本文仅对DPC定时器的几个关键数据结构进行了说明,且本文只包含几处关键代码,想要遍历所有的DPC定时器还有一些额外的工作量,其代码本文并未给出。

APC队列

这里顺带一提,和DPC相对应的还有一个APC(Asynchronous Procedure Calls)队列,这个队列是线程相关的,而且有内核态APC和用户态APC两种。一个线程必须是Alertable State时才可以执行APC队列,比较著名的”线程“是Idle线程,虽然他的名字叫空闲线程(操作系统无任务时在此线程里面做循环,因此经常看到此线程占用99%的CPU)但其实还是会做一些非空闲的事情的,执行APC队列中的函数便是其中之一,这一机制经常被病毒盯上干一些坏事,后面我们遇到合适的样本再展开细说。

KPRCB结构

DPC队列的表头位于TimerTable中,TimerTable为KPRCB结构的一个成员,其在不同操作系统下的位置如下:

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

为了方便下文描述,假设定义如下数据结构,具体使用时需要根据系统版本修正偏移:

  1. typedef struct _KPRCB  
  2. {  
  3. #ifdef _WIN64  
  4.     UCHAR Reserved1[上表x64的各系统偏移];  
  5. #else  
  6.     UCHAR Reserved1[上表x86的各系统偏移];  
  7. #endif  
  8.     KTIMER_TABLE TimerTable;  
  9. } KPRCB, *PKPRCB;  

而KTIMER_TABLE则为我们关注的和DPC相关的数据结构。

KTIMER_TABLE结构

微软一天到晚尽整妖蛾子……
Windows 11的此结构和Windows 7至Windows 10的不同,主要区别是:
  1. TimerEntries成员由一维数组变成了二维数组。
  2. Windows 11下多了个KTIMER_TABLE_STATE类型的成员TableState
因此,有定义如下:

  1. #define TIMER_TABLE_MAX 256  
  2.   
  3. typedef struct _KTIMER_TABLE_STATE  
  4. {  
  5.     UINT8 LastTimerExpiration[2];  
  6.     UINT8 LastTimerHand;  
  7. } KTIMER_TABLE_STATE, *PKTIMER_TABLE_STATE;  
  8.   
  9. typedef struct _KTIMER_TABLE_ENTRY  
  10. {  
  11.     ULONG_PTR Lock;   
  12.     LIST_ENTRY Entry;   
  13.     ULARGE_INTEGER Time;   
  14. } KTIMER_TABLE_ENTRY, *PKTIMER_TABLE_ENTRY;  
  15.   
  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];   
  24. } KTIMER_TABLE_WINDOWS7, *PKTIMER_TABLE_WINDOWS7;  
  25.   
  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;  
  31. } KTIMER_TABLE_WINDOWS11, *PKTIMER_TABLE_WINDOWS11;  

还好KTIMER_TABLE_ENTRY结构是没有改变的,不然又得适配一层……

KTIMER结构

在可以遍历TimerEntries后,还有最后一步,将KTIMER_TABLE_ENTRY与KTIMER建立联系,其实就是如下关系:


对应C语言的遍历如下:

  1. ULONG i = 0, j = 0;  
  2. ULONG NumberOfProcessor = KeQueryMaximumProcessorCount();  
  3.   
  4. for (i = 0; i < NumberOfProcessor; ++i)  
  5. {  
  6.     PKPRCB Prcb = _GetPRCBEachProcessor(i);  
  7.     PKTIMER_TABLE TimerTable = Prcb->TimerTable;  
  8.       
  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;  
  13.   
  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. }  

注意:上述代码只是Windows 7至Windows 10的,不适合Windows 11。因为Windows 11的TimerEntries是一个二维数组(见上文结构定义),因此Windows 11的遍历代码要考虑到此问题。

DPC结构的加密问题

微软一天到晚尽整妖蛾子……
不知什么原因,从x64开始微软对KTIMER结构中的Dpc成员指向的DeferredRoutine函数地址进行了加密,以下代码截取自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.   ……  
  7.     
  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. }  

可以看到,在开启了日志追踪的情况下EtwTraceDpcEnqueueEvent会对Dpc->DeferredRoutine进行解密,其解密算法等价于如下C代码:

  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. }  

上文中的KiWaitNever和KiWaitAlways是内核中的全局变量,可以通过观察KeSetTimerEx函数得到,本文不再赘述。

成果

为了证明上述结果正确,验证一下:


打完收工~

参考

  1. Introduction to DPC Objects

Catalog
  • DPC定时器
  • APC队列
  • KPRCB结构
  • KTIMER_TABLE结构
  • KTIMER结构
  • DPC结构的加密问题
  • 成果
  • 参考
  • CopyRight(c) 2020 - 2025 Debugwar.com

    Designed by Hacksign