Windows内核编程(二)-第一个内核程序

article/2025/10/5 19:03:50

第一个内核程序

通过 Visual Studio新建工程

注意事项:
大部分widnows驱动程序都是内核驱动(Kernel Driver),所以本笔记不分"驱动程序"与"内核编程",也不区分"内核模块"(Kernel Module)、“驱动程序”(Driver)与"内核程序",这些词汇统一指编译出的扩展名为".sys"的可执行文件(并非强制扩展名为.sys),也不区分"应用层"与"用户态"。

驱动分类:

  1. NT驱动 最简单的驱动模型,不支持硬件特性
  2. WDM驱动 在NT驱动的基础上引入的一套驱动模型,支持即插即用、电源事件等特性。
  3. WDF驱动 对WDM驱动的封装与升级,屏蔽了部分细节,简化了大量接口。

笔记中如不是特殊说明,一般都为NT驱动。

打开 Visual Studio 2019 选择"创建新项目",选择 “Empty WDM Driver
在这里插入图片描述
键入工程名字"MyDriver"
在这里插入图片描述
点击"创建"
在这里插入图片描述
创建成功。

然后在菜单中找到“项目§”→“添加新项”,在弹出的对话框中选择“C++文件(.cpp)”,在下方的名称(N)中输入“First.c”,最后点击“添加”。
在这里插入图片描述
现在已经可以看到工程内存在一个空白的First.c文件,开发者可以往这个空白文件中添加内核代码,但在添加代码前,需要包含驱动开发的头文件ntddk.h。

内核入口函数详解

内核驱动入口函数 DriverEntry 原型如下

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)

参数介绍:
DriverObject,表示一个驱动对象的指针,指向操作系统在内存中为该驱动分配的一个类型为PDRIVER_OBJECT数据结构,用于记录该驱动的详细信息。
RegistryPath,表示当前驱动所对应的驱动对象指针。是一个类型为UNCODE_STRING的指针,表示当前驱动所对应的注册表位置。UNICODE_STRING是内核中表示字符串的结构体,定义如下:

typedef struct _UNICODE_STRING {
USHORT Length; //表示Buffer所指向缓冲区中字符串的长度,单位为字节。
USHORT MaximumLength;//表示Buffer所执行的缓冲区的总空间大小,单位为字节。
_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH   Buffer;//指向一个UNICODE类型的字符串缓冲区
} UNICODE_STRING;
typedef UNICODE_STRING* PUNICODE_STRING;

注意:Buffer指向的字符串,并不要求以 ‘\0’ 作为结束,在大多数情况下,Buffer指向的字符串没有以 ‘\0’ 结尾。

RegistryPath 为 PUNICODE_STRING 类型的参数,表示的是这个驱动所对应注册表的位置,这是因为内核驱动时作为 Windows 系统服务 (Service) 存在的,Widnows系统又众多服务,如果从服务运行的环境来区分,服务分为用户态服务、内核态服务,统称为 “服务(Service)”,不同服务通过服务的名字来识别,服务的名称简称 “服务名”。
在安装操作系统后,系统会内置一系列服务,这些服务统称为系统服务,称为第三幅服务。开发者可以开发属于自己的服务,称为第三方服务。一个驱动SYS文件需要运行(加载到内核中),首先需要把这个驱动文件注册(创建)成一个服务(第三方服务),注册成功后,系统会把该服务信息写入到注册表HKEY_LOCAL_MACHINE\ SYSTEM\CurrentControlSet\Services 下,以服务的名字作为一个注册表的键名。

入口函数DriverEntry的返回值类型为NTSTATUS,定义如下
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;,由此可见返回值实际上是一个LONG类型。
Windows操作系统规定DriverEntry返回STATUS_SUCCESS表示成功,返回其他值表示失败。
内核驱动作为Windows服务运行,在执行具体代码前,驱动SYS文件首先会被映射到内核地址空间,作为内核的一个驱动模块(MODULE),接着系统对这个驱动模块执行导入表初始化、修正重定位表中对应的数据偏移等操作,最后系统会调用该驱动模块的DriverEntry入口函数,如果这个入口函数返回STATUS_SUCCESS,系统认为这个驱动初始化成功;如果这个入口函数返回除STATUS_SUCCESS以外的其他值,系统认为驱动初始化失败,系统执行一系列的清理工作,并把驱动模块从内核空间中移除,从用户态角度看,就是服务启动失败。

编写入口函数体

实例:

