Windows内核编程基础(1)

在前面的文章中,介绍了如何配置开发环境以及如何进行调试。

接下来的几篇文章,将会重点介绍内核编程中所需要了解的一些理论基础。

我写这个系列文章的主要目的是方便以后自己查阅,同时也给正在学习内核开发的小伙伴一些参考,所以我会尽可能地以最简单的方式进行描述。

如果在阅读过程中遇到不理解的地方,可以查阅书籍、互联网,或私信与我取得联系。

与用户模式的区别

内核API由C函数组成,本质上与用户模式开发很像。不过这两者之间还是有很多不同之处,可以参考下表

用户模式内核模式
未处理的异常进程崩溃系统崩溃
终止进程终止时,所有私有的内存和资源都自动释放如果驱动卸载时没有释放用过的所有资源,就会造成泄蒲。只有重启才能解决。
返回值API错误有时候会被忽略必须(几乎)从不忽略错误
IRQL永远是PASSIVE_LEVEL( 0)可能是 DISPATCH_LEVEL〔2)或者更高
坏代码(Bad Code)一般局限在进程内会影响到整个系统
测试和调试通常在开发者的机器上进行测试和调试必须在另一台机器上进行调试
能使用几乎全部C/C++库〔例如STL、boost)无法用大多数标准库
异常处理可以使用C++异常处理和结构化异常处理(SEH)只能用SEH
C++用法可以使用完整的C++运行时支持没有C++运行时支持

未处理的异常

用户模式下,如果出现未捕获的异常,程序会中止。

内核模式下,出现未捕获的异常,会造成系统崩溃,出现蓝屏。

蓝屏实际是一种保护机制,防止用户继续执行接下来的代码。所以在编写内核代码时,必须非常小心,而且不能跳过任何细节和错误检查 。

终止

用户模式下,当进程终止时,不管是正常结束 、未处理的异常,还是因为外部代码中止了它,这个进程什么都不会泄漏,所有的资源都会被释放。

内核模式下,如果当驱动被卸载时,仍有资源被占用,那么这么资源不会被自动释放,只有下一次系统重启时才会释放。

所以在进行内核编程时,清除工作是非常重要的。

函数返回值

用户模式下,API函数的返回值有时候会被忽略(我就经常这么干)。大部分的API函数都能正常执行,不会造成什么影响。最坏的情况下,会产生未处理的异常,从而导致进程崩溃,但系统不会受影响。

内核模式下,忽略API的返回值会很危险。所以这里的原则就是永远都去检查API函数的返回值。

IRQL(Interrupt ReQuest Levels ,中断请求级别)

用户模式下,线程有“优先级”的概念,系统调度器以时间片作为粒度,根据线程的优先级来调试线程,线程优先级越高,获得调度的机会越大。

与线程优先级概念类似,CPU提供了一个被称为IRQL(中断请求级别)的概念,并且规定,高IRQL的代码,可以中断(抢占)低IRQL的代码的执行过程,从而得到执行机会。

不同级别的IRQL对应不同的数值,软件驱动常见的IRQL及其数值如下所示,数值越大,表示级别越高。

注意:上表只列出了软件驱动所需要用到的IRQL,并不代表IRQL只有这三个值。

不同的IRQL限制不同:

PASSIVE_LEVEL(0) : 作为级别最低的IRQL,在这个IRQL中可以无限制使用系统提供的API,并且可以访问分页(Paged)内存和非分页(NonPaged)内存

说明:每个内核API对IRQL有不同的要求,在WDK API的官方文档中可以看到这个。如下图所示

APC_LEVEL(1) :这个中断级别可以中断PASSIVE_LEVEL的代码,主要用于APC(Asynchronous Procedure Calls,异步过程调用),在使用系统API时有一定的限制,可以访问分页内存和非分页内存。

DISPATCH_LEVEL(2) :存在的限制更多,只有很少一部分API函数可以在这个级别下使用,在内存访问方面,只能使用非分页内存。

说明:

分页内存:内存的内容可以被置换到磁盘上(也可以是其他介质),

非分页内存:内存的内容不会被系统置换到磁盘上。

 因为不同IRQL的限制不同,所以了解自己代码所处的IRQL就变得非常有意义。

判断代码所在的IRQL有两种方法:

