驱动开发:内核枚举DpcTimer定时器
在笔者上一篇文章《内核枚举IoTimer定时器》中我们通过IoInitializeTimer这个API函数为跳板,向下扫描特征码获取到了IopTimerQueueHead也就是IO定时器的队列头,本章学习的枚举DPC定时器依然使用特征码扫描,唯一不同的是在新版系统中DPC是被异或加密的,想要找到正确的地址,只是需要在找到DPC表头时进行解密操作即可。

DPC定时器的作用是什么?
DPC(Deferred Procedure Call)是一种异步执行的机制。它允许内核代码在不中断当前进程的情况下,延迟执行一些工作。DPC的执行是由内核定时器触发的。内核定时器是一种特殊的内核对象,用于定时执行某个特定的操作。在DPC的上下文中,内核可以安全地访问任何内核数据结构,而不会引起死锁或其他问题。
在内核中可以使用DPC定时器设置任意定时任务,当到达某个节点时自动触发定时回调,定时器的内部使用KTIMER对象,当设置任务时会自动插入到DPC队列,由操作系统循环读取DPC队列并执行任务,枚举DPC定时器可得知系统中存在的DPC任务。
要想在新版系统中得到DPC定时器则需要执行的步骤有哪些?
- 1.找到
KiProcessorBlock地址并解析成_KPRCB结构 - 2.在
_KPRCB结构中得到_KTIMER_TABLE偏移 - 3.解析
_KTIMER_TABLE_ENTRY得到加密后的双向链表 
首先_KPRCB这个结构体与CPU内核对应,获取方式可通过一个未导出的变量nt!KiProcessorBlock来得到,如下双核电脑,结构体存在两个与之对应的结构地址。
lyshark.com 0: kd> dq nt!KiProcessorBlock  | 
此KiProcessorBlock是一个数组,其第一个结构体TimerTable则是结构体的偏移。
lyshark.com 0: kd> dt _KPRCB fffff807`6f77c180  | 
接下来是把所有的KTIMER都枚举出来,KTIMER在TimerTable中的存储方式是数组+双向链表。
lyshark.com 0: kd> dt _KTIMER_TABLE  | 
到了_KTIMER_TABLE_ENTRY这里,Entry开始的双向链表,每一个元素都对应一个Timer也就是说我们已经可以遍历所有未解密的Time变量了。
lyshark.com 0: kd> dt _KTIMER_TABLE_ENTRY 0xfffff807`6f77c180 + 0x3680  | 
至于如何解密,我们需要得到加密位置,如下通过KeSetTimer找到KeSetTimerEx从中得到DCP加密流程。
lyshark.com 0: kd> u nt!KeSetTimer  | 
如上汇编代码KiSetTimerEx中就是DPC加密细节,如果需要解密只需要逆操作即可,此处我就具体分析下加密细节,分析这个东西我建议你使用记事本带着色的。
分析思路是这样的,首先这里要传入待加密的DPC数据,然后经过KiWaitNever和KiWaitAlways对数据进行xor,ror,bswap等操作。

将如上所示的汇编解密流程通过C语言的方式实现,解密函数DPC_Print过程可以被总结为如下几个流程:
首先获取定时器结构体中
Dpc成员的地址,将其转换为ULONG_PTR类型的指针ptrDpc。然后将
ptrDpc异或上一个常量p2dq(ptrKiWaitNever),这个常量是KiWaitNever的指针地址强制转换为ULONG_PTR类型后的结果,相当于异或上一个随机值来进行简单的加密。然后将
ptrDpc循环左移nShift位,其中nShift的值为KiWaitNever指针值的低8位,即取最后一个字节。接着将
ptrDpc异或上定时器结构体的地址,相当于对加密结果进行一个简单的混淆。然后对ptrDpc进行字节交换,相当于将ptrDpc的字节序进行翻转,以便在后面的代码中能够正确地解密DPC结构体。最后将ptrDpc异或上一个常量p2dq(ptrKiWaitAlways),这个常量是KiWaitAlways的指针地址强制转换为ULONG_PTR类型后的结果,相当于再进行一次简单的加密。
最后,如果解密得到的DPC结构体指针DecDpc是一个有效的内核地址,就输出该DPC的地址和它的延迟函数地址。其中DeferredRoutine是KDPC结构体的一个成员,用于保存DPC的回调函数地址,将上述流程通过代码方式实现则如下所示;
  | 
接着将这些功能通过代码实现,首先得到我们需要的函数地址,这些地址包括。
ULONG_PTR ptrKiProcessorBlock = 0xfffff80770a32cc0;  | 
此处我把它分为三步走,第一步找到KiProcessorBlock函数地址,第二步找到KeSetTimer并从里面寻找KeSetTimerEx,第三步根据KiSetTimerEx地址,搜索到KiWaitNever(),KiWaitAlways()这两个函数内存地址,最终循环链表并解密DPC队列。
寻找KiProcessorBlock函数
找到KiProcessorBlock函数地址,该地址可通过__readmsr()寄存器相加偏移得到。
在WinDBG中可以输入rdmsr c0000082得到MSR地址。

MSR寄存器使用代码获取也是很容易,只要找到MSR地址在加上0x20即可得到KiProcessorBlock的地址了。
/*  | 
运行后即可得到输出效果如下:

寻找KeSetTimerEx函数
找到KeSetTimer从里面搜索特征得到call KeSetTimerEx函数地址,还记得《内核枚举IoTimer定时器》中我们采用的特征码定位方式吗,没错本次还要使用这个方法,我们此处需要搜索到e80c000000这段特征。
/*  | 
输出寻找CALL地址效果图如下:

寻找KiWaitNever函数
这一步也是最重要的,在KiSetTimerEx里面,搜索特征,拿到里面的KiWaitNever(),KiWaitAlways()这两个函数地址。
- 488b05850c5100 KiWaitNever
 - 488b356b0e5100 KiWaitAlways
 
这个过程需要重复搜索,所以要把第一步和第二部过程归纳起来,具体代码如下所示。
/*  | 
运行这个程序,我们看下寻找到的地址是否与WinDBG中找到的地址一致。

实现枚举DPC定时器
最后将这些功能整合在一起,循环输出链表元素,并解密元素即可实现枚举当前系统DPC定时器。
代码核心API分析:
- KeNumberProcessors 得到CPU数量(内核常量)
 - KeSetSystemAffinityThread 线程绑定到特定CPU上
 - GetKiProcessorBlock 获得KPRCB的地址
 - KeRevertToUserAffinityThread 取消绑定CPU
 
解密部分提取出KiWaitNever和KiWaitAlways用于解密计算,转换PKDPC对象结构,并输出即可。
  | 
最终运行枚举程序,你将会看到系统中所有的定时器,与ARK工具对比是一致的。