#include "ntddk.h"VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{if (DriverObject != NULL){DbgPrint("[%ws]Driver Upload,Driver Object Address:%p", __FUNCTIONW__, DriverObject);}return;
}/// @brief 入口函数
/// @param DriverObject 一个驱动对象的指针
/// @param RegistryPath 当前驱动对应的注册表位置
/// @code 
/// typedef struct _UNICODE_STRING {
///		USHORT Length;
///		USHORT MaximumLength;
///		#ifdef MIDL_PASS
///		[size_is(MaximumLength / 2), length_is((Length) / 2)] USHORT* Buffer;
///		#else // MIDL_PASS
///		_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH   Buffer;
///		#endif // MIDL_PASS
///	} UNICODE_STRING;
/// typedef UNICODE_STRING* PUNICODE_STRING;
/// @encode
/// @return 返回一个NTSTATUS值,即 STATUS_SUCCESS 或适当的错误状态NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{// DbgPring函数是WDK提供的API,类似c语言的printf函数// %ws表示打印一个以'\0'结尾的UNICODE字符串// __FUNCTIONW__是以'\0' 结束的UNICODE字符串,表示当前函数的名字,对应格式化字符串中的%wsDbgPrint("[%ws]Hello Kernel World\n", __FUNCTIONW__);// 判断两个参数是否为空,不为空打印两个参数值if (RegistryPath != NULL){// RegistryPath类型为UNICODE_STRING类型,打印该类字符串需要使用%wZ格式化参数DbgPrint("[%ws]Driver RegistryPath:%wZ\n", __FUNCTIONW__, RegistryPath);}if (DriverObject != NULL){DbgPrint("[%ws]Driver Object Address:%p\n", __FUNCTIONW__, DriverObject);// 服务停止回调DriverObject->DriverUnload = DriverUnload;}return STATUS_SUCCESS;
}

注意:不能使用一下方式打印UNICODE_STRING类型的变量,例如不能使用下面的方式打印上面实例中的RegistryPath参数:
DbgPring("[%ws] Driver RegistryPath: %ws\n",__FUNCTIONW__,RegistryPath->Buffer);
因为UNICODE_STRING结构体内Buffer指向的字符串结尾不一定有’\0’,而对于 %ws 类型来说,会一直寻找Buffer字符串的’\0’,在这种情况下,行为是不可预料的。

DriverEntry函数除了打印一系列信息,还有一个重要的操作:
DriverObject->DriverUnload = Unload;
DriverUnload是DriverObject结构体中的一个成员,DriverObject表示当前的驱动对象,记录了当前驱动的详细信息,DriverUnload为驱动对象结构体内的一个函数指针。
驱动是作为服务方式运行的,服务可以被启动,也可以被停止,停止的实质就是系统把该驱动模块对应在内核地址空间中的代码以及数据移除。当一个内核驱动被要求停止时,DriverObject→DriverUnload指向的函数就会被系统调用,开发者可以在这个函数中执行一些清理相关的工作。
DriverUnload函数非常重要,但重要并不等于必须,DriverUnload函数是可选的,开发者可以不提供DriverUnload函数,这样做的结果是该驱动不支持停止,也就是说,只要开发者不提供DriverUnload函数,这个驱动对应的服务一旦启动后,再也无法停止。该特性被很多安全软件利用,刻意不提供DriverUnload函数,避免驱动被恶意停止。
DriverEntry函数执行一系列操作后,最后返回STATUS_SUCCESS,表示驱动初始化成功。DriverEntry函数返回除STATUS_SUCCESS以外的其他值时,表示驱动初始化失败,系统发现驱动初始化失败会移除内核地址空间的驱动代码与数据,这个操作看起来与驱动服务的停止非常类似,但是请读者注意:驱动初始化失败不会触发DriverUnload函数的调用,DriverUnload只有在驱动服务成功启动(初始化)后,被要求停止时才会触发。

编译第一个驱动

通过Visual Studio 编译

点击生成,如果出现以下错误,则删除MyDriver.inf文件即可,Ctrl/Command + ; 输入MyDriver.inf查找文件,右键点中该文件,选择 “删除”。

在这里插入图片描述

编译成功后,读者可以在工程的文件夹目录中找到一个x64的文件夹,在x64目录下找到Debug文件夹,该文件夹下的FirstDriver.sys文件就是编译好的驱动文件,对应的还有FirstDriver.pdb文件,FirstDriver.pdb文件包含了驱动相应的调试信息,如结构体定义、函数名等,在驱动调试中非常重要。该文件夹下还有其他文件,如FirstDriver.cer,可以暂时忽略。