1、静态方法

这种方法更多是根据微软WDK帮助文档来判断,比如说驱动的入口函数DriverEntry,系统在调用这个入口函数时,IRQL为PASSIVE_LEVEL,这个是由系统保证的,WDK对这点也有明确的说明。

2、动态方法

如某些回调函数在被系统调用时,IRQL可能是PAS-SIVE_LEVEL至DISPATCH_LEVEL级别范围,对于这种情况,我们可以在该回调函数中,通过调用KeGetCurrentIrql函数来获取当前的IRQL。如下面的示例代码:

 1 #include <ntddk.h>2 3 VOID DriverUnload(PDRIVER_OBJECT DriverObject)4 {5     if (DriverObject != NULL)6         DbgPrint("Driver Upload,Driver 0bject Address :%p,CurrentIRQL = 0x%u\n", DriverObject, KeGetCurrentIrql());7     return;8 }9 
10 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
11 {
12     DbgPrint("Hello Kernel world,CurrentIRQL = 0x%u\n", KeGetCurrentIrql());
13 
14     if (RegistryPath != NULL)
15     {
16 
17     }
18 
19     if (DriverObject != NULL)
20     {
21         DriverObject->DriverUnload = DriverUnload;
22     }
23 
24     return STATUS_SUCCESS;
25 }

运行输出如下:

提升和降低IRQL

在用户模式下,IRQL这个概念从不被提及,也没有办法改变它。

在内核模式下,IRQL能用KeRaiseIrql函数提升并用KeLowerIrql函数降回来。

下面的示例代码将IRQL提升到DISPATCH_LEVEL(2),在此IRQL上执行一些操作,然后降回到原来的IRQL。

 1 #include<ntddk.h>2 3 VOID DriverUnload(PDRIVER_OBJECT DriverObject)4 {5     if (DriverObject != NULL)6     {7 8     }9 
10     return;
11 }
12 
13 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
14 {
15     if (DriverObject != nullptr)
16     {
17         DriverObject->DriverUnload = DriverUnload;
18     }
19 
20     if (RegistryPath != NULL)
21     {
22 
23     }
24 
25     KIRQL oldIrql;
26 
27     DbgPrint("[%ws]Before Raise IRQL:CurrentIRQL = 0x%u", __FUNCTIONW__, KeGetCurrentIrql());
28 
29     KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
30 
31     DbgPrint("[%ws]After Raise IRQL:CurrentIRQL = 0x%u", __FUNCTIONW__, KeGetCurrentIrql());
32 
33     KeLowerIrql(oldIrql);
34 
35     DbgPrint("[%ws]After Lower IRQL:CurrentIRQL = 0x%u", __FUNCTIONW__, KeGetCurrentIrql());
36 
37     return STATUS_SUCCESS;
38 }

运行结果如下:

注意:

如果提升了IRQL,请确保在同一个函数里将它降低。如果函数返回时的IRQL比进入时要高,那么这种情况是很危险的。

另外 ,要确保KeRaiseIrql确实提升了IRQL,KeLowerIrql确实降低了IRQL,否则,系统随便就会崩溃。

C++用法

C++几乎全部内容都能用在内核代码里,但是在内核模式下,没有C++运行时,因此一些C++特性没办法使用

1、不支持new和delete操作符

使用它们会导致编译失败。这是由于它们的正常操作是从用户模式堆分配内存,而在内核模式里这显然毫无意义。内核API里有接近于malloc和free这些C函数的“替代”函数,在后面的文章中将会详细介绍内核模式下的内存分配和释放。然而,用类似于用户模式的C++的方式重载这些操作符,并调用内核的分配和释放函数这是可以的。在后面的文章中也会提及到。

2、非默认构造函数中的全局变量将不会被调用

因为没有C++运行时,所以构造函数不会被调用。这些情况可以通过以下方式避免:

  1. 避免把代码放到构造函数中,而是创建一些Init函数,并显式地从驱动 程序代码(如DriverEntry)中调用
  2. 仅仅把类指针定义成全局变量,然后动态分配其实例,编译器会生成正确的代码调用构造函数。但是调用的前提是已经重载了new和delete操作符,如前面描述的那样。

3、 不支持异常处理的关键字(try、catch、throw)

