目的
拿到类似于 PChunter 中的信息:
过程
首先,要拿到系统的 _KPRCB ,也就是 KiProcessorBlock 的地址:
我这里显示了 4 地址,这是因为我的 CPU 是 4 核 ——
(垃圾电脑勿喷)
对于 KiProcessorBlock 这个 API , Windows 没有导出:
但是可以通过另一种办法获取到这个地址:
// 获取特殊寄存器中的值,拿 _KPRCB
ULONG64 PrcbAddress = (ULONG64)__readmsr(0xC0000101) + 0x20;
// PrcbAddress 是一个地址,这个地址存放了某个 CPU 的 _KPRCB 的地址
DbgPrint("[LYSM] *(ULONG_PTR*)PrcbAddress = %p\n", *(ULONG_PTR*)PrcbAddress);
因为当前 CPU 不一定运行在哪一个核上,所以我们用这个指令获取到的值可能是 fffff80396987180 也可能是 ffff80009eba0180。
如果想要获取指定核心的 _KPRCB,则需要使用以下代码将当前线程绑定在指定的 CPU 上,具体方法之后再讲,这里先跳过。
获取到 _KPRCB 地址后,看一下它的结构:
lkd> dt _KPRCB fffff803`96987180
在 +0x3680 偏移处有一个成员 TimerTable(这个偏移在不同系统是不同的,win10是 0x3680,win7是 0x2200,其他的请自行查阅)
这是一个 _KTIMER_TABLE 结构,继续查看:
dt _KTIMER_TABLE 0xfffff80396987180+0x3680
在 +0x200 处有一个 _KTIMER_TABLE_ENTRY 类型的成员 TimerEntries[256]
dt _KTIMER_TABLE_ENTRY 0xfffff80396987180+0x3680+0x200
这是一个数组,大小为 0x20(其实就是两个指针加上一个结构体,这个结构体内也是两个指针,一共4个指针,4*8=32=0x20)
继续观察 ,+0x08 处有一个 _LIST_ENTRY 类型的成员 Entry
再观察 Entry 的结构,它是一个双向链表:
dt _LIST_ENTRY 0xfffff80396987180+0x3680+0x200+0x08
那么它的数据结构我们大致清楚了,是对象数组+双向链表,由于我刚才重启了一次,现在的 _KPRCB 里的值已经变成了 0xfffff80109fde180:
按之前的计算方式,来到 0xfffff80109fde180+0x3680+0x200,来到对象数组的头部:
① 其实就是对象数组中的 Lock,② 和 ③ 分别是双向链表的 Flink 和 Blink ,④ 是 对象数组中的 Time 。
Lock 恒等于 0,Time 是一个动态变化的值,这些都不需要关心。只需要关心 ② 即可:
(因为内存里的值变化较快,所以请参考新图中的数据)
(1)比如第一个 Entry 的 FLink 指向了它本身,说明这个双向链表是空的,可以直接 pass 掉:
(2)往下找,找到一个不指向自己的 Fink:(我重新执行了下指令所以和上一个图不一样)
看到这个双向链表是有长度的:
但是这种内存变化的非常快,我们无法对它实时的分析(当然双机调试除外),在这里我就只说下找到它的原理。剩余的用代码来完成。
首先访问这个 FLink 指向的地址,判断这个地址是否已经到达了双向链表的结尾(结尾就是头部,在上图中就是 fffff80109fe1b68),如果没有结束,则判断这个Flink 指向的地址是否非法,如果合法 —— 关键点来了!
如果合法,就计算 FLink 指针 - 0x20 ,为什么?
因为 FLink 是一个 PLIST_ENTRY 类型的指针,-0x20 就是反推出 _KTIMER 对象的地址:
而这个地址,其实就是 dpc 定时器中的 定时器对象地址:
当然,拿到了 _KTIMER 对象后,也可以通过 Period 这个成员拿到 定时器的触发周期:
继续划重点!
那么函数入口地址怎么拿呢?
_KTIMER 里有一个成员 *DPC,这个是一个结构体对象指针,它指向一个这样的结构:
DeferredRoutine 就是我们想要的函数地址,其他的成员暂时不关心。
但有一点需要注意,当你拿到了 _KTIMER 对象了以后,通过 _KTIMER->Dpc
的方式拿到的其实是一个错误的 Dpc 地址,如果想要拿到正确的地址,我们要对这个错误地址进行解密!(我说的是 x64,x86下的是不需要解密的)
首先用 Windbg 找到 KeSetTimer 这个函数:
这知识一个空壳函数,真正的代码存放在 KiSetTimerEx 内(但只有 KeSetTimer 被导出了!):
大概意思就是一些 异或运算,循环右移,反序,如果看不明白可以参考下面的高级语言代码。
此处应该有掌声 😀
代码:
#include <Fltkernel.h> // 需要放在 ntddk 前,否则报重定义错误
#include <ntddk.h>
#include <intrin.h>
// I/O 控制码
#define IRP_TEST CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
UNICODE_STRING name_device; // 设备名
UNICODE_STRING name_symbol; // 符号链接
PDEVICE_OBJECT deviceObj; // 设备对象
BOOLEAN get_KiWait(PULONG64 never, PULONG64 always) {
// 获取 KeSetTimer 地址
ULONG64 ul_KeSetTimer = 0;
UNICODE_STRING uc_KeSetTimer = { 0 };
RtlInitUnicodeString(&uc_KeSetTimer, L"KeSetTimer");
ul_KeSetTimer = (ULONG64)MmGetSystemRoutineAddress(&uc_KeSetTimer);
if (ul_KeSetTimer == NULL) {
return FALSE;
}
// 前 30 字节找 call 指令
BOOLEAN b_e8 = FALSE;
ULONG64 ul_e8Addr = 0;
for (INT i = 0; i < 30; i++) {
if (!MmIsAddressValid((PVOID64)ul_KeSetTimer)) { continue; }
if (*(PUCHAR)(ul_KeSetTimer + i) == 0xe8) {
b_e8 = TRUE;
ul_e8Addr = ul_KeSetTimer + i;
break;
}
}
// 找到 call 则解析目的地址
ULONG64 ul_KiSetTimerEx = 0;
if (b_e8 == TRUE) {
if (!MmIsAddressValid((PVOID64)ul_e8Addr)) { return FALSE; }
INT ul_callCode = *(INT*)(ul_e8Addr + 1);
ULONG64 ul_callAddr = ul_e8Addr + ul_callCode + 5;
ul_KiSetTimerEx = ul_callAddr;
}
// 没有 call 则直接在当前函数找
else {
ul_KiSetTimerEx = ul_KeSetTimer;
}
// 前 50 字节找 找 KiWaitNever 和 KiWaitAlways
if (!MmIsAddressValid((PVOID64)ul_KiSetTimerEx)) { return FALSE; }
ULONG64 ul_arr_ret[2]; // 存放 KiWaitNever 和 KiWaitAlways 的地址
INT i_sub = 0; // 对应 ul_arr_ret 的下标
for (INT i = 0; i < 50; i++) {
if (*(PUCHAR)(ul_KiSetTimerEx + i) == 0x48 &&
*(PUCHAR)(ul_KiSetTimerEx + i + 1) == 0x8b &&
*(PUCHAR)(ul_KiSetTimerEx + i + 6) == 0x00
) {
ULONG64 ul_movCode = *(UINT32*)(ul_KiSetTimerEx + i + 3);
ULONG64 ul_movAddr = ul_KiSetTimerEx + i + ul_movCode + 7;
// 只拿符合条件的前两个值
if (i_sub < 2) {
ul_arr_ret[i_sub++] = ul_movAddr;
}
}
}
*never = ul_arr_ret[0];
*always = ul_arr_ret[1];
return TRUE;
}
// 从 WinDbg 里抄来的未导出结构
typedef struct _KTIMER_TABLE_ENTRY{
ULONG_PTR Lock;
LIST_ENTRY Entry; // 双向链表
ULONG_PTR Time;
}KTIMER_TABLE_ENTRY, * PKTIMER_TABLE_ENTRY;
typedef struct _KTIMER_TABLE{
ULONG_PTR TimerExpiry[64];
KTIMER_TABLE_ENTRY TimerEntries[256]; // 对象数组
}KTIMER_TABLE, * PKTIMER_TABLE;
BOOLEAN EnumDpc() {
// 获取 CPU 核心数
INT i_cpuNum = KeNumberProcessors;
for (KAFFINITY i = 0; i < i_cpuNum; i++) {
// 线程绑定特定 CPU
KeSetSystemAffinityThread(i + 1);
// 获得 KPRCB 的地址
ULONG64 p_PRCB = (ULONG64)__readmsr(0xC0000101) + 0x20;
if (!MmIsAddressValid((PVOID64)p_PRCB)) {
return FALSE;
}
// 取消绑定 CPU
KeRevertToUserAffinityThread();
// 判断操作系统版本
RTL_OSVERSIONINFOEXW OSVersion = { 0 };
OSVersion.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
RtlGetVersion((PRTL_OSVERSIONINFOW)&OSVersion);
// 计算 TimerTable 在 _KPRCB 结构中的偏移
PKTIMER_TABLE p_TimeTable = NULL;
if (OSVersion.dwMajorVersion == 10 && OSVersion.dwMinorVersion == 0) {
p_TimeTable = (PKTIMER_TABLE)(*(PULONG64)p_PRCB + 0x3680);
}
else if (OSVersion.dwMajorVersion == 6 && OSVersion.dwMinorVersion == 1) {
p_TimeTable = (PKTIMER_TABLE)(*(PULONG64)p_PRCB + 0x2200);
}
else {
DbgPrint("[LYSM] 预料之外的系统版本! \n");
return FALSE;
}
// 遍历 TimerEntries[] 数组(大小 256)
for (INT j = 0; j < 256; j++) {
// 获取 Entry 双向链表地址
if (!MmIsAddressValid((PVOID64)p_TimeTable)) { continue; }
PLIST_ENTRY p_ListEntryHead = &(p_TimeTable->TimerEntries[j].Entry);
// 遍历 Entry 双向链表
for (PLIST_ENTRY p_ListEntry = p_ListEntryHead->Flink; p_ListEntry != p_ListEntryHead; p_ListEntry = p_ListEntry->Flink) {
// 根据 Entry 取 _KTIMER 对象地址
if (!MmIsAddressValid((PVOID64)p_ListEntry)) { continue; }
PKTIMER p_Timer = CONTAINING_RECORD(p_ListEntry, KTIMER, TimerListEntry);
// 硬编码取 KiWaitNever 和 KiWaitAlways
ULONG64 never = 0, always = 0;
if (get_KiWait(&never, &always) == FALSE) {
return FALSE;
}
// 获取解密前的 Dpc 对象
if (!MmIsAddressValid((PVOID64)p_Timer)) { continue; }
ULONG64 ul_Dpc = (ULONG64)p_Timer->Dpc;
INT i_Shift = (*((PULONG64)never) & 0xFF);
// 解密 Dpc 对象
ul_Dpc ^= *((ULONG_PTR*)never); // 异或
ul_Dpc = _rotl64(ul_Dpc, i_Shift); // 循环左移
ul_Dpc ^= (ULONG_PTR)p_Timer; // 异或
ul_Dpc = _byteswap_uint64(ul_Dpc); // 颠倒顺序
ul_Dpc ^= *((ULONG_PTR*)always); // 异或
// 对象类型转换
PKDPC p_Dpc = (PKDPC)ul_Dpc;
// 打印验证
if (!MmIsAddressValid((PVOID64)p_Dpc)) { continue; }
DbgPrint("[LYSM] 定时器对象:0x%p \t 函数入口:0x%p \n", p_Timer, p_Dpc->DeferredRoutine);
}
}
}
return TRUE;
}
// 对应 IRP_MJ_DEVICE_CONTROL
NTSTATUS myIrpControl(IN PDEVICE_OBJECT pDevObj, IN PIRP pIRP) {
// 获取 IRP 对应的 I/O 堆栈指针
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIRP);
// 得到输入缓冲区大小
ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;
// 得到输出缓冲区大小
ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;
// 得到 IOCTL 码
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
// 捕获 I/O 操作类型(MajorFunction)
switch (code) {
case IRP_TEST:
{
// do something ...
break;
}
default:
break;
}
// 完成 IO 请求
IoCompleteRequest(pIRP, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// 对应 IRP_MJ_CREATE 、 IRP_MJ_CLOSE
NTSTATUS dpc_CAC(IN PDEVICE_OBJECT pDevObj, IN PIRP pIRP) {
// 将 IRP 返回给 I/O 管理器
IoCompleteRequest(
pIRP, // IRP 指针
IO_NO_INCREMENT // 线程优先级,IO_NO_INCREMENT :不增加优先级
);
// 设置 I/O 请求状态
pIRP->IoStatus.Status = STATUS_SUCCESS;
// 设置 I/O 请求传输的字节数
pIRP->IoStatus.Information = 0;
return STATUS_SUCCESS;
}
NTSTATUS CreateDevice(IN PDRIVER_OBJECT DriverObject) {
// 定义返回值
NTSTATUS status;
// 初始化设备名
RtlInitUnicodeString(&name_device, L"\\Device\\LYSM_device");
// 创建设备
status = IoCreateDevice(
DriverObject, // 指向驱动对象的指针
0, // 设备扩展分配的字节数
&name_device, // 设备名
FILE_DEVICE_UNKNOWN, // 设备类型
0, // 驱动设备附加信息
TRUE, // 设备对象是否独占设备
&deviceObj // 设备对象指针
);
if (!NT_SUCCESS(status)) {
KdPrint(("[LYSM] IoCreateDevice error:%X",status));
return status;
}
// 初始化符号链接名
RtlInitUnicodeString(&name_symbol, L"\\??\\LYSM_symbol");
// 创建符号链接
status = IoCreateSymbolicLink(&name_symbol, &name_device);
if (!NT_SUCCESS(status)) {
DbgPrint("[LYSM] IoCreateSymbolicLink error:%X",status);
return status;
}
return STATUS_SUCCESS;
}
NTSTATUS DriverUnload(IN PDRIVER_OBJECT DriverObject) {
// 定义返回值
NTSTATUS status;
// 输出日志
DbgPrint("[LYSM] DriverUnload start.\n");
// 删除符号链接
status = IoDeleteSymbolicLink(&name_symbol);
if (!NT_SUCCESS(status)) {
DbgPrint("[LYSM] IoDeleteSymbolicLink error:%X",status);
return status;
}
// 删除设备
IoDeleteDevice(deviceObj);
return STATUS_SUCCESS;
}
extern "C" NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
) {
// 定义返回值
NTSTATUS status;
// 输出日志
DbgPrint("[LYSM] DriverEntry start.\n");
// 指定驱动卸载函数
DriverObject->DriverUnload = (PDRIVER_UNLOAD)DriverUnload;
// 指定派遣函数
DriverObject->MajorFunction[IRP_MJ_CREATE] = dpc_CAC;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = dpc_CAC;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = myIrpControl;
// 创建设备
status = CreateDevice(DriverObject);
if(!NT_SUCCESS(status)) { DbgPrint("[LYSM] CreateDevice error:%X",status); return status; }
// 执行代码
if (!EnumDpc()) {DbgPrint("[LYSM] EnumDpc 执行失败!");}
else {DbgPrint("[LYSM] EnumDpc 执行成功!");}
return STATUS_SUCCESS;
}
效果图:
最后感谢看雪论坛的 Karlx 大佬提供的思路,膜拜大佬。 ( ̄︶ ̄)↗