通过WDK直接编译

通过WDK自带的编译程序来进行编译。在编译前,还需要准备一个Sources文件,Sources文件实际上是一个文本文件,文件名字为Sources,该文件主要描述了需要编译的C文件列表,以及需要链接的LIB库等信息,针对本例,Sources文件的内容编写如下:

TARGETNAME=FirstDriver
TARGETTYPE=DRIVER
SOURCES=First.c

TARGETNAME表示编译出来的目标名字;TARGETTYPE表示编译出来的二进制类型是DRIVER,而不是LIB或其他;SOURCES后面是需要编译的C文件,本例中只有一个First.c,如果有多个C文件需要编译,可以写成:

SOURCES=First.c \Second.c

在开始菜单中找到Windows Driver Kit→选择Windows 7的x64 Checked Build Environment。在WDK编译工具的命令行中,使用cd命令进入First.c文件所在的目录,然后在命令行中输入“build”命令开始编译。注意,输入的build命令不带引号。
在这里插入图片描述
在编译过程中会报以下错误:
在这里插入图片描述
这是因为老版本的WDK不支持__FUNCTIONW__标识。这里给读者一个建议,如果编写的驱动代码需要支持不同版本的WDK编译,请不要使用新版本WDK独有的特性。针对上面的问题,请读者修改代码,去掉__FUNCTIONW__以及配套的%ws,或者简单地把DbgPrint注释掉,当然,更好的做法是根据不同WDK版本进行条件编译。修改后重新编译。成功生成驱动文件。
在这里插入图片描述

成功编译后,在First.c的文件夹内会生成一个objfre_wxp_x86\i386文件夹,成功编译后,在First.c的文件夹内会生成一个objchk_win7_amd64文件夹,这个文件夹的命名包含了驱动编译的版本信息,其中chk表示Debug版本,Win7表示使用的是Windows 7版本WDK编译环境,amd64表示64位驱动程序,在objchk_win7_amd64\amd64\文件夹下,生成了FirstDriver.sys文件以及FirstDriver.pdb文件。


http://chatgpt.dhexx.cn/article/EDMcsFSl.shtml

相关文章

Windows内核原理与实现之Windows研究内核(WRK)

Windows并非一个开放源码的操作系统,但正如上一章所提,Microsoft开放了一份以Windows XP x64和Windows Server 2003 SP1为基础的内核源代码,它可以编译和运行,作为教育科研机构的教学实践和研究的平台使用,称为WRK&…

Windows内核结构

Windows内核结构 第一篇博客,随便写下练练手:) Windows内核总共分为三层: 与硬件直接打交道的这一层叫做硬件抽象层简称HAL,这一层的用意就是把所有与硬件相关联的代码逻辑隔离到一个专门的模块中,从而做到尽可能的独立于硬件平…

WINDOWS内核对象及其理解

一.前言 Windows中有很多像进程对象、线程对象、文件对象等等这样的对象,我们称之为Windows内核对象。内核对象是系统地址空间中的一个内存块,由系统创建并维护,这个内存对象是一个数据结构,维护着与对象相关的信息&a…

windows内核——基石

友链 内存布局 用户内存空间和内核内存空间之间的gap是为了避免不经意的越界而导致安全问题 CPU的初始化 系统启动期间,会对所有的处理器进行初始化操作 大部分初始化操作我们都不必了解,因为你了不了解都不影响你的逆向,毕竟你又不是开发…

Windows内核原理与实现--Windows基本结构概述

一、Windows系统结构概述 1、Windows采用双模式来保护操作系统本身,内核模式和用户模式。在Windows中,用户代码和内核代码有各自的运行环境,而且它们可以访问的内存空间也并不相同。在x86中,内核代码可以访问当前进程的4GB虚拟地址空间,而用户代码只能访问底端2GB虚拟地址…

《Windows内核原理与实现笔记》(一)Windows系统结构和基本概念

Windows内核结构 上图是windows内核的组成结构 如图Windows内核分三层,与硬件直接打交道的是硬件抽象层HAL,这一层把所有与硬件相关代码逻辑隔离到一个专门模块中,从而是上层尽可能独立于硬件平台。HAL是一个独立动态链接库,wind…

windows内核基础

windows分层模型 硬件抽象层屏蔽了硬件实现功能的细节。 IRP为内核层重要的数据结构。 物理地址和虚拟地址 x64的cpu仅仅支持64位地址中的前48位。其中若虚拟地址为内核,则前16位为ffff;若虚拟地址为用户模式,则前16位为0000。用户能看到的…