C++的异常处理机制需要它自己的运行时,而在内核中没有这个运行时。异常处理只能通过结构化异常处理(SEH,内核的异常处理机制)来完成。

4、 不能使用标准C++库

虽然标准库里的大部分内容是基于模板的,但它依赖用户模式及其语义,所以无法使用。

但是C++模板作为语言特性,在内核 里是可以使用的。

内核API

内核驱动程序使用的是从内核的组件输出的函数,这些函数被称为内核API。大多数函数则在内核本身模块(NtOskrnl.exe)里实现。

但还有些是在别的模块中实现,比如在HAL.dll中。

 NtOskrnl.exe中的内核API(部分截图)

内核API是大量C函数的集合,其中多数的名称前有一个前缀,这个前缀指示了实现该函数的模块

这里有一组函数值得讨论一下,以Zw开头的函数(ZwCreateFile、ZwReadFile等)。这组函数作为NTDLL.dll中的原生API的镜像,是从原生API到位于执行体中的实现之间的网关。

当用户模式调用了Nt函数,比如NtCreateFile(ZwCreateFile)时,最终将到达执行体中实际的NtCreateFile实现。此时,基于原始调用来自用户模式这个事实,NtCreateFile可能会做各种合法性检查。这里调用者的信息以线程为基础保存在每个线程对应的KTHREAD结构中未公开的PreviousMode字段里。

When an Nt function is called from user mode, such as NtCreateFile, it reaches the
Executive at the actual NtCreateFile implementation. At this point, NtCreateFile might do various
checks based on the fact that the original caller is from user mode. This caller information is stored
on a thread-by-thread basis, in the undocumented PreviousMode member in the KTHREAD structure
for each thread.

另一方面,如果内核驱动程序要调用某个系统服务,它就没必要做跟用户模式一样的检查以及接受用户模式调用者所受的限制。这就是为什么要有Zw系列函数。调用Zw函数会将PreviousMode设置成KernelMode ( o ),然后调用原生函数。举个例子,调用ZwCreateFile会将前一个调用者的模式设置为KernelMode,然后调用NtCreateFile,这使得NtCreateFile绕过一些安全性和缓冲区的检查。底线是,驱动程序必须调用Zw系列函数。

On the other hand, if a kernel driver needs to call a system service, it should not be subjected to the
same checks and constraints imposed on user-mode callers. This is where the Zw functions come into
play. Calling a Zw function sets the previous caller mode to KernelMode (0) and then invokes the
native function. For example, calling ZwCreateFile sets the previous caller to KernelMode and then
calls NtCreateFile, causing NtCreateFile to bypass some security and buffer checks that would
otherwise be performed. The bottom line is that kernel drivers should call the Zw functions unless
there is a compelling reason to do otherwise.

说明:这里我贴出《Windows Kernel Programming 2nd》英文原版,因为中文翻译得不是非常通俗易懂

函数和错误代码

多数内核API函数会返回一个状态,用来指示操作成功或者失败。这个状态被定义为NTSTATUS

1 typedef _Return_type_success_(return >= 0) LONG NTSTATUS;

在文件ntstatus.h中可以找到所有定义的NTSTATUS值

ntstatus.h

 1 #define STATUS_SUCCESS                   ((NTSTATUS)0x00000000L)    // ntsubauth2 3 //4 // MessageId: STATUS_WAIT_15 //6 // MessageText:7 //8 //  STATUS_WAIT_19 //
10 #define STATUS_WAIT_1                    ((NTSTATUS)0x00000001L)
11 
12 //
13 // MessageId: STATUS_WAIT_2
14 //
15 // MessageText:
16 //
17 //  STATUS_WAIT_2
18 //
19 #define STATUS_WAIT_2                    ((NTSTATUS)0x00000002L)
...
...
...

大部分的代码并不关心确切的错误值,只要测试最高位就行,可以使用NT_SUCCESS宏来完成,就像使用SUCCEED(HRESULT)宏一样。

在某些情况下,从函数返回的NTSTATUS值最终会返回到用户模式。这时候STATUS_XXX值会被转换成ERROR_XXX值,在用户模式中可以通过GetLastError函数得到这些值。

驱动程序对象

在前面的示例代码中,我们可以看到DriverEntry函数会接收一个DRIVER_OBJECT的参数

