Windows内核

内核杂记

内核(Kernel)

安全角度:Rootkit,漏洞利用,病毒

Rootkit(也称隐匿软件[1])是指主要为隐藏其他程序进程软件,可能是一种或以上软件的组合;广义而言,Rootkit也可视为一项技术。今天,Rootkit一词更多指伪装成驱动程序加载到操作系统内核中的恶意软件,其代码在特权模式运行,能造成意外危险。

三种模式

CPU的三种模式:

  1. 实模式(Real Mode)

实模式是x86处理器的初始模式,也是最早期的处理模式。在实模式下,CPU的特性和限制主要包括:

  • 地址空间:只有20位地址总线,因此可以访问的物理内存最多为1MB。
  • 段式内存管理:内存地址通过段寄存器和偏移量组合计算,例如,地址由段寄存器(16位)乘以16再加上偏移量(16位)组成。
  • 无内存保护:没有内存保护机制,任何程序可以访问所有内存区域,包括操作系统和硬件设备的内存。
  • 简单的中断处理:中断向量表固定在内存地址0x0000到0x03FF之间。
  1. 保护模式(Protected Mode)

保护模式是x86处理器的一种增强模式,提供了更强大的内存管理和保护功能。保护模式的主要特性包括:

  • 32位地址空间:支持32位地址,总共可以访问4GB的内存。
  • 分页和分段内存管理:支持复杂的内存管理机制,包括分段(Segmented)和分页(Paging)。分段机制通过段描述符表(GDT和LDT)管理内存,分页机制则通过页表管理内存。
  • 内存保护:提供了内存保护机制,每个段和页都有权限设置,可以防止进程间的内存访问冲突。
  • 虚拟内存:支持虚拟内存,通过分页机制实现进程的地址空间隔离和内存扩展。
  • 多任务:支持硬件级别的多任务处理,通过任务状态段(TSS)实现任务切换。
  1. 虚拟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包含了开发驱动程序所需的所有必要工具、文档和示例代码。

主要功能和组件

  1. 编译工具:包含编译驱动程序所需的编译器、链接器和其他工具,这些工具与Visual Studio集成,支持C和C++编程语言。
  2. 驱动程序模板和示例:提供了一系列预定义的驱动程序模板和示例代码,帮助开发人员快速开始开发不同类型的驱动程序。
  3. 头文件和库:包含了开发驱动程序所需的各种头文件和库文件,使开发人员能够方便地调用Windows内核和驱动程序框架提供的API。
  4. 测试和验证工具:提供了一系列测试和验证工具,如Driver Verifier、Static Driver Verifier(SDV)、Windows Hardware Lab Kit(HLK)等,帮助开发人员检测和修复驱动程序中的错误和兼容性问题。
  5. 调试工具:包含调试驱动程序的工具,如WinDbg调试器,通过符号表和内核调试接口,开发人员可以对驱动程序进行详细的调试和分析。
  6. 文档和帮助文件:提供了详细的文档和帮助文件,涵盖了驱动程序开发的各个方面,包括编程指南、API参考、最佳实践等。

驱动程序类型

WDK支持开发多种类型的驱动程序,包括但不限于:

  • 设备驱动程序:如USB驱动程序、网络驱动程序、存储驱动程序等。
  • 文件系统驱动程序:如文件系统过滤驱动程序、虚拟文件系统驱动程序等。
  • 内核模式驱动程序:直接与Windows内核交互的驱动程序。
  • 用户模式驱动程序:在用户模式下运行,具有较低权限,提供对特定硬件设备的访问。

内核研究的是啥,研究的就是系统代码如何修改

驱动研究的是啥,研究的是硬件

但是玩内核也要学驱动,因为需要驱动程序去实现内核目标

但是加载驱动不是玩内核的唯一方法,还有方法就是API,一旦API存在bug,系统有漏洞,也是有可能进入内核的,这样就不需要内核,当然这种机会渺茫

编写第一个驱动程序

IDE创建

高版本写驱动程序就可以使用IDE,也就是可以用VS来写程序

选择一个空的驱动程序(Empty WDM Driver),因为其他框架几乎都是涉及到某些具体硬件

1720535897376

第一个驱动程序的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<ntddk.h>

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
if (DriverObject != NULL)
{
DbgPrint("Driver Unload...Driver Object Address: %p\n", DriverObject);
}

return;
}

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("Hello World\n");

if (RegistryPath != NULL)
{
DbgPrint("Driver RegistryPath: %wZ\n", RegistryPath);
}

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

return STATUS_SUCCESS;
}

这里需要注意的是,可以直接创建后缀为.c的文件,如果后缀是.cpp,需要在DriverEntry前增加extern “C”,否则会报错 ,因为Cpp把函数名字给粉碎了,导致找不到入口

更好的做法是做一个头文件:

把所有函数都用C命名方式也行

1720578835891

值得注意的是,最好把ntddk.h这个头文件也包含进来,因为如果用到了里面的函数,也会被名称粉碎

1720596657472

这里可以选择需要生成的平台,操作系统一般是向下兼容的,比如是这里显示Windows7,那么高于或者等于Windows7的系统都可以运行这个驱动程序(或者说不去调用新版的API,低于Windows10也可以用)

注意注意,我加载的驱动是在XP平台上的,所以一定要选Windows7,否则会蓝屏

然后直接编译会报错:

1720536718407

解决方法是直接把这个inf文件删掉

然后还可能会报错 缺少缓解库 :

1720578344766

解决方法是:

要么去下载SDK版本对应的缓解库

要么就直接在属性
1720578414343

把漏洞缓解库检查disable掉即可。但是内存数据有泄露风险

驱动程序的检查十分的严格,如果传入的参数没有使用到,那么就需要删掉

或者加一个宏

1
2
3
4
5
void Unload(IN PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
DbgPrint("Hello World");
}

代码组成

ntddk.h 内核开发所需要的头文件

DriverEntry

在之前写程序时,程序入口函数为main,参数有argc和argv代表着命令行的参数个数及对应的字符串指针,驱动也有入口函数为DriverEntry,其返回类型为NTSTATUS

函数的第一个参数pDriverObj 表示一个驱动对象的指针,可以简单认为,一个驱动文件(sys)运行之后,操作系统在内存中为该驱动分配了一个类型为DRIVER_OBJECT的数据结构,用于记录该驱动的详细信息

函数的第二个参数pRegistryString 是一个类型为UNICODE_STRING的指针,表示当前驱动所对应的注册表位置。UNICODE_STRING是内核中表示字符串的结构体,对应定义如下:

1
2
3
4
5
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWCH Buffer; //PWCH -> wchar_t* 不要求以'\0'结束
} UNICODE_STRING

DriverUnload

是驱动卸载的回调,如果我们不设置DriverUnload,那么此时我们将无法正常的卸载驱动,系统这么做的原因是为了保证系统的稳定性。

1
2
3
4
5
6
7
8
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
if (DriverObject != NULL)
{
DbgPrint("Driver Unload...Driver Object Address: %p\n", DriverObject);
}
return;
}
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,把捕获内核的开关打开,这样才能捕捉到内核信息

1720576990120

因为是32位的驱动程序,因此我们选择用XP系统进行安装,启动,停止,卸载

1720576913786