Windows内核理论基础学习
文章目录
前言
近俩天的学习-Windows内核基础的学习笔记。算是一个概述,毕竟每一个小点单拉出来都可以讲好久。学了这俩天,对内核的理解算是清晰了那么一点点了,持续学习ing…欢迎师傅一起交流讨论私信andwx都欢迎。感谢。
参考文献:
《加密与解密》Windows内核基础
《Windows内核原理与实现》系统总述
《深入理解Windows操作系统》
Chat Gpt…
MSDN文档。
一些零零散散的大佬的blog。感谢。
Windosw内核 理论基础
Windows体系结构
CPU权限级别
系统内核层,又称零环(Ring0,简称R0;与此对应的是3环,R3,应用层);实际上是CPU的4个级别,CPU在设计时将CPU的运行级别从内到外分为4个层级R0-R3,R0权限最高依次降低(实际上,现在只区分R0和R3,并没有使用R1和R2)。
分级的目的是为了保护系统的稳定性和安全性。通过限制某些操作只能在高权限级别下执行,操作系统可以防止用户级应用程序意外或恶意地修改关键系统资源,进而导致系统崩溃或安全漏洞。
Windows 体系结构简图:
从上图可以看到,Windows使用双模式来保护操作系统本身,用户模式对应的就是R3,内核模式即R0。在这种架构下,应用程序的代码只能运行在用户模式下,每当它需要使用到系统内核或内核的扩展模块(内核驱动程序)所提供的服务时,应用程序通过硬件指令从用户模式切换到内核模式中;当系统内核完成了所请求的服务以后,控制权又回到用户模式代码。
内存空间布局
Windows中,用户代码和内核代码有各自的运行环境,并且它们可以访问的内存空间也不同。
以x64为例,在4G的虚拟内存空间中,WIndows系统的内存分为内核空间和应用空间,每部分各占2GB。
其中用户空间占用低地址(00000000 ~ 7FFFEFFF),内核空间占用高地址(7FFF000 ~ FFFFFFFF);若是开启了大地址空间模式的程序(LARGE_ADDRESS_AWARE),则内存空间布局会变成3GB 的用户空间,和 1GB 的内核空间。
Windows内核结构
如图,Windows内核中主要可以分成三层:硬件抽象层(HAL),内核层(也称微内核micro-kernel),执行体层。
内核层实现操作系统的基本机制,而所有的策略决定则留给执行体。执行体中的对象绝大多数封装了一个或者多个内核对象,并且通过某种方式(比如对象句柄)暴露给应用程序。
Windows内核为用户模式提供了一组系统服务,供应用程序使用内核中的功能。应用程序通常并不直接调用这些系统服务,而是通过一组系统DLL,最终通过ntdll.dll切换到内核模式下的执行体API函数中,以调用内核中的系统服务。
Ntdll.dll是链接用户模式代码和内核模式系统服务的桥梁。对于内核提供的每一个系统服务,该DLL都提供一个相应的存根函数,这些存根函数的名称以“Nt”作为前缀,例如NtCreateProcess
、NtOpenFile
等。另外ntdll.dll还提供了许多系统级的支持函数,比如映像加载器函数(以“Ldr”为前缀)、系统时间函数(以“Etw”为前缀),以及一般的运行支持函数(以“Rtl”为前缀)和字符串支持函数等。
执行体API函数接收的参数来自于各种应用程序,因此为了保证系统的安全以及抵抗来自用户模式的恶意攻击,所有的执行体API都必须保证参数的有效性。通常执行体系统服务函数会在其开始处,对所接收的参数逐一探查它们的可访问性。例如如下代码:
PreviousMode = KeGetPreviousMode();
if (PreviousMode != kernelMode){
try{
ProbeForWrite(InputInformation,InputInformationLength,sizeof(ULONG));
if (ARGUMENT_PRESENT(ReturnLength)){
ProbeForWriteUlong(ReturnLength);
} except(EXCEPTION_EXECUTE_HANDLER){
return GetExceptionCode();
}
}
}
解析:
- 调用
KeGetPreviousMode
函数来获取当前的执行模式(内核模式or用户模式) - 用
if
判断是否是内核模式(kernelMode) - 使用了一个
try{...}except(){...}
异常处理,try中包含一些堆内存的探测(Probe)操作 ProbeForWirte
是一个检查内存可写性的函数,用于检查InputInformation
指向的内存区域是否可以安全写入数据,第二三个参数分别是数据的长度和数据的大小。- 接着检查是否传递了
ReturnLength
参数,如果传递了就进一步检查内存可写性 - except 如果触发了异常,用
GetExceptionCode
获取错误码
Windows内核中的关键组件:
硬件抽象层(HAL)
硬件抽象层(Hardware Abstraction Layer,HAL),这一层把所有与硬件相关联的代码逻辑隔离到一个专门的模块中,为操作系统的上层提供一个抽象的、一致的硬件资源模型。这使得上层的模块无须考虑硬件的差异,它们通过HAL而不是直接访问硬件。
在Windows中,HAL是一个独立的动态链接库。HAL提供了一些例程供其他内核模块或设备驱动程序调用,这使得一个驱动程序可以支持同样的设备在各种硬件平台上运行。HAL不仅涵盖了处理器的体系结构,也涉及了中断控制器、单处理器或多处理器等硬件条件。
内核层
这是大内核中的小内核,也称微内核。它是内核模块ntoskrnl.exe的下层部分(上层为执行体),最接近HAL层,负责线程调度和中断、异常的处理。对于多处理器系统,它还负责同步处理器之间的行为,以优化系统的性能。
Windows的内核实现了抢占式线程调度机制,按照优先级顺序将线程分配到处理器上,并且允许高优先级的线程中断或抢占低优先级的线程。每个线程有一个基本优先级值(base priority)和一个动态优先级值。根据这俩个值,内核根据调度规则来切换线程,让系统更快响应用户的动作,以及在系统服务和其他低优先级进程之间平衡处理器资源的分配。
Windows内核按照面向对象的思想来设计,它管理俩种类型的对象:分发器对象和控制对象。分发器对象实现了各种同步功能,这些对象的状态会影响线程的调度。Windows内核实现的分发器对象包括事件(event)、突变体(mutant)、信号量(semaphore)、进程(process)、线程(thread)、队列(queue)、门(gate)和定时器(timer)。控制对象被用于控制内核的操作但是不影响线程的调度,它包括异步过程调用(APC)、延迟过程调用(DPC),以及中断对象等。
执行体层
执行体是内核模块ntoskrnl.exe的上层部分,它包含5种类型的函数:
- 系统服务调度函数(System Service Dispatch Functions):这些函数主要负责响应系统服务请求。当用户模式应用程序请求操作系统服务时,这些函数会被调用。例如
NtCreateFile
、NtReadFile
等函数。 - 内核模式支持函数(Kernel Mode Support Functions):这类函数提供给其他内核模式组件使用,以执行各种底层任务,如内存管理、进程和线程管理等。例如
ExAlloctePool
:分配内核池内存、KeSetEvent
:设置一个事件对象的状。 - 执行对象管理函数(Executive Object Management Functions):这些函数用于管理Windows中的各种执行对象,如进程、线程、事件、信号量等。例如
ObOpenObjectByPointer
:根据对象指针打开对象、ExCreateCallback
:创建一个回调对象。 - 安全引用监视器函数(Security Reference Monitor Functions):这些函数用于实现操作系统的安全机制,包括访问控制、权限检查等。例如
SeAccessCheck
:检查访问权限。 - I/O系统支持函数(I/O System Support Functions):这些函数支持输入/输出系统的操作,包括文件系统的管理、设备驱动程序的交互等。例如
IoWriteErrorLogEntry
:写入错误日志条目。
关于函数前缀,这些函数都以固定的前缀开始,分别属于内核中不同的管理模块:
与应用层函数不同,在windows操作系统中调用内核函数必须要关注中断请求级别(IRQL,Interrupt Request Level)。
IRQL 是一个表示中断优先级的数字,用于确保处理器在处理不同的任务时维持正确的操作顺序和安全性。操作系统内核使用不同的 IRQL 来管理对硬件资源的访问,以及处理不同级别的中断和异常。
在调用内核函数时,必须要确保当前的IRQL与被调用函数所要求的级别相符。不遵守这一规则可能导致系统崩溃或数据损坏。IRQL的级别如下:
- 被动级别(PASSIVE_LEVEL):这是最低的IRQL级别。在此级别,线程可以被抢占,可以执行任何类型的内核模式代码,包括页面操作。
- APC级别(APC_LEVEL):此级别用于阻止异步过程调用(APC)的执行。
- 调度级别(DISPATCH_LEVEL):在此级别,可以阻止线程调度,但仍允许处理硬件中断。很多非分页内存操作需要在此级别或更低级别执行。
- DIRQL(设备IRQL):这是特定于设备的中断级别。不同的设备驱动程序可能会使用不同的DIRQL。
- 高IRQL(HIGH_LEVEL):这是最高的IRQL级别,用于系统关键操作,此时几乎所有的中断都被禁止。
开发内核模式驱动程序时,合理地管理IRQL至关重要。如果一个函数要求在低IRQL下运行,而当前IRQL较高,就不能直接调用那个函数;反之亦然。不正确的IRQL处理可能导致系统不稳定或蓝屏(BSOD)。
执行体中除了函数组成,还包含了如图中的多个重要的组件,以下:
- 内存管理器:实现了虚拟内存管理,既负责系统地址空间的内存管理,又为每个进程提供了一个私有的地址空间,支持进程之间的内存共享。内存管理器也为缓存管理器提供了底层支持。
- 缓存管理器:它为文件系统提供了统一的数据缓存支持,允许文件系统驱动程序将磁盘上的数据映射到内存中,并通过内存管理器来协调物理内存的分配。
- 文件系统:管理文件和目录的创建、读写和组织。
- 进程和线程管理器:负责进程线程的创建和终止。在Windows中,对于进程和线程的底层支持是在内核层提供的,执行体只是在其基础上提供了一些语义和功能。
- 即插即用管理器:负责列举设备,加载并初始化设备所需的驱动程序。还负责检测系统中的设备变化。
- 安全引用监视器(SRM):该组件强制在本地计算机上实施安全策略,它守护着操作系统的资源,执行对象的保护和审计。
- 配置管理器:管理系统注册表,提供系统配置和启动信息。
- I/O管理器:实现了与设备无关的输入和输出功能,负责将I/O请求分发给正确的设备驱动程序以便进一步处理。
- 对象管理器:它负责创建、管理、删除Windows执行体对象,以及用于表达操作系统资源的抽象数据类型,比如进程、线程和各种同步对象。
- 局域网管理器(本地过程调用,LPC):负责处理进程之间的通信,管理消息传递和远程过程调用。
设备驱动程序
在内核中除了内核模块ntoskrnl.exe和HAL以外,其他模块几乎都以设备驱动程序的形式存在。
Windows中的设备驱动程序,并不一定对应物理设备;它既可以创建虚拟设备,也可以与设备无关,它仅仅是内核的扩展模块。从软件结构角度而言,可以认为设备驱动程序是Windows内核的一种扩展机制,系统通过设备驱动程序来支持新的物理设备或者扩展功能。
设备驱动程序是可以加载到系统中的模块,其文件扩展名为**.sys**,其格式是标准的 PE文件格式。驱动程序中的代码运行在内核下,尽管它们可以直接操纵硬件,但理想的情况是,调用HAL中的函数与硬件打交道,因此,驱动程序往往用C/C++语言来编写,从而可以方便地在Windows所支持的体系结构之间进行源代码层次上的移植。
根据设备驱动程序的功能和行为可以将设备驱动程序分为三类:
- 即插即用驱动程序:支持即插即用技术的驱动程序。它们可以在设备连接到计算机时自动被识别和配置,无需用户手动干预。
- 非即插即用驱动程序:不支持即插即用技术的驱动程序。可能在安装时需要用户手动配置。
- 文件系统驱动程序:专门用于处理文件操作的驱动程序,如管理文件的存取、文件系统的结构等。
即插即用驱动程序,也可以称为WDM(Windows Driver Model)驱动程序。WDM是一种设备驱动模型,它提供了一个统一的框架,使驱动程序可以在不同版本的WIndwos操作系统上运行。
WDM通常分为三个层次:
- 总线驱动程序:负责管理总线上的设备,也为总线上的设备提供了访问总线资源的方法。
- 功能驱动程序:负责管理具体的设备,向操作系统提供该设备的功能。
- 筛选/过滤驱动程序:监视一个设备的I/O请求以及其处理过程,增加或改变一个设备或驱动程序的行为。
在WDM中,每个硬件设备都有一个设备驱动程序栈(简称设备栈),其中包含一个总线驱动程序和一个功能驱动程序,以及零个或多个过滤驱动程序。
文件系统/存储管理
在现代操作系统中,文件系统是外部存储设备的标准接口,它为应用程序使用这些设备中的数据提供了统一的抽象,多个应用程序和系统本身可以共享使用这些设备。
在Windows中,文件系统的接口部分由I/O管理器定义和实现,但文件系统的实现部分位于专门的一类驱动程序中。但文件系统接收到I/O请求时,它会根据文件系统格式规范,将这些请求转变为更底层的对于外部存储设备的I/O请求,通过它们的设备驱动程序来完成原始的I/O请求。
因此,文件系统的驱动程序定义了外部存储设备中数据的逻辑结构,使得这些数据可直接被操作系统和应用程序使用。
这些文件系统驱动程序负责管理磁盘上的文件和目录,处理文件的创建、读取、写入和删除操作,提供了文件存储和访问的基本功能。
常见的文件系统:
- NTFS(NT File System):这是Windows的原生文件系统,其驱动程序为ntfs.sys。NTFS是专门为Windows设计的,它提供了许多高级的如元数据支持、数据压缩加密的功能,同时支持大型存储卷和大文件。
- FAT(File Allocation Table):这是从DOS时代发展起来的文件系统格式,格式规范相对简单,目前主要用于兼容老版本的操作系统,以及用于移动设备以便跨操作系统传送数据。
文件系统的底层是对存储设备的管理。大容量存储设备以“分区(Partition)”和“卷(volume)”来管理整个存储空间。
分区是指存储设备上连续的存储区域(连续的扇区),而卷是指扇区的逻辑集合。一个卷内部的扇区可能来自一个分区,也可能来自多个分区,甚至来自不同的磁盘。文件系统则是卷内部的逻辑结构。
网络
在Windows操作系统中,网络是由一系列网络驱动程序和网络协议栈组成。
Windows内核层中的网络相关组件:
- 网络驱动程序(Network Drivers):负责管理物理网络接口卡(NIC)或虚拟网络适配器的通信。
- 协议栈(Protocol Stack):是一个多层次的协议栈,用于处理网络通信。这个协议栈包括了各种网络协议,如TCP/IP、UDP、ICMP等。协议栈负责数据包的封装、路由、传输和解包,确保数据在网络中的正确传输。
- 套接字(Sockets):套接字是应用程序与网络协议栈之间的接口,允许应用程序创建网络连接、发送和接收数据。Windows内核还提供了套接字API,应用程序可以使用这些API与网络进行交互。
- 网络服务(Network Services):Windwos操作系统还提供了各种网络服务,如DHCP客户端、DNS客户端、网络发现服务等,它们能够进行获取IP地址,解析域名或发现网络设备等操作。
- 网络筛选器驱动程序(Network Filter Drivers):该驱动程序允许实施网络策略和安全性控制,如防火墙和安全软件可能会使用网络筛选器驱动程序来监视和过滤网络流量。
Windows为应用程序提供了多种网络API:
- Winsock(Windows Sockets) : 套接字API,允许应用程序使用套接字进行网络通信。可以使用Winsock来创建TCP/IP和UDP网络连接,发送和接收数据。
- HTTP API:允许应用程序创建HTTP服务器和客户端,发送HTTP请求、接收HTTP响应,并处理Web服务。
- WebSocket API:这是一种双向通信的说协议,允许实时数据传输,适用于在线游戏、即时聊天等应用。
- WebRTC API:一种用于实时音视频通信的开放标准,可以用于创建支持视频会议、实时音频通话等应用程序。
- UPnP API:一种用于自动发现和配置网络设备的协议。
- WinINet API:WinINet提供了对Internet资源的访问,包括HTTP、FTP和Gopher等协议,它允许应用程序进行Web页面下载、文件上传和下载等操作。
- Network Management API:网络管理API,允许应用程序管理网络设置、配置网络连接和查看网络状态。
这些网络API都提供了用户模式的动态链接库(DLL)。当应用程序通过这些DLL发出网络I/O请求时,它们将这些请求传递给内核中相应的驱动程序。通常,这些网络API要么通过专门的系统服务切换到内核模式,比如命名管道和邮件槽就有专门的系统服务;要么通过标准的系统服务接口,比如NtReadFile
、NtWriteFile
和NtDeviceIoControlFile
,由I/O管理器和对象管理器将网络请求转送至对应的驱动程序中。
Windows子系统
Windows子系统是Windows操作系统的组成部分,用于支持不同类型的应用程序和环境在Windows平台上运行。每个子系统专门设计用于处理特定类型的应用程序和操作环境。
Windows提供了多种子系统,同时在PE文件格式中的SubSystem域指示了可执行文件的子系统类型,即程序应在何种环境下运行;SubSystem域通常包含一个数字值,代表不同的子系统类型。
以下是一些常见的SubSystem及其对应的子系统:
-
Native (0):
Native子系统表示该PE文件是一个本地的执行文件,通常是驱动程序或操作系统内核组件。这些文件在操作系统内核模式下运行。
-
Windows GUI (2):
Windows GUI子系统表示该PE文件是一个图形用户界面(GUI)应用程序。它运行在Windows桌面环境中,通常有用户界面和窗口。
-
Windows CUI (3):
Windows CUI子系统表示该PE文件是一个字符用户界面(CUI)应用程序。它通常是命令行应用程序,没有图形界面,用户通过控制台来与之交互。
-
OS/2 CUI (5):
OS/2 CUI子系统表示该PE文件是一个OS/2字符用户界面应用程序。这种类型的应用程序通常用于运行在IBM OS/2环境中。
-
Posix CUI (7):
Posix CUI子系统表示该PE文件是一个POSIX兼容的字符用户界面应用程序。它适用于在Windows上运行UNIX/Linux应用程序。
Windows子系统中既有用户模式部分,也有内核模式部分。内核模式部分的核心是win32k.sys,虽然它的形式是一个驱动程序,但实际上它并不处理I/O请求,相反,它向代码提供大量的系统服务。从功能上讲,它包含俩部分:窗口管理和图形设备接口。其中窗口管理部分负责收集和分发消息,以及控制窗口显示和管理屏幕输出;图形设备接口部分包含各种形状绘制以及文本输出功能。
窗口管理
Windows子系统的用户界面管理有一个层次结构,通常应用程序只是在一个默认的桌面上运行。
每个子系统会话都有自己的会话空间,属于某一个会话的资源将会从该会话空间中分配。当用户登录到Windwos中时,操作系统为该用户建立一个会话;即使用户通过远程桌面或者终端服务连接到一个系统中。系统也会为该用户建立一个单独的会话。
在一个会话中,有一个交互式窗口站,可能还有非交互式窗口站。在交互式窗口站中通常有三个桌面:登录桌面、默认桌面和屏幕保护桌面。交互式窗口站有独立的剪贴板、键盘、鼠标、显示器等,在它的三个桌面中,任一时刻只有一个是激活的,输入输出设备归激活的桌面所有。
在每个桌面,都有一个顶级窗口列表,这些窗口往往可以相互重叠,有系统菜单、最大化/最小化按钮和滚动条等。通常各个图形界面应用程序的主窗口属于当前桌面的顶级窗口。在Windows中,窗口可以有子窗口,子窗口占据父窗口的客户区域。因此,桌面上的窗口形成了一个层次结构。一个窗口下总是可以创建它自己的子窗口。
Windows为常用的窗口定义了一些窗口类(window class)。窗口类规定了其对象将如何响应各种信息,包括系统发送给它的消息和用户触发的消息。
Windows子系统会话有一个RIT(Raw Input Thread)线程,负责从输入设备读取原始的输入时间,生成消息寄送到正确的线程消息队列。
图形设备接口
Windows的图形引擎有俩方面特点:
- 提供了一套与设备无关的编程接口,即GDI,这使得应用程序可以适应各种底层显示设备的差异
- 应用程序与图形设备驱动之间通信足够高效。在频繁输出和刷新图形元素的情况下,windows也能提供良好的视觉效果。
上图是Windows子系统定义的图形体系结构。win32k.sys通过DDI(显示设备驱动程序接口)与现实驱动程序打交道,而显示驱动程序通过ENG(图形引擎接口)调用Win32k.sys中图形引擎的功能。
视频端口驱动程序实际上是一个动态链接库,用于辅助视频小端口驱动程序实现一些公共的、与图形有关的功能,以及为小端口驱动程序提供一个与系统内核和执行体打交道的环境。视频小端口驱动程序则直接负责的硬件资源管理和控制。
系统线程和系统进程
系统线程则是一些特殊线程,与普通用户线程不同,系统线程不属于任何特定的用户进程,它直接运行在内核模式下。
一些常见的系统线程:
- Idle线程:它的任务是在系统没有其他任务要执行时,占用CPU周期并降低CPU功耗,它通常属于最低优先级,以确保在需要时可以立即释放CPU资源给其他任务。
- Deferred Procedure Call (DPC) 线程:DPC线程是用于处理延迟的硬件中断请求的系统线程。当硬件设备产生中断请求时,DPC线程负责处理这些请求并执行相应的处理程序。
- System线程:System线程执行一些关键的内核操作,如系统调度、中断处理、内存管理等。它是操作系统的核心部分,用于协调和管理其他系统线程和用户进程。
系统线程中还有一组系统辅助线程(system worker thred),它们代表操作系统或者其他的应用进程来完成一些特殊的工作。实际上,系统辅助线程是一个线程池,Windows在系统初始化时创建了一定数量的辅助线程,而且随着辅助线程的负载的变化,执行体也会动态地创建一些辅助线程,以满足系统负载的变化需求。
在Windows操作系统中,有一些重要的系统进程,它们负责管理和控制操作系统的不同方面。
一些常见的系统进程:
- 系统空闲进程(Idle):该进程的PID为0,其中每个处理器或核对应有一个线程。
- System 进程:这是操作系统的好恶心进程,PID为4。它负责管理内核模式下线程、设备驱动程序、中断处理和其他核心人物。
- 会话管理器(smss.exe):Windows系统中创建的第一个用户模式进程,在Windows启动过程中创建环境变量,(启动了子系统进程csrss.exe和登录进程winlogon.exe)。另外,它还负责创建新的终端服务器会话,包括建立会话空间的数据结构,为新建的终端服务器加载子系统。
- 登录进程(winlogon.exe):负责处理交互用户的登录和注销。
- Windows子系统进程(csrss.exe):负责为用户提供一个子系统环境。
- 本地安全权威子系统进程(lsass.exe):负责本地系统安全策略。
- Shell进程(explorer.exe):Windows的默认Shell,它提供了系统与用户打交道的各种界面,包括开始菜单、任务栏、资源管理窗口等几乎所有Windows用户都熟悉的界面。
- 服务控制管理器(services.exe):负责Windows的系统服务,指一些特殊的进程。
内核基本概念
Windows内核中的各个组件并非单纯的独立模块,相反地,组件之间不可避免地包含了复杂的依赖关系,甚至存在交叉引用。下面是一些Windows内核中的基本概念。
处理器模式
在Intel x86处理器上,段描述符有一个2位长度的特权级:0表示最高特权级,3表示最低特权级。也就是CPU的权限级别0环和3环,分别对应内核模式和用户模式。
处理器有许多指令只有在零环内才可以使用,例如I/O指令、操纵内部寄存器指令等,当处理器处于用户模式时,它处于一种相对隔离的状态:能够执行的指令有限,能够访问的内存也是有限的(用户代码和内核代码有各自的运行环境)。一旦越过这些限制,就会引发处理器异常,此时操作系统会捕获这些异常,并决定处理器是否继续执行。
用户模式下,处理器只能访问用户地址空间,而在内核模式下,处理器不仅可以访问用户地址空间,也可以访问系统地址空间。在内核模式下的代码和数据都是共享的,所有的进程一旦其指令流进入到内核模式下,则系统地址空间的代码和护具都是相同的。
一个指令流(即线程)在执行时,在以下情况会发生模式切换:
- 用户模式代码触发了异常,则控制流进入到内核模式,内核中的异常处理函数可以决定该控制流是否继续执行。
- 用户模式代码执行时,被一个中断打断,控制流进入特权模式,等中断处理例程完成后,它若调用
iret/iretd
指令,则控制流恢复到用户模式下。 - 执行特殊的模式切换指令,如Intel x86的
sysenter
指令,从用户模式切换到内核模式。若想从内核模式切换到用户模式1,通常使用sysexit
、iret/iretd
这样的指令。
由于系统空间是所有进程共享的,所以,任何一个进程在执行内核模式的代码时,实际上是在使用操作系统的服务。在Windows体系结构中,内核模式向上优一个执行体API,对于应用程序而言,这便是系统服务。
Windows将这些系统服务组织成了一张表,称为SDT(Service Descriptor Table,服务描述符表)。
内存管理
任何一个进程都定义了它自己完整的4GB地址空间(虚拟内存空间),在内存空间分布一图中,将其划分成2GB内核空间以及2GB应用空间,换句话说,内核空间是所有进程共享的,也称作系统地址空间,剩下的2GB空间才是它自己私有的,也叫进程地址空间。
为了有效管理2GB的系统地址空间,Windows将2GB划分成了一些固定的区域,主要包括:内核模块映像、PFN数据库、换页内存池、非换页内存池、会话空间、系统缓存区、系统视图以及页表等。
分页机制:
Windows使用分页机制管理虚拟内存和物理内存。它将虚拟内存和物理内存划分成固定大小的页面,通过映射这些页面来实现虚拟地址到物理地址的转换。
通常操作系统将内存划分为大小固定的页面,通常为 4KB、8KB 或其他大小。这些页面是虚拟内存和物理内存的基本单位;之后操作系统将进程的虚拟地址空间也划分成页面大小的块。当程序访问进程的虚拟地址时,操作系统将虚拟地址转化成相应的物理地址。
为了实现虚拟地址到物理地址的转换,需要使用到页面表。
- 页面表 是操作系统中的一个数据结构,用于记录虚拟地址空间中每个页面与实际物理内存中的对应关系。页面表的条目存储了虚拟页号到物理页号的映射关系。
- 页面表中的每个条目称为页表项(Page Table Entry,PTE)。每个 PTE 存储了虚拟页号到物理页号的映射,以及一些额外的控制信息,例如页面是否在物理内存中、是否被修改等。
当程序访问进程的虚拟地址时,MMU负责将这个虚拟地址通过页面表转化成物理地址。
如果虚拟页已经在物理内存中,则直接获取物理地址。如果虚拟页不在物理内存中,就需要进行页面调度。
页面调度:
-
如果虚拟页不在物理内存中,会先引发一个 缺页异常。这时,操作系统需要根据页表中的信息确定要将哪一页加载到物理内存中。
-
然后操作系统会将当前没用的物理页写入磁盘中,将需要的虚拟页加载入物理页。
从内存中获取数据的过程:
- 程序访问进程的虚拟地址
- MMU在通过页面表查询虚拟地址对应的虚拟页是否在物理内存中
- 若在,直接获取物理地址,返回数据;
- 若不在,引发缺页异常,MMU在页面表中查找对应的虚拟页,通过页面调度将虚拟页加载到物理内存中
- 获取物理地址,返回数据。
? 至于数据在物理内存中还是虚拟内存中是没有规律的,取决于数据使用的频繁程度。
内存页面管理算法
在系统地址空间中,不同的区域使用并不完全相同的内存页面管理算法,较为典型的有以下三种:
- 非换页内存池:这部分内存区域在初始化时就已经被映射到物理页面,所以Windows利用空闲链表的做法,按照不同的粒度(1、2、3、>=4个页面大小),将空闲页面链接起来。空闲页面本身即链表中的节点,因而这些链表无需额外的内存空间(除了头节点)。申请和释放页面的操作实际上是针对空闲链表来进行的。
- 换页内存池:在换页内存池区域,空闲的页面并没有被映射好物理页面,Windows使用位图来管理页面的分配。分配连续的多个页面,即从位图中找到连续的零位。
- 系统PTE区域:这部分内存区域存放的并非PTE,而只是表示这部分地址范围是以PTE的形式来管理的,即把PTE当做资源来管理。当内核需要一段虚拟地址来映射物理页面时,它可以使用系统PTE区域中的地址。
以上这些内存区域按照页面粒度来管理其分配情况,Windows执行体在这些系统内存区域管理的基础上,还提供了一组更小的粒度(8B的倍数,最小位8B)的内存管理,包括执行体换页内存池和执行体非换页内存池。这些内存池通过空闲链表记录下每个已申请页面中的空闲内存块;当释放内存时,自动与相邻的空闲块合并以构成更大的空闲内存块。内核其他组件或驱动程序通过执行体暴露的API函数(例如ExAllocatePoolWithTag
和ExFreePoolWithtag
)来使用这些内存池。
进程地址空间是随进程一起被创建的,每个进程都有它自己的页目录页面,其中有一半的**页目录项(PDE)**是共享的,即系统地址空间部分,余下一半初始化为零。随着进程中的映像文件(包括.exe文件和各DLL文件)被加载进来,以及各个模块的初始化代码被执行,进程地址空间将被建立起来。
进程地址空间按照其虚拟地址是否被分配或保留来进行管理,用户模式代码通过Windows API函数 VirtualAlloc
和 VirtualFree
来申请或释放地址范围,而内核中的虚拟内存管理器则通过一颗平衡二叉树来管理进程地址空间被使用的情况。数中的每个节点为VAD(虚拟地址描述符,Virtual Address Descriptor),描述了一段连续的地址方位。
在VAD树中,有一种重要的节点类型为内存区对象(section object),它是Windows平台上俩个或多个进程之间共享内存的一种方式。
除了对系统地址空间和进程地址空间的管理,内存管理器另一个重要的任务是管理有限的物理内存。在WIndows的系统地址空间中,专门保留了一个称为**PFN数据库(Page Frame Number Database,页帧编号数据库)**的区域。
当系统中的进程需要使用大量内存时,内存管理其如何将有限的物理页面分配给那些需要使用内存的进程?利用Windows工作集管理器。工作集(working set)是指一个进程当前正在使用的物理页面的集合。Windows系统中除了进程工作集,还有系统工作集(即系统空间中动态映射的页面集合)和会话工作集(即会话空间中的代码和数据区)。
工作集管理器运行在一个称为平衡管理器(balance set manager)的线程中,它的作用除了触发工作集管理器,还定期触发进程/栈交换器(process/stack swapper)。进程/栈交换是另一个单独的线程,一旦被唤醒,就会将满足特定条件的进程和栈换入内存或换出内存。
进程和线程管理
**进程(process)**定义了一个执行环境,包括它自己的私有空间、一个句柄表、以及一个安全环境;线程(thread)是一个控制流,有自己的调用栈(call stack),记录它的执行历史。
一个进程包含一个或多个线程。
如图,用户模式下的进程只能访问进程地址空间,若在内核模式下,就可以访问真个地址空间。
在Windows内核结构中,进程和线程的核心机制是在微内核中实现的,而管理机制是在执行体中实现的。例如,微内核负责线程调度,而线程进程的创建和初始化由执行体完成。
windows实现了抢占式线程调度,每个线程都有一个基本优先级和动态优先级。本质上每个线程都处于俩种状态之一:满足继续执行的条件,正在排队或已经执行;不满足继续执行的条件,处于等待状态,或者它的调用栈升职所处的进程已经被换出内存。在前一种情况下,线程按照优先级排队执行;对于多处理器系统,排队过程要更为复杂,不仅要处理多个队列,还要考虑每个处理器的就绪线程队列的平衡程度。还需要考虑处理器亲和性。
关于作业和迁程,**作业(job)**是一个执行体支持的内核对象,它使得一个或多个进程被当做一个整体来加以管理和控制。管理程序通过Windows API 可以控制一个作业小号系统资源(CPU或内存)的各种限制,例如用户模式CPU时间的限制、进程的处理器亲和性、工作集的最大值和最小值、虚拟内存的使用限制等。**迁程(fiber)**是一种用户线程,它对于内核是不可见的,由kernel32.dll实现。应用程序可以在一个线程环境中创建多个迁程,然后手动控制它们的执行。迁程不会被自动执行,应用程序必须显式地选择某个迁程来执行,而且一旦迁程运行起来,要么一直运行到它退出,要么运行到它显式地切换至另一个迁程。
中断和异常
**中断(interrupt)**是指处理器外部事件(如硬件设备)触发的信号,它会中断当前的处理器活动。**异常(Exceptions)**指程序执行过程中出现的非正常或意外情况,由处理器内部事件(如执行了错误指令)触发。
尽管中断和异常的触发来源和方式不同,但Intel x86处理器内部使用同一套陷阱机制来处理中断和异常,它利用IDT(Interrupt Descriptor Table,中断描述符表),将每个中断或异常与一个处理该中断或异常的服务例程联系起来,因而一旦发生异常或中断,该相应的服务例程将被执行。Windows在此基础上,添加了一种更灵活的机制,允许设备驱动程序为特定的中断向量添加它的中断服务例程(ISR,Interrupt Service Routine)。一个中断向量允许连接多个中断对象(interrupt object),这里中断对象是一种封装了中断服务例程的内核对象。当中断发生时,这些中断对象中的服务例程都有机会处理该中断。通过中断对象机制,设备驱动程序可以在不操纵IDT的情况下加入它们的中断服务例程;另一方面,多个硬件设备也可以共享同样的硬件中断向量。
执行体层中介绍过中断请求级别IRQL。
异常是程序指令流执行过程中的同步处理过程,既可以由处理器硬件产生,也可以由指令流软件产生。Windows为所有需要处理的异常都提供了异常处理器(exception handler,即异常处理例程)。
同步
在现代操作系统中,由于多处理器、多核或者中断各种并发性(concurrency)因素的存在,同样的代码可能被并发执行,而数据也可能被并发访问。在这种情况下,对于可能被并发访问的数据进行必要的同步(synchronization)保护是一种常见的编程实践。
一些同步中会遇到的概念:
- 关于锁。什么是锁?锁是一种同步机制,用于确保在同一时刻只有一个线程能够访问共享资源;也就是保护临界区,确保在任何时刻只有一个线程可以进入临界区执行操作。锁包括互斥锁、自旋锁、读写锁等不同类型。
- 临界区。什么临界区?临界区是用于保护共享资源免受多个线程同时访问的一种同步机制:当一个线程进入临界区时,其他线程必须等待,直到第一个线程离开临界区。常用于用户模式下。
Windows根据执行环境中的IRQL大于APC_LEVEL 或者等于PASSIVE_LEVEL,将同步机制分为“不依赖线程调度的同步机制”和“基于线程调度的同步机制”。
不依赖线程调度的同步机制主要是在IRQL的高优先级下执行的,通常用于中断处理程序和内核模式代码中,以确保在处理中断或执行关键内核代码时,不会被其他线程打断。常见的不依赖线程调度的同步机制包括:
- 自旋锁(Spin Lock):自旋锁用于在多个线程之间互斥的访问共享资源,本质上是一种忙等待(busy-wait),意思是线程会一直自旋(忙等待)直到锁可用为止,而不会被挂起等待。常使用在高IRQL下,因为此时是不允许切换线程的,使用自旋锁可以确保关键代码不会被其他线程打断。一些自旋锁扩展:执行体自旋锁(支持共享和独占的语义)、排队自旋锁(queued spin lock)和栈内排队自旋锁(in-stack queued spin lock)。
- 中断服务例程(ISR):ISR用于响应硬件中断,当硬件设备触发中断时,操作系统会立即执行ISR来处理中断。ISR运行在IRQL的高优先级下,不允许进行线程调度,以确保快速响应中断。
- 延迟过程调用(Deferred Procedure Call,DPC):DPC用于延迟执行一些代码块或处理程序,常与中断处理有关。当硬件设备触发中断时,操作系统会将**中断服务例程(ISR)**用于快速响应中断,但有时需要执行一些耗时的操作,例如数据传输或资源释放,这时就会使用DPC来延迟执行这些操作。
另一种基于线程调度的同步机制:当一个线程的执行条件不满足时,该线程进入等待状态,系统将控制权交由其他满足执行条件但没有得到处理器资源的线程;以后,当该线程的执行条件满足时,它又有机会继续执行。这里的执行条件正是Windows提供的线程同步机制中的语义。Windows定义了统一的机制来支持各种线程同步原语:分发器对象(dispatcher object),其数据结构头部为DISAPATCH_HEADER。
常见的分发器对象:
- 事件(event):用于线程之间的通信和同步,它有俩种状态:已触发和未触发。事件可以用于线程等待某个事件发生,或者通知其他线程事件的发生。
- 互斥量(Mutex):互斥量用于确保在同一时刻只有一个线程能够访问共享资源。它允许线程请求锁定,当一个线程获得锁定时,其他线程必须等待,直到锁被释放。
- 信号量(Semaphore):用于控制对共享资源的并发访问。它维护一个计数器,允许指定数量的线程同时访问资源。
- 条件变量(Condition Variable):条件变量用于线程之间传递信号和等待特定条件的发生。通常与互斥量一起使用,用于等待某个条件满足后执行操作。
- 读写锁(Read-Write Locks):读写锁用于控制对共享资源的读和写,它允许多个线程同时读取资源,但只允许一个线程写入资源。
- 计数器对象(Counting Semaphore):一种特殊的信号量,它可以增加和减少计数器的值,通常用于跟踪资源的可用性或完成的任务数量。
**互斥锁(Mutex)**是一种基于线程调度的同步机制,它用于确保在同一时刻只有一个线程能够访问共享资源。当一个线程获得互斥锁的所有权后,其他线程必须等待,直到该线程释放锁。
关于互斥锁和自旋锁:互斥锁基于线程调度,而自旋锁不依赖线程调度;还有一个区别是线程尝试获取锁时,如果该锁被占用,线程是被挂起还是一直自旋;互斥锁适用于长时间的临界区和资源竞争激烈的情况,而自旋锁适用于非常短暂的临界区。
Windows在上述基础上,实现了同步语义更为丰富的一些同步机制,包括:快速互斥体(fast mutex)、守护互斥体(guarded mutex)、执行体资源(executive resource)和推锁(push lock)。
内核重要数据结构
windows内核中一些常见的数据结构。
内核对象
内核对象是Windows内核中一种重要的数据结构管理机制。应用层的进程、线程、文件、驱动模块、事件、信号量等对象或者打开的句柄在内核中都有与之对应的内核对象。
如图,一个Windows内核对象可以分为对象头和对象体俩部分。在对象头中至少有一个OBJECT_HEADER和对象额外信息。对象体紧接着对象头中的OBJECT_HEADER。一个对象指针总是指向对象体而不是对象头。如果要访问对象头,需要将对象体指针减去一个特定的偏移值,以获取OBJECT_HEADER结构,通过OBJECT_HEADER结构定位从而访问其他对象结构辅助。对象体内部一般会有1个type和1个size成员,用来表示对象的类型和大小。
Windows内核对象可以分为如下3种类型:
-
Dispatcher对象
这种对象在对象体开始位置了一个公共数据结构DISPATCHER_HEADER,其结构代码如下:
包含了
DISPATCHER_HEADER
结构的内核对象都以字母“K”开头,表明这是一个内核对象,例如KPROCESS、KTHREAD,但以字母“K”开头的内核对象不一定是Dispatcher对象。包含该结构的内核对象都是可以等待的(waitable),也就是说,这些内核对象可以作为参数传给内核的KewaitForSingleObject()
和KeWaitForMultipleObjects()
函数,以及应用层的WaitForSingleObject()
和WaitForMultipleObject()
函数。 -
I/O对象
I/O对象在对象体开始位置没有DISPATCHER_HEADER结构,但通常会放置一个与type和size有关的整型成员,以表示该内核对象的类型(例如文件内核对象的类型为26)和大小。常见的I/O对象包括DEVICE_OBJECT、DRIVER_OBJECT、FILE_OBJECT等。
-
其他对象
除了Dispatcher对象和I/O对象,剩下的都属于其他内核对象。其中有俩个常用的内核对象,分别是进程对象(EPROCESS)和线程对象(ETHREAD)。
EPROCESS用于在内核中管理进程的各种信息,如进程ID、进程状态、内存管理信息等。所有进程的EPROCESS内核结构都被放入一个双向链表,R3在枚举系统进程的时候,通过遍历这个链表获得了进程的列表。因此有的Rookit会试图将自己进程的EPROCESS结构从这个链表摘掉,从而达到隐藏自己的目的。
EPROCESS结构中的一些关键数据如下:
调用下面俩个内核函数可以获得进程的EPROCESS结果。
PsLookupProcessByProcessId()
函数,根据进程PID拿到进程的EPROCESS结构;PsGetGurrentProcess()
函数,直接获取当前进程的EPROCESS结构。ETHREAD结构是线程的内核管理对象。它代表Windows中的一个线程,包含了线程的执行上下文、调度信息。
EPROCESS、KPROCESS、ETHREAD、KTHREAD结构之间的关系如下图。
可以看出,EPROCESS和ETHREAD结构都是通过双向循环链表组织管理的。一个EPROCESS结构中包含一个KPROCESS结构,而在一个KPROCESS结构中又有一个指向ETHREAD结构的指针。在ETHREAD结构中,又包含了KTHREAD结构成员。
SSDT
**系统服务描述表(System Services Descriptor Table,SSDT)**其在内核中的实际名称是 “KeServiceDescriptorTable”。
它用于处理应用层通过kernel32.dll下发的各个API操作请求。ntdll.dll中的API是一个简单的包装函数,当kernel32.dll中的API通过ntdll.dll时,会先完成对参数的检查,再调用一个中断(int 2Eh
或者SysEnter
指令),从而实现从R3层进入R0层,并将要调用的服务号(也就是SSDT数组中的索引号index值)存放到寄存器EAX中,最后根据存在EAX中的索引值在SSDT数组中调用指定的服务(Nt*系列函数)。如下图:
SSDT表的结构定义:
#pragma pack(1)
typedef struct ServiceDescriptorEntry
{
unsigned int *ServiceTableBase; //表的基地址
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices; //表中服务的个数
unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t,
*PServiceDescriptorTableEntry_t;
#pragma pack()
其中较为重要的俩个成员为ServiceTableBase
(SSDT表的基地址)和NumberOfServices
(表示系统中SSDT服务函数的个数)。SSDT表实际上是一个连续存放该函数指针的数组。
SSDT表的导入方法:
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
由此可以知道SSDT表的基地址和SSDT函数的索引号(index),从而求出对应的服务函数地址。在x86平台上,它们之间满足如下规则:
FuncAddr = KeServiceDescriptorTable + 4 * index
与x86平台上直接在SSDT中存放SSDT函数地址不同,在x64平台上,SSDT中存放的是索引号锁对象的SSDT函数地址和SSDT表基地址的偏移量左移4位的值,因而计算公式变为:
FuncAddr = ([KeServiceDescriptorTable + index * 4] >> 4 + KeServiceDescriptorTable)
通过这个公式,只要知道SSDT表的基地址和对应函数的索引号,就可以将对应位置的服务函数替换成自己的函数,从而完成SSDT Hook过程了。
PEB
操作系统会为每个进程设置一个数据结构,用来记录进程的相关信息。该结构就是PEB(Process Environment Block,进程环境块),PEB存在于用户地址空间中,记录了进程的相关信息。
在NT中,PEB位于进程空间的FS:[0x30]
处。同时,TEB中的 ProcessEnvironmentBlock
就是PEB结构的地址,其结构的0x30偏移处是一个指向PEB的指针。
因此,访问PEB有俩种方法:
-
直接获取:
mov eax, dword ptr fs:[30h] ; fs:[30]里存放即是PEB地址
-
通过TEB获取:
mov eax, dword ptr fs:[18h] ;此时eax里为TEB的指针 mov eax, dword ptr [eax+30h] ;此时eax里为PEB的指针
PEB结构(部分):
其中,BeingDebugged
成员用于指定该进程是否处于被调试状态,该值为0时进程未处于调试状态,若该值为非零值,则进程处于调试状态。(可以使用Windows API,如IsDebuggerPresent
、CheckRemoteDebuggerPresent
函数来访问该成员)
Ldr字段也是一个很重要的成员,该字段指向的结构记录了进程加载进内存的所有模块的基地址,通过Ldr指向的三个链表就可以找到kernel32.dll
的基地址。
TEB
**TEB(Thread Environment Block,线程环境块)**同样位于应用层之中。它包含了系统频繁使用的一些与线程相关的数据,进程中的每个线程都有一个自己的TEB。一个进程的所有TEB都存放在从0x7FFDE000开始的线性内存中,每4KB为一个完整的TEB。
在NT中,FS:[0]
的地址指向了TEB结构,这个结构的开头是一个NT_TIB结构,具体(部分)如下:
NT_TIB结构的0x18偏移处是一个Self指针,指向这个结构自身,也就是TEB结构的开头。TEB结构的0x30偏移处是一个指向PEB的指针。
在TEB结构的0xE10偏移处有个字段TlsSlots[]
,是一个无类型的指针数组(TLS存储槽),它的大小是40h字节。也就是说,一个线程同时存在的动态TLS不能超过64项。
可以通过NtCurrentTeb函数调用和FS段寄存器俩种方法来访问TEB结构:
-
NtCurrentTeb函数调用
-
FS段寄存器访问
mov eax, dword ptr fs:[18h]
注册表
Windows注册表也可以被视为一种特殊的数据结构,它是一个层次结构数据库,用于存储和组织系统配置信息、应用程序设置和其他系统相关的数据。
Windows提供了一些API供应用程序访问注册表,例如RegOpenKeyEx
、RegCreateKeyEx
等。这些API运行开发人员在应用程序中读取、写入、编辑和删除注册表中的键值和数据。
在Win10中通过Win+R输入regedit即可查看注册表。
如上图,Windows注册表是一个树状(层次)结构:
- 每个节点是一个键(key)或值(value)。
- 键是一个容器,好比文件系统中的目录,它可以包含其他的键(子键)和值。注册表中的每个键都有一个唯一的名称,用于标识它。
- 值存储的是数据,好比是文件系统中的文件。数据可以是字符串、整数、二进制数据等不同的类型。
- 注册表的根也是一个键,称为根键。一般为5个根键,常见的根键包括HKEY_LOCAL_MACHINE(存储计算机范围的配置信息)、HKEY_CURRENT_USER(存储当前用户的配置信息)等。
在内核之中,执行体包含了一个称为“配置管理器”的组件,它是注册表的真正实现。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!