1 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)

这个结构由内核分配,并且进行了部分初始化,然后传递给DriverEntry(在驱动程序卸载之前,还会传递给Unload)。此时驱动程序需要进一步对这个结构进行初始化,从而指明该驱动能支持哪些操作。

在前面的代码中,我们见过了这些操作之一 ---- Unload函数,如下所示:

1 if (DriverObject != NULL)
2 {
3     DbgPrint("Driver Object Address: %p\n", DriverObject);
4     DriverObject->DriverUnload = DriverUnload;
5 }

另外那些需要初始化的重要操作集合被称为分发例程。它是一个函数指针数组,位于DRIVER_OBJECT的MajorFunction字段。这个集合指明驱动程序支持哪些操作。如创建、读取、写入等。数组的索引被定义成带有IRP_MJ_前缀的常量。部分定义如下所示:

说明:详细的介绍可以参考以下链接:

处理 IRP - Windows drivers | Microsoft Learn

起初MajorFunction数组会被内核初始化成指向内核的内部例程IopInvalidDeviceRequest,它给调用者返回一个错误的状态,以表明不支持所请求的操作。

举一个例子,前面的示例代码中,到现在为止还不支持任何分发例程,因此现在无法与驱动程序通信。

驱动程序必须至少支持IRP_MJ_CREATEIRP_MJ_CLOSE操作,才能打开该驱动程序设备对象的一个句柄。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/149896.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

seaCMS v12.9代码审计学习(上半)

