近俩天的学习-Windows内核基础的学习笔记。算是一个概述,毕竟每一个小点单拉出来都可以讲好久。学了这俩天,对内核的理解算是清晰了那么一点点了,持续学习ing...欢迎师傅一起交流讨论私信andwx都欢迎。感谢。 参考文献: 《加密与解密》Windows内核基础 《Windows内核原理与实现》系统总述 《深入理解Windows操作系统》 Chat Gpt.... MSDN文档。 一些零零散散的大佬的blog。感谢。
系统内核层 ,又称零环 (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内核中主要可以分成三层:硬件抽象层 (HAL),内核层 (也称微内核micro-kernel),执行体层 。
内核层实现操作系统的基本机制,而所有的策略决定则留给执行体。执行体中的对象绝大多数封装了一个或者多个内核对象,并且通过某种方式(比如对象句柄)暴露给应用程序。
Windows内核为用户模式提供了一组系统服务 ,供应用程序使用内核中的功能。应用程序通常并不直接调用这些系统服务,而是通过一组系统DLL,最终通过ntdll.dll 切换到内核模式下的执行体API函数中,以调用内核中的系统服务。
Ntdll.dll是链接用户模式代码和内核模式系统服务的桥梁。对于内核提供的每一个系统服务,该DLL都提供一个相应的 存根函数 ,这些存根函数的名称以“Nt ”作为前缀,例如NtCreateProcess
、NtOpenFile
等。另外ntdll.dll还提供了许多系统级的支持函数,比如映像加载器函数(以“Ldr ”为前缀)、系统时间函数(以“Etw ”为前缀),以及一般的运行支持函数(以“Rtl ”为前缀)和字符串支持函数等。
执行体API函数接收的参数来自于各种应用程序,因此为了保证系统的安全以及抵抗来自用户模式的恶意攻击,所有的执行体API都必须保证参数的有效性 。通常执行体系统服务函数会在其开始处,对所接收的参数逐一探查它们的可访问性。例如如下代码:
解析:
Windows内核中的关键组件 :
硬件抽象层(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种类型的函数:
关于函数前缀,这些函数都以固定的前缀开始,分别属于内核中不同的管理模块:
与应用层函数不同,在windows操作系统中调用内核函数必须要关注中断请求级别(IRQL,Interrupt Request Level) 。
IRQL 是一个表示中断优先级 的数字,用于确保处理器在处理不同的任务时维持正确的操作顺序和安全性。操作系统内核使用不同的 IRQL 来管理对硬件资源的访问,以及处理不同级别的中断和异常。
在调用内核函数时,必须要确保当前的IRQL与被调用函数所要求的级别相符 。不遵守这一规则可能导致系统崩溃或数据损坏。IRQL的级别如下:
开发内核模式驱动程序时,合理地管理IRQL至关重要。如果一个函数要求在低IRQL下运行,而当前IRQL较高,就不能直接调用那个函数;反之亦然。不正确的IRQL处理可能导致系统不稳定或蓝屏(BSOD)。
执行体中除了函数组成,还包含了如图中的多个重要的组件 ,以下:
在内核中除了内核模块ntoskrnl.exe和HAL以外,其他模块几乎都以设备驱动程序 的形式存在。
Windows中的设备驱动程序,并不一定对应物理设备;它既可以创建虚拟设备,也可以与设备无关,它仅仅是内核的扩展模块 。从软件结构 角度而言,可以认为设备驱动程序是Windows内核的一种扩展机制,系统通过设备驱动程序来支持新的物理设备或者扩展功能。
设备驱动程序是可以加载到系统中的模块,其文件扩展名为**.sys**,其格式是标准的 PE文件格式 。驱动程序中的代码运行在内核下,尽管它们可以直接操纵硬件,但理想的情况是,调用HAL 中的函数与硬件打交道,因此,驱动程序往往用C/C++语言来编写,从而可以方便地在Windows所支持的体系结构之间进行源代码层次上的移植。
根据设备驱动程序的功能和行为 可以将设备驱动程序分为三类:
即插即用驱动程序,也可以称为WDM(Windows Driver Model)驱动程序 。WDM是一种设备驱动模型,它提供了一个统一的框架 ,使驱动程序可以在不同版本的WIndwos操作系统上运行。
WDM通常分为三个层次:
在WDM中,每个硬件设备都有一个设备驱动程序栈(简称设备栈 ),其中包含一个总线驱动程序和一个功能驱动程序,以及零个或多个过滤驱动程序。
在现代操作系统中 ,文件系统是外部存储设备的标准接口,它为应用程序使用这些设备中的数据提供了统一的抽象,多个应用程序和系统本身可以共享使用这些设备。
在Windows中 ,文件系统的接口部分由I/O管理器定义和实现,但文件系统的实现部分位于专门的一类驱动程序中。但文件系统接收到I/O请求时,它会根据文件系统格式规范,将这些请求转变为更底层的对于外部存储设备的I/O请求,通过它们的设备驱动程序来完成原始的I/O请求。
因此,文件系统的驱动程序定义了外部存储设备中数据的逻辑结构 ,使得这些数据可直接被操作系统和应用程序使用。
这些文件系统驱动程序负责管理磁盘上的文件和目录,处理文件的创建、读取、写入和删除操作,提供了文件存储和访问的基本功能。
常见的文件系统:
文件系统的底层是对存储设备 的管理。大容量存储设备以“分区(Partition) ”和“卷(volume) ”来管理整个存储空间。
分区 是指存储设备上连续的存储区域 (连续的扇区),而卷 是指扇区的逻辑集合 。一个卷内部的扇区可能来自一个分区,也可能来自多个分区,甚至来自不同的磁盘。文件系统则是卷内部的逻辑结构。
在Windows操作系统中,网络是由一系列网络驱动程序 和网络协议栈 组成。
Windows内核层中的网络相关组件:
Windows为应用程序提供了多种网络API:
这些网络API都提供了用户模式的动态链接库(DLL) 。当应用程序通过这些DLL发出网络I/O请求时,它们将这些请求传递给内核中相应的驱动程序。通常,这些网络API要么通过专门的系统服务 切换到内核模式,比如命名管道和邮件槽就有专门的系统服务;要么通过标准的系统服务接口 ,比如NtReadFile
、NtWriteFile
和NtDeviceIoControlFile
,由I/O管理器 和对象管理器 将网络请求转送至对应的驱动程序中。
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的图形引擎有俩方面特点:
上图是Windows子系统定义的图形体系结构。win32k.sys通过DDI(显示设备驱动程序接口)与现实驱动程序打交道,而显示驱动程序通过ENG(图形引擎接口)调用Win32k.sys中图形引擎的功能。
视频端口驱动程序实际上是一个动态链接库,用于辅助视频小端口驱动程序实现一些公共的、与图形有关的功能,以及为小端口驱动程序提供一个与系统内核和执行体打交道的环境。视频小端口驱动程序则直接负责的硬件资源管理和控制。
系统线程 则是一些特殊线程,与普通用户线程不同,系统线程不属于任何特定的用户进程,它直接运行在内核模式下。
一些常见的系统线程:
系统线程中还有一组系统辅助线程(system worker thred) ,它们代表操作系统或者其他的应用进程来完成一些特殊的工作。实际上,系统辅助线程是一个线程池 ,Windows在系统初始化时创建了一定数量的辅助线程,而且随着辅助线程的负载的变化,执行体也会动态地创建一些辅助线程,以满足系统负载的变化需求。
在Windows操作系统中,有一些重要的系统进程 ,它们负责管理和控制操作系统的不同方面。
一些常见的系统进程:
Windows内核中的各个组件并非单纯的独立模块,相反地,组件之间不可避免地包含了复杂的依赖关系,甚至存在交叉引用。下面是一些Windows内核中的基本概念。
在Intel x86处理器上,段描述符 有一个2位长度的特权级:0表示最高特权级,3表示最低特权级。也就是CPU的权限级别0环和3环,分别对应内核模式和用户模式。
处理器有许多指令只有在零 环内才可以使用,例如I/O指令、操纵内部寄存器指令等,当处理器处于用户模式时,它处于一种相对隔离的状态:能够执行的指令有限,能够访问的内存也是有限的 (用户代码和内核代码有各自的运行环境)。一旦越过这些限制,就会引发处理器异常 ,此时操作系统会捕获这些异常,并决定处理器是否继续执行。
用户模式下,处理器只能 访问用户地址空间,而在内核模式下,处理器不仅 可以访问用户地址空间,也 可以访问系统地址空间。在内核模式下的代码和数据都是共享 的,所有的进程一旦其指令流进入到内核模式下,则系统地址空间的代码和护具都是相同的。
一个指令流(即线程)在执行时,在以下情况会发生模式切换:
由于系统空间是所有进程共享的,所以,任何一个进程在执行内核模式的代码时,实际上是在使用操作系统的服务。在Windows体系结构中,内核模式向上优一个执行体API,对于应用程序而言,这便是系统服务。
Windows将这些系统服务组织成了一张表,称为SDT(Service Descriptor Table,服务描述符表) 。
任何一个进程都定义了它自己完整的4GB地址空间(虚拟内存空间),在内存空间分布一图中,将其划分成2GB内核空间以及2GB应用空间,换句话说,内核空间是所有进程共享的,也称作系统地址空间 ,剩下的2GB空间才是它自己私有的,也叫进程地址空间 。
为了有效管理2GB的系统地址空间,Windows将2GB划分成了一些固定的区域,主要包括:内核模块映像、PFN数据库、换页内存池、非换页内存池、会话空间、系统缓存区、系统视图以及页表等。
分页机制 :
Windows使用分页机制 管理虚拟内存和物理内存。它将虚拟内存和物理内存划分成固定大小的页面,通过映射这些页面来实现虚拟地址到物理地址的转换。
通常操作系统将内存划分为大小固定的页面 ,通常为 4KB 、8KB 或其他大小。这些页面是虚拟内存和物理内存的基本单位 ;之后操作系统将进程的虚拟地址空间也划分成页面大小的块。当程序访问进程的虚拟地址时,操作系统将虚拟地址转化成相应的物理地址。
为了实现虚拟地址到物理地址的转换,需要使用到页面表 。
当程序访问进程的虚拟地址时,MMU 负责将这个虚拟地址通过页面表转化成物理地址。
如果虚拟页已经在物理内存中,则直接获取物理地址。如果虚拟页不在物理内存中,就需要进行页面调度 。
页面调度 :
如果虚拟页不在物理内存中,会先引发一个 缺页异常 。这时,操作系统需要根据页表中的信息确定要将哪一页加载到物理内存中。
然后操作系统会将当前没用的物理页写入磁盘中,将需要的虚拟页加载入物理页。
从内存中获取数据的过程 :
至于数据在物理内存中还是虚拟内存中是没有规律的,取决于数据使用的频繁程度。
内存页面管理算法
在系统地址空间中,不同的区域使用并不完全相同的内存页面管理算法,较为典型的有以下三种:
以上这些内存区域按照页面粒度 来管理其分配情况,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的高优先级 下执行的,通常用于中断处理程序和内核模式代码中,以确保在处理中断或执行关键内核代码时,不会被其他线程打断。常见的不依赖线程调度的同步机制包括:
另一种基于线程调度的同步机制:当一个线程的执行条件 不满足时,该线程进入等待状态,系统将控制权交由其他满足执行条件 但没有得到处理器资源的线程;以后,当该线程的执行条件 满足时,它又有机会继续执行。这里的执行条件正是Windows提供的线程同步机制中的语义 。Windows定义了统一的机制来支持各种线程同步原语:分发器对象(dispatcher object) ,其数据结构头部为DISAPATCH_HEADER 。
常见的分发器对象:
**互斥锁(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结构成员。
**系统服务描述表(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表的结构定义:
其中较为重要的俩个成员为ServiceTableBase
(SSDT表的基地址)和NumberOfServices
(表示系统中SSDT服务函数的个数)。SSDT表实际上是一个连续存放该函数指针的数组 。
SSDT表的导入方法:
由此可以知道SSDT表的基地址和SSDT函数的索引号(index),从而求出对应的服务函数地址。在x86平台上,它们之间满足如下规则:
与x86平台上直接在SSDT中存放SSDT函数地址不同,在x64平台上,SSDT中存放的是索引号锁对象的SSDT函数地址和SSDT表基地址的偏移量左移4位的值,因而计算公式变为:
通过这个公式,只要知道SSDT表的基地址和对应函数的索引号,就可以将对应位置的服务函数替换成自己的函数,从而完成SSDT Hook过程了。
操作系统会为每个进程设置一个数据结构,用来记录进程的相关信息。该结构就是PEB(Process Environment Block,进程环境块) ,PEB存在于用户地址空间中,记录了进程的相关信息。
在NT中,PEB位于进程空间的FS:[0x30]
处。同时,TEB中的 ProcessEnvironmentBlock
就是PEB结构的地址,其结构的0x30偏移处是一个指向PEB的指针。
因此,访问PEB有俩种方法:
直接获取:
通过TEB获取:
PEB结构(部分):
其中,BeingDebugged
成员用于指定该进程是否处于被调试状态,该值为0时进程未处于调试状态,若该值为非零值,则进程处于调试状态。(可以使用Windows API,如IsDebuggerPresent
、CheckRemoteDebuggerPresent
函数来访问该成员)
Ldr 字段也是一个很重要的成员,该字段指向的结构记录了进程加载进内存的所有模块的基地址,通过Ldr指向的三个链表就可以找到kernel32.dll
的基地址。
**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段寄存器访问
Windows注册表也可以被视为一种特殊 的数据结构,它是一个层次结构数据库 ,用于存储和组织系统配置信息、应用程序设置和其他系统相关的数据。
Windows提供了一些API供应用程序访问注册表,例如RegOpenKeyEx
、RegCreateKeyEx
等。这些API运行开发人员在应用程序中读取、写入、编辑和删除注册表中的键值和数据。
在Win10中通过Win+R输入regedit即可查看注册表。
如上图,Windows注册表是一个树状(层次)结构:
在内核之中,执行体包含了一个称为“配置管理器 ”的组件,它是注册表的真正实现。
PreviousMode = KeGetPreviousMode();
if
(PreviousMode != kernelMode){
try
{
ProbeForWrite(InputInformation,InputInformationLength,
sizeof
(
ULONG
));
if
(ARGUMENT_PRESENT(ReturnLength)){
ProbeForWriteUlong(ReturnLength);
} except(EXCEPTION_EXECUTE_HANDLER){
return
GetExceptionCode();
}
}
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-1-5 12:22
被Sciurdae.编辑
,原因: 图挂了