驱动开发:内核枚举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工具对比是一致的。