
人是讨厌等待的动物,哪怕硬件响应慢个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结构的一个成员,其在不同操作系统下的位置如下:
x86 | x64 | |
Windows 7 | 0x1960 | 0x2200 |
Windows 8 | 0x2260 | 0x2e00 |
Windows 8.1 | 0x2260 | 0x2e00 |
Windows 10 | 0x2260 | 0x3600 |
Windows 11 | - | 0x3C00 |
为了方便下文描述,假设定义如下数据结构,具体使用时需要根据系统版本修正偏移:
- typedef struct _KPRCB
- {
- #ifdef _WIN64
- UCHAR Reserved1[上表x64的各系统偏移];
- #else
- UCHAR Reserved1[上表x86的各系统偏移];
- #endif
- KTIMER_TABLE TimerTable;
- } KPRCB, *PKPRCB;
而KTIMER_TABLE则为我们关注的和DPC相关的数据结构。
KTIMER_TABLE结构
微软一天到晚尽整妖蛾子……
Windows 11的此结构和Windows 7至Windows 10的不同,主要区别是:
- TimerEntries成员由一维数组变成了二维数组。
- Windows 11下多了个KTIMER_TABLE_STATE类型的成员TableState
因此,有定义如下:
- #define TIMER_TABLE_MAX 256
- typedef struct _KTIMER_TABLE_STATE
- {
- UINT8 LastTimerExpiration[2];
- UINT8 LastTimerHand;
- } KTIMER_TABLE_STATE, *PKTIMER_TABLE_STATE;
- typedef struct _KTIMER_TABLE_ENTRY
- {
- ULONG_PTR Lock;
- LIST_ENTRY Entry;
- ULARGE_INTEGER Time;
- } KTIMER_TABLE_ENTRY, *PKTIMER_TABLE_ENTRY;
- typedef struct _KTIMER_TABLE_WINDOWS7
- {
- #ifdef _WIN64
- KTIMER *TimerExpiry[64];
- #else
- KTIMER *TimerExpiry[16];
- #endif
- KTIMER_TABLE_ENTRY TimerEntries[TIMER_TABLE_MAX];
- } KTIMER_TABLE_WINDOWS7, *PKTIMER_TABLE_WINDOWS7;
- typedef struct _KTIMER_TABLE_WINDOWS11
- {
- KTIMER *TimerExpiry[64];
- KTIMER_TABLE_ENTRY TimerEntries[2][TIMER_TABLE_MAX];
- KTIMER_TABLE_STATE TableState;
- } KTIMER_TABLE_WINDOWS11, *PKTIMER_TABLE_WINDOWS11;
还好KTIMER_TABLE_ENTRY结构是没有改变的,不然又得适配一层……
KTIMER结构
在可以遍历TimerEntries后,还有最后一步,将KTIMER_TABLE_ENTRY与KTIMER建立联系,其实就是如下关系:

对应C语言的遍历如下:
- ULONG i = 0, j = 0;
- ULONG NumberOfProcessor = KeQueryMaximumProcessorCount();
- for (i = 0; i < NumberOfProcessor; ++i)
- {
- PKPRCB Prcb = _GetPRCBEachProcessor(i);
- PKTIMER_TABLE TimerTable = Prcb->TimerTable;
- for (j = 0; j < TIMER_TABLE_MAX; ++j)
- {
- PKTIMER_TABLE_ENTRY Node = (PUCHAR)(TimerTable->TimerEntries) + j * sizeof(KTIMER_TABLE_ENTRY);
- PLIST_ENTRY Work = Node->Entry.Blink;
- for (Work = Node->Entry.Blink; Work != &Node->Entry.Blink; Work = Work->Blink)
- {
- PKTIMER Timer = CONTAINING_RECORD(Work, KTIMER, TimerListEntry);
- }
- }
- }
注意:上述代码只是Windows 7至Windows 10的,不适合Windows 11。因为Windows 11的TimerEntries是一个二维数组(见上文结构定义),因此Windows 11的遍历代码要考虑到此问题。
DPC结构的加密问题
微软一天到晚尽整妖蛾子……
不知什么原因,从x64开始微软对KTIMER结构中的Dpc成员指向的DeferredRoutine函数地址进行了加密,以下代码截取自Windows 10 x64:
- __int64 __fastcall KiInsertQueueDpc(_KDPC *Dpc, __int64 a2, __int64 a3, volatile signed __int32 *a4, char a5)
- {
- ……
- v8 = 0;
- v10 = (DWORD1(PerfGlobalGroupMask) & 0x40000) != 0;
- ……
- if ( _InterlockedCompareExchange64((volatile signed __int64 *)&Dpc->DpcData, IsrDpcStats, 0i64) )
- {
- DpcQueueDepth = 0;
- }
- else
- {
- ……
- v8 = 1;
- ……
- }
- ……
- if ( v8 )
- {
- if ( v10 )
- {
- EtwTraceDpcEnqueueEvent(
- 0xF3DD7277
- * (KiWaitNever ^ __ROR8__(
- (unsigned __int64)Dpc->DeferredRoutine ^ _byteswap_uint64((unsigned __int64)Dpc ^ KiWaitAlways),
- KiWaitNever)),
- Dpc->DeferredRoutine,
- DpcQueueDepth,
- DpcCount,
- v15,
- Dpc->Importance);
- v15 = v31;
- }
- ……
- }
- ……
- }
可以看到,在开启了日志追踪的情况下EtwTraceDpcEnqueueEvent会对Dpc->DeferredRoutine进行解密,其解密算法等价于如下C代码:
- PKDPC Dpc = NULL;
- ULONG_PTR ULongDpc = 0;
- ULongDpc = (ULONG_PTR)Timer->Dpc;
- ULongDpc ^= *(PULONG_PTR)KiWaitNever;
- ULongDpc = _rotl64(ULongDpc, *(PUCHAR)KiWaitNever);
- ULongDpc ^= (ULONG_PTR)Timer;
- ULongDpc = _byteswap_uint64(ULongDpc);
- ULongDpc ^= *(PULONG_PTR)KiWaitAlways;
- if (MmIsAddressValid(ULongDpc))
- {
- Dpc = ULongDpc;
- }
上文中的KiWaitNever和KiWaitAlways是内核中的全局变量,可以通过观察KeSetTimerEx函数得到,本文不再赘述。
成果
为了证明上述结果正确,验证一下:

打完收工~