x64驱动遍历 DPC 定时器

目的

拿到类似于 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,② 和 ③ 分别是双向链表的 FlinkBlink ,④ 是 对象数组中的 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 大佬提供的思路,膜拜大佬。 ( ̄︶ ̄)↗

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值