文章目录 CMS介绍环境搭建代码总览漏洞复现/js/player/dmplayer/player/index.php 反射性xss(详见https://github.com/HuaQiPro/seacms/issues/28)admin_ping.php 代码执行漏洞太多了&#xff0c;整理完了下半部分一次性写完 CMS介绍 海洋cms是一款经典的开源影视建站系统&…

召回05 矩阵补充、最近邻查找

matrix completion 上述矩阵补充模型&#xff1a; 基于embadding做推荐&#xff0c;输入用户和物品id&#xff0c;输出一个实数&#xff0c;即用户对物品兴趣的预估值。把id映射到一个向量a,是对用户的表征&#xff0c;embadding层是一个矩阵&#xff0c;a是矩阵的一列&#x…

Dify部署及初步测试

文章目录 Dify安装Dify启动模型接入模型测试 Dify安装 根据Docker Compose 部署中的相关指引&#xff0c;完成以下步骤 git clone https://github.com/langgenius/dify.git cd dify/docker cp .env.example .env docker compose up -d查看容器状态&#xff1a;docker compose…

408选择题笔记|自用|随笔记录

文章目录 B树&#xff1a;访问节点建堆&#xff01;将结点插入空堆广义指令求每个子网可容纳的主机数量虚拟内存的实现方式文件目录项FCB和文件安全性管理级别索引文件三种存取方式及适用器件成组分解访问磁盘次数 C语言标识符 最小帧长物理传输层介质 局域网&广域网考点总…

【全新课程】正点原子《基于GD32 ARM32单片机项目实战入门》培训课程上线!

正点原子《ESP32物联网项目实战》全新培训课程上线啦&#xff01;正点原子工程师手把手教你学&#xff01;彻底解决ARM32单片机项目入门难的问题&#xff01; 一、课程介绍 本课程专为ARM32单片机的入门学习者设计&#xff0c;涵盖了环境搭建、编程软件使用、模块基础驱动和多…

Vue3 + ElementPlus 的后台菜单指引

文章目录 需求实现思路 需求 实现思路 引导页用 Drive.js 基本的使用操作这里写了一些菜单使用 ElementPlus 的组件&#xff0c;可以调用组件中暴露的这个方法&#xff0c;具体使用方法在这里说明 二者结合一下&#xff0c;就可以有这样的效果了

10.软件工程知识详解上

软件工程概述 软件开发生命周期 软件定义时期&#xff1a;包括可行性研究和详细需求分析过程&#xff0c;任务是确定软件开发工程必须完成的总目标&#xff0c;具体可分成问题定义、可行性研究、需求分析等。软件开发时期&#xff1a;就是软件的设计与实现&#xff0c;可分成…

MySQL—触发器详解

基本介绍 触发器是与表有关的数据库对象&#xff0c;在 INSERT、UPDATE、DELETE 操作之前或之后触发并执行触发器中定义的 SQL 语句。 触发器的这种特性可以协助应用在数据库端确保数据的完整性、日志记录、数据校验等操作。 使用别名 NEW 和 OLD 来引用触发器中发生变化的记…

水电站/水库大坝安全监测系统完整解决方案

一、背景 在当今社会&#xff0c;随着全球对清洁能源需求的日益增长&#xff0c;水电站作为可再生能源的重要组成部分&#xff0c;其安全稳定运行显得尤为重要。水电站&#xff0c;尤其是大型水库大坝&#xff0c;不仅承载着发电、防洪、灌溉等多重功能&#xff0c;还直接关系…

后端回写前端日期格式化

问题 不进行格式化处理&#xff0c;就会导致传递的字符串很奇怪 解决方案 注解&#xff08;字段&#xff09; <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.2</…

pandas外文文档快速入门

pandas资源可以在github中进行查询 特点&#xff1a; 1、基于numpy库进行开发 2、主要处理一维、二维的数据 3、可以处理sql&#xff0c;execl&#xff0c;时间表等复杂数据结构 导言 创建一维、二维表 创建的表格其实会和二维表很像 假设我们存储一些数据 其中&#xf…

2024年流动式起重机司机证考试题库及流动式起重机司机试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年流动式起重机司机证考试题库及流动式起重机司机试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人员上岗证考试…

单周涨粉过万,这3种AI绘画风格也太火了!

今天给大家分享小红书平台的一些爆款AI绘画类型&#xff0c;如果想要通过AI绘画赚钱或者想要快速起号一定要看&#xff01; 当然&#xff0c;除了小红书平台也可以发一些到其他自媒体平台上&#xff0c;变现方式有很多&#xff0c;可以开店铺卖壁纸、提示词、头像、接定制、合…

Linux centerOS 服务器搭建NTP服务

1&#xff0c;安装 NTP软件 sudo yum -y install ntp2&#xff0c;编辑配置文件 sudo vim /etc/ntp.conf 3&#xff0c;修改配置 在ntp.conf文件中&#xff0c;可以配置服务器从哪些上游时间源同步时间。如果你想让你的服务器对外同步时间&#xff0c;可以去掉restrict d…

蓝象智联与高德签署战略合作,共创时空“数聚港”

9月20日&#xff0c;高德地图副总裁兼高德云图总经理董振宁一行莅临蓝象智联杭州总部进行座谈交流。蓝象智联董事长童玲、CEO徐敏以及合伙人兼算法科学家毛仁歆给予热情接待。在双方的共同见证下&#xff0c;蓝象智联与高德云图签署战略合作协议&#xff0c;标志着双方在数据流…

牛客小白月赛101(上)

tb的区间问题 题目描述 登录—专业IT笔试面试备考平台_牛客网 运行代码 #include <iostream> #include <climits>using namespace std;int main() {int n, k;cin >> n >> k;int arr[50000];for (int i 0; i < n; i) {cin >> arr[i];}int …

从零开始,Docker进阶之路(一):Docker基础

一、简介与概述 1.Docker是一个开源的应用容器引擎&#xff0c;基于Go语言并遵从Apache2.0协议开源。 Docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的Linux机器上&#xff0c;也可以实现虚拟化。 2.Docker的主要…

可视化设计:华为不出,谁与争锋。

在可视化设计上&#xff0c;华为也是独树一帜的存在&#xff0c;本期分享几个华为的可视化作品。

vue echarts tooltip动态绑定模板,并且处理vue事件绑定

先上代码&#xff1a; tooltip: {// 这里是车辆iconshow: true,// trigger: "item",// backgroundColor: "transparent",appendToBody: true,textStyle: {color: "#ffffff" //设置文字颜色},formatter: (params) > {const TruckTooltip Vue.…

# 高可用的并发解决方案nginx+keepalived(三)

高可用的并发解决方案nginxkeepalived&#xff08;三&#xff09; 一、Nginx搭建图片服务器 针对任何站点&#xff0c;几乎都要访问图片&#xff0c;而一个网页里面几乎有好些张图片&#xff0c;这时候会占据大量tomcat连接&#xff0c;造成大量并发&#xff0c;我们可以通过…