Windows内核(1)——内核初探&编写第一个驱动程序
Windows内核
内核杂记
内核(Kernel)
安全角度:Rootkit,漏洞利用,病毒
Rootkit(也称隐匿软件[1])是指主要为隐藏其他程序进程的软件,可能是一种或以上软件的组合;广义而言,Rootkit也可视为一项技术。今天,Rootkit一词更多指伪装成驱动程序加载到操作系统内核中的恶意软件,其代码在特权模式运行,能造成意外危险。
三种模式
CPU的三种模式:
- 实模式(Real Mode)
实模式是x86处理器的初始模式,也是最早期的处理模式。在实模式下,CPU的特性和限制主要包括:
- 地址空间:只有20位地址总线,因此可以访问的物理内存最多为1MB。
- 段式内存管理:内存地址通过段寄存器和偏移量组合计算,例如,地址由段寄存器(16位)乘以16再加上偏移量(16位)组成。
- 无内存保护:没有内存保护机制,任何程序可以访问所有内存区域,包括操作系统和硬件设备的内存。
- 简单的中断处理:中断向量表固定在内存地址0x0000到0x03FF之间。
- 保护模式(Protected Mode)
保护模式是x86处理器的一种增强模式,提供了更强大的内存管理和保护功能。保护模式的主要特性包括:
- 32位地址空间:支持32位地址,总共可以访问4GB的内存。
- 分页和分段内存管理:支持复杂的内存管理机制,包括分段(Segmented)和分页(Paging)。分段机制通过段描述符表(GDT和LDT)管理内存,分页机制则通过页表管理内存。
- 内存保护:提供了内存保护机制,每个段和页都有权限设置,可以防止进程间的内存访问冲突。
- 虚拟内存:支持虚拟内存,通过分页机制实现进程的地址空间隔离和内存扩展。
- 多任务:支持硬件级别的多任务处理,通过任务状态段(TSS)实现任务切换。
- 虚拟86模式(Virtual 8086 Mode, V86 Mode)
虚拟86模式是在保护模式下的一种特殊模式,用于运行实模式程序。其主要特性包括:
- 实模式兼容:允许实模式程序在保护模式下运行,提供了对老旧DOS程序和设备驱动的支持。
- 内存保护:虽然运行在实模式兼容环境中,但依然享有保护模式的内存保护机制。
- 多任务支持:可以在保护模式的多任务环境中同时运行多个虚拟86模式的任务,每个任务都有自己的虚拟地址空间。
现代的Windows操作系统运行在保护模式下(Protected Mode)。保护模式提供了多任务处理、内存保护和虚拟内存等关键功能,这些功能是现代操作系统稳定性、安全性和多任务能力的基础。
驱动和内核的关系
内核:
内核是操作系统的核心部分,负责管理系统资源和硬件抽象层。内核的主要职责包括:
- 进程管理:负责进程的创建、调度和终止。
- 内存管理:管理系统内存的分配和回收,提供虚拟内存支持。
- 设备管理:通过驱动程序与硬件设备交互。
- 文件系统管理:提供文件系统接口,管理文件和目录的读写操作。
- 安全和权限管理:控制系统资源的访问权限,保证系统安全。
驱动程序(Driver)
驱动程序是用于控制和操作硬件设备的特殊软件模块。驱动程序运行在内核模式下,直接与硬件进行通信。驱动程序的主要职责包括:
- 设备初始化和配置:在系统启动时初始化硬件设备,并进行必要的配置。
- 设备控制:响应操作系统和应用程序的请求,执行设备的读写操作。
- 中断处理:处理硬件设备发出的中断信号,确保设备能够及时响应事件。
- 数据传输:在硬件设备和系统内存之间传输数据。
驱动程序和内核之间的关系是紧密而复杂的。内核提供了驱动程序运行的环境和基础设施,而驱动程序则负责具体的硬件控制和操作。两者共同合作,确保操作系统能够高效、稳定地管理和使用硬件资源。内核通过标准化的接口与驱动程序通信,驱动程序则实现这些接口来操作硬件,从而实现硬件和软件之间的有效协作。
CPU中的环(Rings)概念是指处理器权限级别的一种分层机制,通常用于保护模式下的操作系统。环的设计目的是为了增强系统安全性和稳定性,通过限制不同级别的代码对系统资源和硬件的访问。x86架构的处理器通常定义了四个环,从Ring 0到Ring 3,每个环对应不同的权限级别。
Ring 0:内核模式(Kernel Mode)
- 最高权限级别:Ring 0拥有最高的权限,允许执行任何指令和访问所有硬件资源。
- 运行内容:操作系统内核和一些关键的驱动程序运行在Ring 0,因为它们需要直接管理硬件和系统资源。
- 特性:能够直接访问内存、I/O端口、CPU状态寄存器等关键资源。错误或恶意代码在Ring 0运行可能会导致系统崩溃或被完全控制。
Ring 1 和 Ring 2:中间层(Intermediate Levels)
- 中间权限级别:Ring 1和Ring 2权限介于Ring 0和Ring 3之间。
- 运行内容:通常较少使用,在一些操作系统中用于设备驱动程序或特权较低的系统服务。
- 特性:访问权限比Ring 0低,但比Ring 3高。在实际操作系统中,这两个环通常被简化或合并,更多依赖Ring 0和Ring 3的分离。
值得注意的是,Ring1和Ring2似乎出现的概率并不高,甚至在64位好像已经没有Ring1,Ring2的概念了
Ring 3:用户模式(User Mode)
- 最低权限级别:Ring 3拥有最低的权限,限制最严格,主要用于运行用户应用程序。
- 运行内容:大多数应用程序和一些用户级别的驱动程序运行在Ring 3,不能直接访问硬件和系统资源。
- 特性:通过系统调用与内核交互,不能直接访问内存和硬件。这样可以防止用户级别的代码对系统核心资源造成破坏,增强系统的安全性和稳定性。
那么有个问题,应用程序的代码,有没有机会进入 ring0 ?
如果按照:程序代码(ring3)-> 系统调用 (API (syscall))-> 系统代码(ring0) -> 操控硬件
那么确确实实是没有机会But,系统作者给了机会
系统作者(微软)本身不生产硬件,或者硬件不是全都由它生产,那么它就需要兼容各个硬件
微软通过定义标准,硬件产商遵守这套标准,这样就可以让操作系统控制硬件
总结就是
系统(LoadLibrary) 厂家(Dll) 操控硬件
系统(Load Kenel Moudle) 厂家(Kernel Moudle) 操控硬件
那么为什么驱动做成dll,ring3加载不行吗?必须放在0环?这就是权限问题。
那么厂家就需要做一个内核模块,让系统去加载,这样就能运行ring0的代码
内核模块在Windows就叫驱动(Driver)
一个键盘,有额外的扩展按键,不装厂家的驱动可以用吗?当然可以,但是使用不了额外扩展的按键
WDK介绍
WDK(Windows Driver Kit,Windows 驱动程序开发工具包)是微软提供的一套工具和库,用于开发、测试和调试Windows操作系统上的设备驱动程序。WDK包含了开发驱动程序所需的所有必要工具、文档和示例代码。
主要功能和组件
- 编译工具:包含编译驱动程序所需的编译器、链接器和其他工具,这些工具与Visual Studio集成,支持C和C++编程语言。
- 驱动程序模板和示例:提供了一系列预定义的驱动程序模板和示例代码,帮助开发人员快速开始开发不同类型的驱动程序。
- 头文件和库:包含了开发驱动程序所需的各种头文件和库文件,使开发人员能够方便地调用Windows内核和驱动程序框架提供的API。
- 测试和验证工具:提供了一系列测试和验证工具,如Driver Verifier、Static Driver Verifier(SDV)、Windows Hardware Lab Kit(HLK)等,帮助开发人员检测和修复驱动程序中的错误和兼容性问题。
- 调试工具:包含调试驱动程序的工具,如WinDbg调试器,通过符号表和内核调试接口,开发人员可以对驱动程序进行详细的调试和分析。
- 文档和帮助文件:提供了详细的文档和帮助文件,涵盖了驱动程序开发的各个方面,包括编程指南、API参考、最佳实践等。
驱动程序类型
WDK支持开发多种类型的驱动程序,包括但不限于:
- 设备驱动程序:如USB驱动程序、网络驱动程序、存储驱动程序等。
- 文件系统驱动程序:如文件系统过滤驱动程序、虚拟文件系统驱动程序等。
- 内核模式驱动程序:直接与Windows内核交互的驱动程序。
- 用户模式驱动程序:在用户模式下运行,具有较低权限,提供对特定硬件设备的访问。
内核研究的是啥,研究的就是系统代码如何修改
驱动研究的是啥,研究的是硬件
但是玩内核也要学驱动,因为需要驱动程序去实现内核目标
但是加载驱动不是玩内核的唯一方法,还有方法就是API,一旦API存在bug,系统有漏洞,也是有可能进入内核的,这样就不需要内核,当然这种机会渺茫
编写第一个驱动程序
IDE创建
高版本写驱动程序就可以使用IDE,也就是可以用VS来写程序
选择一个空的驱动程序(Empty WDM Driver),因为其他框架几乎都是涉及到某些具体硬件
第一个驱动程序的代码:
1 |
|
这里需要注意的是,可以直接创建后缀为.c的文件,如果后缀是.cpp,需要在DriverEntry前增加extern “C”,否则会报错 ,因为Cpp把函数名字给粉碎了,导致找不到入口
更好的做法是做一个头文件:
把所有函数都用C命名方式也行
值得注意的是,最好把ntddk.h这个头文件也包含进来,因为如果用到了里面的函数,也会被名称粉碎
这里可以选择需要生成的平台,操作系统一般是向下兼容的,比如是这里显示Windows7,那么高于或者等于Windows7的系统都可以运行这个驱动程序(或者说不去调用新版的API,低于Windows10也可以用)
注意注意,我加载的驱动是在XP平台上的,所以一定要选Windows7,否则会蓝屏
然后直接编译会报错:
解决方法是直接把这个inf文件删掉
然后还可能会报错 缺少缓解库 :
解决方法是:
要么去下载SDK版本对应的缓解库
要么就直接在属性
把漏洞缓解库检查disable掉即可。但是内存数据有泄露风险
驱动程序的检查十分的严格,如果传入的参数没有使用到,那么就需要删掉
或者加一个宏
1 | void Unload(IN PDRIVER_OBJECT DriverObject) |
代码组成
ntddk.h 内核开发所需要的头文件
DriverEntry
在之前写程序时,程序入口
函数为main
,参数有argc和argv代表着命令行的参数个数及对应的字符串指针,驱动
也有入口
函数为DriverEntry
,其返回类型为NTSTATUS
。
函数的第一个参数pDriverObj
表示一个驱动对象的指针,可以简单认为,一个驱动文件(sys)运行之后,操作系统在内存中为该驱动分配了一个类型为DRIVER_OBJECT的数据结构,用于记录该驱动的详细信息
函数的第二个参数pRegistryString
是一个类型为UNICODE_STRING的指针,表示当前驱动所对应的注册表位置。UNICODE_STRING是内核中表示字符串的结构体,对应定义如下:
1 | typedef struct _UNICODE_STRING { |
DriverUnload
是驱动卸载的回调,如果我们不设置DriverUnload,那么此时我们将无法正常的卸载驱动,系统这么做的原因是为了保证系统的稳定性。
1 | VOID DriverUnload(PDRIVER_OBJECT DriverObject) |
1 | DriverObject->DriverUnload = DriverUnload; |
当我们在DriverEntry中添加了某些系统回调,此如果时我们没有DriverUnload,因此系统不知道什么时候该移除这些回调,如果暴力移除驱动,此时系统回调会出问题,系统回调表中存在了一个被移除掉的驱动的回调,当调用时系统蓝屏 。
有时候驱动程序需要卸载。在卸载驱动时(关闭服务),DriverObject->DriverUnload函数会被调用,以便执行一些清理操作。需要注意的是,如果未在DriverUnload中执行清理工作,会产生泄漏,在下一次重启之前,内核无法清除这些泄漏。(这一点不像用户层编程,在进程退出后,资源会释放,但是内核层不会自己释放。)
DriverUnload函数非常重要,但DriverUnload函数是可选的,开发者可以不提供DriverUnload函数,这样做的结果是该驱动不支持停止,也就是说,只要开发者不提供DriverUnload函数,这个驱动对应的服务一旦启动后,再也无法停止。该特性被很多安全软件利用,刻意不提供DriverUnload函数,避免驱动被恶意停止。
需要注意的是:
驱动初始化失败不会触发DriverUnload函数的调用,DriverUnload只有在驱动服务成功启动(初始化)后,被要求停止时才会触发。
返回值NTSTATUS,NTSTATUS实际是一个LONG类型,定义如下:
1 | 1 typedef LONG NTSTATUS; |
DriverEntry返回STATUS_SUCCESS表示成功,返回其他值表示失败。
STATUS_SUCCESS定义如下:
1 | 1 #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth |
简单来说,内核驱动作为Windows服务运行,在执行具体代码前,驱动SYS文件首先会被映射到内核地址空间,作为内核的一个驱动模块(MODULE),接着系统对这个驱动模块执行导入表初始化、修正重定位表中对应的数据偏移等操作,最后系统会调用该驱动模块的DriverEntry 入口函数,如果这个入口函数返回STATUS_SUCCESS,系统认为这个驱动初始化成功;如果这个入口函数返回除STATUS_SUCCESS以外的其他值,系统认为驱动初始化失败,系统执行一系列的清理工作,并把驱动模块从内核空间中移除,从用户态角度看,就是服务启动失败。
DbgPrint函数
DbgPrint函数是WDK提供的API,类似用户层的OutputDebugString函数
查看效果
打开Debug View,把捕获内核的开关打开,这样才能捕捉到内核信息
因为是32位的驱动程序,因此我们选择用XP系统进行安装,启动,停止,卸载