WebService的工作原理

WebService的工作原理如下: 服务提供者WebService2和WebService3通过UDDI协议将服务注册WebService目录中服务消费者WebService1通过UDDI协议从WebService目录中查询服务,并获得服务的WSDL服务描述文件服务消费者WebService1通过WSDL语言远程调用WebSer…

WebService原理

1、WebService实际上就是两个应用程序之间的远程调用,而且这种调用是跨语言的。 2、应用程序调用WebService的接口,实际上就是解析XML语言。也就是说两个应用程序之间的交流实际上就是通过XML来交流的。 3、WebService内部的实现是基于HTTP协议的&…

Web Service 的工作原理

Web Service基本概念 Web Service也叫XML Web Service WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独立的通讯技术。是:通过SOAP在Web上提供的软件服务,使用WSDL文件进行说明,并通过UDDI进行注册。…

JavaScript高级程序设计(第三版)pdf版 下载

分享关于JavaScript高级程序设计(第3版)[美] Nicholas C.Zakas著 李松峰 曹力 译 一书供大家学习!!! 链接: https://pan.baidu.com/s/1RD4EXuQnTqH3kUfHWFaOYw 提取码: vias 复制这段内容后打开百度网盘手机App&#…

JS高级程序设计(14)

DOM 文档对象模型(DOM)是HTML和XML文档的编程借口。DOM表示由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。 文章目录 DOM一、节点层级1.Node类型2.Document类型3.Element类型4.Text类型5.Comment属性6.CDATASectio…

javascript高级程序设计(红宝书)记录

故心故心故心故心小故冲啊 文章目录 第3章.语法基础第4章.变量作用域与内存第5章.基本引用类型第6章.集合引用类型第8章.对象、类与面向对象编程第10章.函数第11章.期约与异步函数第12章.BOM第17章事件第24章.网络请求与远程资源第25章.客户端存储 第3章.语法基础 undefined 值…

JavaScript高级编程

原文地址: http://www.onlamp.com/pub/a/onlamp/2007/07/05/writing-advanced-javascript.html Web应用程序(Web Applications) 从计算机纪元的黎明刚刚来临开始,不同平台间软件的互用性就一直是关注的焦点。为了尽可能实…

javascript高级程序设计 第三版

网盘地址 提取码&#xff1a;vh81 笔记 第二章 2.1script标签 <script>元素属性&#xff1a;async、charset、defer、language、src、type async和defer只对外部脚本有效&#xff0c;language已废弃&#xff0c;type默认为“text/javascript”defer属性可以让脚本在文…

js高级程序设计(一) —— js简介

学习《JavaScript高级程序设计》的知识总结&#xff0c;以及对部分内容的扩展~ 1、一个完整的js实现的三个部分 核心&#xff08;ECMAScript) 文档对象模型&#xff08;DOM) 浏览器对象模型&#xff08;BOM) 1-1&#xff1a;ECMAScript ECMA-262的近一版是第 5版&#xf…

JS高级程序设计(12)

BOM 浏览器对象模型BOM提供了与网页无关的浏览器功能对象。 文章目录 BOM一、window对象1.Global作用域2.窗口关系3.窗口位置与像素比4.窗口大小5.视口位置6.导航与打开新窗口7.定时器8.系统对话框 二、location对象1.查询字符串2.操作地址 三、navigator对象1.检测插件2.注册…

JavaScript 高级程序设计

理解原型对象 无论什么时候&#xff0c;只要创建一个新函数&#xff0c;就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。 在默认的情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个包含一个指向prototyp…

js高级程序设计(第一章)

1.什么是JavaScript 1.1 简短的历史回顾 出现背景&#xff1a;当时&#xff0c;验证简单的表单&#xff0c;需要大量与服务器的往返通信成为用户的痛点。 出现时间&#xff1a;1997 年&#xff0c;JavaScript 1.1 作为提案被提交给欧洲计算机制造商协会&#xff08;Ecma&…

JavaScript高级程序

文章目录 1. JavaScript 实现1.1 ECMAScript1.2 DOM1.3 BOM1.4 小结 1. JavaScript 实现 虽然 JavaScript 和 ECMAScript 基本上是同义词&#xff0c;但 JavaScript 远远不限于 ECMA-262 所定义的那样。 完整的 JavaScript 实现包含以下几个部分:  核心(ECMAScript)  文档…