6.3 Windows驱动开发:内核枚举IoTimer定时器
内核I/O定时器(Kernel I/O Timer)是Windows内核中的一个对象,它允许内核或驱动程序设置一个定时器,以便在指定的时间间隔内调用一个回调函数。通常,内核I/O定时器用于周期性地执行某个任务,例如检查驱动程序的状态、收集性能数据等。
今天继续分享内核枚举系列知识,这次我们来学习如何通过代码的方式枚举内核IoTimer定时器,内核定时器其实就是在内核中实现的时钟,该定时器的枚举非常简单,因为在IoInitializeTimer初始化部分就可以找到IopTimerQueueHead地址,该变量内存储的就是定时器的链表头部。

内核I/O定时器通常由内核或驱动程序创建,使用KeInitializeTimerEx函数进行初始化。然后,使用KeSetTimerEx函数启动定时器,以指定间隔和回调函数。每次定时器超时时,回调函数都会被调用,然后定时器重新启动以等待下一个超时。
内核I/O定时器是内核中常见的机制之一,它允许内核和驱动程序实现各种功能,如性能监视、定时执行任务等。但是,使用内核I/O定时器必须小心谨慎,因为它们可能会影响系统的性能和稳定性,特别是当存在大量定时器时。
枚举Io定时器过程是这样的:
- 1.找到
IoInitializeTimer函数,该函数可以通过MmGetSystemRoutineAddress得到。 - 2.找到地址以后,我们向下增加
0xFF偏移量,并搜索特征定位到IopTimerQueueHead链表头。 - 3.将链表头转换为
IO_TIMER结构体,并循环链表头输出。
这里解释一下为什么要找IoInitializeTimer这个函数他是一个初始化函数,既然是初始化里面一定会涉及到链表的存储问题,找到他就能找到定时器链表基址,该函数的定义如下。
NTSTATUS IoInitializeTimer(IN PDEVICE_OBJECT DeviceObject, // 设备对象指针IN PIO_TIMER_ROUTINE TimerRoutine, // 定时器例程IN PVOID Context // 传给定时器例程的函数);
接着我们需要得到IO定时器的结构定义,在DEVICE_OBJECT设备对象指针中存在一个Timer属性。
kd> dt _DEVICE_OBJECT
ntdll!_DEVICE_OBJECT+0x000 Type : Int2B+0x002 Size : Uint2B+0x004 ReferenceCount : Int4B+0x008 DriverObject : Ptr64 _DRIVER_OBJECT+0x010 NextDevice : Ptr64 _DEVICE_OBJECT+0x018 AttachedDevice : Ptr64 _DEVICE_OBJECT+0x020 CurrentIrp : Ptr64 _IRP+0x028 Timer : Ptr64 _IO_TIMER+0x030 Flags : Uint4B+0x034 Characteristics : Uint4B+0x038 Vpb : Ptr64 _VPB+0x040 DeviceExtension : Ptr64 Void+0x048 DeviceType : Uint4B+0x04c StackSize : Char+0x050 Queue : <anonymous-tag>+0x098 AlignmentRequirement : Uint4B+0x0a0 DeviceQueue : _KDEVICE_QUEUE+0x0c8 Dpc : _KDPC+0x108 ActiveThreadCount : Uint4B+0x110 SecurityDescriptor : Ptr64 Void+0x118 DeviceLock : _KEVENT+0x130 SectorSize : Uint2B+0x132 Spare1 : Uint2B+0x138 DeviceObjectExtension : Ptr64 _DEVOBJ_EXTENSION+0x140 Reserved : Ptr64 Void

这里的这个+0x028 Timer定时器是一个结构体_IO_TIMER其就是IO定时器的所需结构体。
kd> dt _IO_TIMER
ntdll!_IO_TIMER+0x000 Type : Int2B+0x002 TimerFlag : Int2B+0x008 TimerList : _LIST_ENTRY+0x018 TimerRoutine : Ptr64 void +0x020 Context : Ptr64 Void+0x028 DeviceObject : Ptr64 _DEVICE_OBJECT

如上方的基础知识有了也就够了,接着就是实际开发部分,首先我们需要编写一个GetIoInitializeTimerAddress()函数,让该函数可以定位到IoInitializeTimer所在内核中的基地址上面,具体实现调用代码如下所示。
#include <ntifs.h>// 得到IoInitializeTimer基址
PVOID GetIoInitializeTimerAddress()
{PVOID VariableAddress = 0;UNICODE_STRING uioiTime = { 0 };RtlInitUnicodeString(&uioiTime, L"IoInitializeTimer");VariableAddress = (PVOID)MmGetSystemRoutineAddress(&uioiTime);if (VariableAddress != 0){return VariableAddress;}return 0;
}VOID UnDriver(PDRIVER_OBJECT driver)
{DbgPrint(("Uninstall Driver Is OK \n"));
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint(("hello lyshark \n"));// 得到基址PUCHAR IoInitializeTimer = GetIoInitializeTimerAddress();DbgPrint("IoInitializeTimer Address = %p \n", IoInitializeTimer);Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}
运行这个驱动程序,然后对比下是否一致:

接着我们在反汇编代码中寻找IoTimerQueueHead,此处在LyShark系统内这个偏移位置是nt!IoInitializeTimer+0x5d 具体输出位置如下。
kd> uf IoInitializeTimernt!IoInitializeTimer+0x5d:
fffff805`74b85bed 488d5008 lea rdx,[rax+8]
fffff805`74b85bf1 48897018 mov qword ptr [rax+18h],rsi
fffff805`74b85bf5 4c8d054475e0ff lea r8,[nt!IopTimerLock (fffff805`7498d140)]
fffff805`74b85bfc 48897820 mov qword ptr [rax+20h],rdi
fffff805`74b85c00 488d0dd9ddcdff lea rcx,[nt!IopTimerQueueHead (fffff805`748639e0)]
fffff805`74b85c07 e8141e98ff call nt!ExInterlockedInsertTailList (fffff805`74507a20)
fffff805`74b85c0c 33c0 xor eax,eax
在WinDBG中标注出颜色lea rcx,[nt!IopTimerQueueHead (fffff805748639e0)]更容易看到。

接着就是通过代码实现对此处的定位,定位我们就采用特征码搜索的方式,如下代码是特征搜索部分。
- StartSearchAddress 代表开始位置
- EndSearchAddress 代表结束位置,粗略计算0xff就可以定位到了。
#include <ntifs.h>// 得到IoInitializeTimer基址
PVOID GetIoInitializeTimerAddress()
{PVOID VariableAddress = 0;UNICODE_STRING uioiTime = { 0 };RtlInitUnicodeString(&uioiTime, L"IoInitializeTimer");VariableAddress = (PVOID)MmGetSystemRoutineAddress(&uioiTime);if (VariableAddress != 0){return VariableAddress;}return 0;
}VOID UnDriver(PDRIVER_OBJECT driver)
{DbgPrint(("Uninstall Driver Is OK \n"));
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint(("hello lyshark \n"));// 得到基址PUCHAR IoInitializeTimer = GetIoInitializeTimerAddress();DbgPrint("IoInitializeTimer Address = %p \n", IoInitializeTimer);INT32 iOffset = 0;PLIST_ENTRY IoTimerQueueHead = NULL;PUCHAR StartSearchAddress = IoInitializeTimer;PUCHAR EndSearchAddress = IoInitializeTimer + 0xFF;UCHAR v1 = 0, v2 = 0, v3 = 0;for (PUCHAR i = StartSearchAddress; i < EndSearchAddress; i++){if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2)){v1 = *i;v2 = *(i + 1);v3 = *(i + 2);// 三个特征码if (v1 == 0x48 && v2 == 0x8d && v3 == 0x0d){memcpy(&iOffset, i + 3, 4);IoTimerQueueHead = (PLIST_ENTRY)(iOffset + (ULONG64)i + 7);DbgPrint("IoTimerQueueHead = %p \n", IoTimerQueueHead);break;}}}Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}
搜索三个特征码v1 == 0x48 && v2 == 0x8d && v3 == 0x0d从而得到内存位置,运行驱动对比下。
- 运行代码会取出
lea指令后面的操作数,而不是取出lea指令的内存地址。

最后一步就是枚举部分,我们需要前面提到的IO_TIMER结构体定义。
- PIO_TIMER Timer = CONTAINING_RECORD(NextEntry, IO_TIMER, TimerList) 得到结构体,循环输出即可。
#include <ntddk.h>
#include <ntstrsafe.h>typedef struct _IO_TIMER
{INT16 Type;INT16 TimerFlag;LONG32 Unknown;LIST_ENTRY TimerList;PVOID TimerRoutine;PVOID Context;PVOID DeviceObject;
}IO_TIMER, *PIO_TIMER;// 得到IoInitializeTimer基址
PVOID GetIoInitializeTimerAddress()
{PVOID VariableAddress = 0;UNICODE_STRING uioiTime = { 0 };RtlInitUnicodeString(&uioiTime, L"IoInitializeTimer");VariableAddress = (PVOID)MmGetSystemRoutineAddress(&uioiTime);if (VariableAddress != 0){return VariableAddress;}return 0;
}VOID UnDriver(PDRIVER_OBJECT driver)
{DbgPrint("卸载完成... \n");
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint(("hello lyshark \n"));// 得到基址PUCHAR IoInitializeTimer = GetIoInitializeTimerAddress();DbgPrint("IoInitializeTimer Address = %p \n", IoInitializeTimer);// 搜索IoTimerQueueHead地址/*nt!IoInitializeTimer+0x5d:fffff806`349963cd 488d5008 lea rdx,[rax+8]fffff806`349963d1 48897018 mov qword ptr [rax+18h],rsifffff806`349963d5 4c8d05648de0ff lea r8,[nt!IopTimerLock (fffff806`3479f140)]fffff806`349963dc 48897820 mov qword ptr [rax+20h],rdifffff806`349963e0 488d0d99f6cdff lea rcx,[nt!IopTimerQueueHead (fffff806`34675a80)]fffff806`349963e7 e8c43598ff call nt!ExInterlockedInsertTailList (fffff806`343199b0)fffff806`349963ec 33c0 xor eax,eax*/INT32 iOffset = 0;PLIST_ENTRY IoTimerQueueHead = NULL;PUCHAR StartSearchAddress = IoInitializeTimer;PUCHAR EndSearchAddress = IoInitializeTimer + 0xFF;UCHAR v1 = 0, v2 = 0, v3 = 0;for (PUCHAR i = StartSearchAddress; i < EndSearchAddress; i++){if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2)){v1 = *i;v2 = *(i + 1);v3 = *(i + 2);// fffff806`349963e0 48 8d 0d 99 f6 cd ff lea rcx,[nt!IopTimerQueueHead (fffff806`34675a80)]if (v1 == 0x48 && v2 == 0x8d && v3 == 0x0d){memcpy(&iOffset, i + 3, 4);IoTimerQueueHead = (PLIST_ENTRY)(iOffset + (ULONG64)i + 7);DbgPrint("IoTimerQueueHead = %p \n", IoTimerQueueHead);break;}}}// 枚举列表KIRQL OldIrql;// 获得特权级OldIrql = KeRaiseIrqlToDpcLevel();if (IoTimerQueueHead && MmIsAddressValid((PVOID)IoTimerQueueHead)){PLIST_ENTRY NextEntry = IoTimerQueueHead->Flink;while (MmIsAddressValid(NextEntry) && NextEntry != (PLIST_ENTRY)IoTimerQueueHead){PIO_TIMER Timer = CONTAINING_RECORD(NextEntry, IO_TIMER, TimerList);if (Timer && MmIsAddressValid(Timer)){DbgPrint("IO对象地址: %p \n", Timer);}NextEntry = NextEntry->Flink;}}// 恢复特权级KeLowerIrql(OldIrql);Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}
运行这段源代码,并可得到以下输出,由于没有IO定时器所以输出结果是空的:

至此IO定时器的枚举就介绍完了,在教程中你已经学会了使用特征码定位这门技术,相信你完全可以输出内核中想要得到的任何结构体。
相关文章:
6.3 Windows驱动开发:内核枚举IoTimer定时器
内核I/O定时器(Kernel I/O Timer)是Windows内核中的一个对象,它允许内核或驱动程序设置一个定时器,以便在指定的时间间隔内调用一个回调函数。通常,内核I/O定时器用于周期性地执行某个任务,例如检查驱动程序…...
大数据-之LibrA数据库系统告警处理(ALM-37005 GTM进程异常)
告警解释 当出现如下情况时,产生该告警: GTM实例数据目录中的gtm.conf配置文件不存在或者其中某个配置参数不正确时。GTM实例服务线程无法监听IP,或者无法绑定监听端口。GTM实例进程没有其数据目录读写权限时。 告警属性 告警ID 告警级别…...
一种LED驱动专用控制电路
一、基本概述 TM1620是一种LED(发光二极管显示器)驱动控制专用IC,内部集成有MCU数字接口、数据锁存 器、LED驱动等电路。本产品质量可靠、稳定性好、抗干扰能力强。主要适用于家电设备(智能热 水器、微波炉、洗衣机、空调、电磁炉)、机顶盒、电子称、…...
Matlab进阶绘图第33期—双曲面图
在《Matlab论文插图绘制模板第56期—曲面图(Surf)》中,我分享过曲面图的绘制模板。 然而,有的时候,需要在一张图上绘制两个及以上的曲面图,且每个曲面图使用不同的配色方案。 在Matlab中,一张…...
【Linux】23、内存超详细介绍
文章目录 零、资料一、内存映射1.1 TLB1.2 多级页表1.3 大页 二、虚拟内存空间分布2.1 用户空间的段2.2 内存分配和回收2.2.1 小对象2.2.2 释放 三、查看内存使用情况3.1 Buffer 和 Cache3.1.1 proc 文件系统3.1.2 案例3.1.2.1 场景 1:磁盘和文件写案例3.1.2.2 场景…...
官网IDM下载和安装的详细步骤
目录 一、IDM是什么 二、下载安装 三、解决下载超时的问题 四、谷歌浏览器打开IDM插件 谷歌浏览器下载官网👇 五、测试 六、资源包获取 一、IDM是什么 IDM(internet download manager)是一个互联网下载工具插件,常见于用…...
【面经八股】搜广推方向:常见面试题(三)
【面经&八股】搜广推方向:常见面试题(三) 文章目录 【面经&八股】搜广推方向:常见面试题(三)1. 如何解决数据不平衡2. 假设检验的两类错误3. 为什么快排比堆排快4. RMSE、MSE、MAE5. 双塔模型的应用6. XGBoost如果损失函数没有二阶导,该怎么办7. AUC是如何实现的…...
[NOIP2006]明明的随机数
一、题目 登录—专业IT笔试面试备考平台_牛客网 二、代码 set去重,再利用vector进行排序 std::set是一个自带排序功能的容器,它已经按照一定的规则(默认是元素的小于比较)对元素进行了排序。因此,你不能直接对std::s…...
auth模块
一. auth模块前戏 # 引入:其实我们在创建好一个django项目之后直接执行数据库迁移命令会自动生成很多表 例如:django_sessionauth_user我们知道django在启动之后就可以直接访问admin路由,需要输入用户名和密码,数据参考的就是auth_user表,并且还必须是管…...
H5ke12--3--iframe--编辑邮箱的制作
下面我们来window.iframes[] frames是一个全局变量,它是一个对象数组,其中包含当前窗口中的所有框架(如果存在)。 在这段代码中,let frameframes[0];是将第一个框架赋值给变量frame。通过frame.document.designMode&q…...
Python面经【3】
零、可迭代对象 可迭代对象是迭代器和生成器的基础,简单来说,可以使用for循环遍历的对象就是可迭代对象,比如常见的list、set和dict。在python中,可迭代对象是指实现了__iter__()方法的对象,当我们使用for循环遍历一个…...
Python集合类型
目录 目标 版本 官方文档 集合分类 实战 创建 循环 常用方法 目标 掌握set和frozenset两种集合的使用方法,包括:创建、交集、并集、差集等操作。 版本 Python 3.12.0 官方文档 Set Types — set, frozensethttps://docs.python.org/3/library/s…...
npm install报错常用解题思路
最近刚接手一个“新”项目,让我很无语。明明是去年起的项目,但是它所用的部分技术栈非常旧,我启动项目,控制台一堆warning报错,然后项目结构也很让我不适应,很多地方都可以用文件夹包一下来方便定位。哎&am…...
conda: error: argument COMMAND: invalid choice
简介 使用conda activate 时,可能会报:conda: error: argument COMMAND: invalid choice: ‘activate’ (choose from ‘clean’, ‘compare’, ‘config’, ‘create’, ‘info’, ‘init’, ‘install’, ‘list’, ‘notices’, ‘package’, ‘remo…...
数仓成本下降近一半,StarRocks 存算分离助力云览科技业务出海
成都云览科技有限公司倾力打造了凤凰浏览器,专注于为海外用户提供服务,公司致力于构建一个全球性的数字内容连接入口,为用户带来更为优质、高效、个性化的浏览体验。 作为数据驱动的高科技公司,从数据中挖掘价值一直是公司核心任务…...
Apache基线检查
一、确保对OS根目录禁用覆盖 当 AllowOverride 指令设置为 None 时,Apache 将禁止在该目录下使用 .htaccess 文件来覆盖任何配置项。这意味着,除非您在主配置文件中显式地指定,否则该目录下的任何 .htaccess 文件都将被忽略。 禁用 .htaccess 文件可以提高服务器的安全性,因…...
flink的集成测试
背景 日常测试中我们使用flink的TestHarness只能测试单个算子,很多情况下我们需要集成测试来测试真正的问题,所以在flink中进行集成测试还是非常有必要的,本文就来记录下如何在flink中进行集成测试 flink中进行集成测试 flink中进行集成测…...
gitee推荐-1Panel
以下内容来源于gitee。 gitee地址:1Panel: 🔥 🔥 🔥 现代化、开源的 Linux 服务器运维管理面板。 大概和宝塔类似,但支持docker。在线体验:https://demo.1panel.cn/ 稍微试了下,没找到apache,…...
GEE 22:基于GEE实现物种分布模型(更新中。。。。。。)
物种分布模型 1. 数据点准备1.1 数据加载1.2 去除指定距离内的重复点1.3 定义研究区范围1.4 选择预测因子 1. 数据点准备 1.1 数据加载 首先需要将CSV文件导入到GEE平台中,同样也可以导入shp格式文件。 // 1.Loading and cleaning your species data *************…...
阿里云windwos 安装oracle数据库,外部用工具连接不上,只能在服务器本机通过127.0.0.1 连接
1. 首先检查阿里云服务器安全组端口是否开放 oracle 数据库端口 2. 其次找到oracle 安装的目录,打开这俩个文件,将localhost 修改为 服务器本机名称 3.重启oracle 监听服务,就可以连接了...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...
[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...
MinIO Docker 部署:仅开放一个端口
MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...
毫米波雷达基础理论(3D+4D)
3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文: 一文入门汽车毫米波雷达基本原理 :https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...
