Windows内核基础
0x01 内核基础理论
1.1 权限级别
系统内核层,又叫0环(Ring 0);应用层叫3环。实际上是CPU4个运行级别中的一个,运行级别由内到外R0~R3,运行权限依次降低。R0由最高执行权限。
AMD64CPU诞生之后,CPU与操作系统保持一致,只保留了R0,R3两个级别。
HAL是一个可加载的核心模块HAL.DLL,它为WindowsXP上的硬件提供低级接口。WindowsXP的执行体是NTOSKRNL.EXE的上层,内核是其下层。用户层可调用的函数接口在NTDLL.DLL中。
1.2内存布局
x86支持32位寻址,最多支持2^32=4GB的虚拟内存空间。在4GB的虚拟内存地址空间中,Windows内存主要分为内核空间和应用层空间,每部分占2GB,其中还包括一个64KB的NULL空间及非法区域。CPU在进行地址翻译的时候,先通过分段机制计算出一个线性地址(段选择符和偏移量计算的虚拟地址),随后通过分页基址找到对应的物理地址。
x64(AMD64)的内存空间会存在一些空洞(hole),x64内存理论上支持2^64B大小的寻址空间。一般用不到这么大,Windows支持44位(16TB),Linux支持48位(256TB)。
1.3Windows内核启动过程
1.3.1启动自检阶段
打开电源,计算器开启自检过程,从BIOS载入必要的指令,进行一系列的自建操作,进行硬件的初始化检查,同时在屏幕上显示信息。
1.3.2初始化启动阶段
自检完成后,根据CMOS的设置,BIOS加载启动盘,将主引导记录(MBR)中的引导代码载入内存。接着,启动过程由MBR来执行。启动代码搜索MBR的分区表,找到活动分区,将第一个扇区中的引导代码载入内存,引导代码检测当前的文件系统,找到ntldr文件,随后启动它。BIOS将控制权转交给ntldr,由ntldr完成操作系统的启动过程(Windows7使用的是bottmgr)
1.3.3Boot加载阶段
先从启动分区加载ntldr,然后对ntldr进行如下设置
- 设置内存模式,32位系统和32位处理器,则设置“32-bit flat memory mode”;64位则设置64位模式
- 启动一个简单的文件系统,以定位boot.ini,ntoskrnl,Hal等启动文件
- 读取boot.ini文件
1.3.4检测和配置硬件阶段
在这个阶段会检查和配置一些硬件设备
1.3.5内核加载阶段
ntldr将首先加载windows内核ntoskrnl.exe和硬件抽象层(HAL)。HAL会对硬件底层的特性进行隔离,位操作系统提供统一的调用接口。接下来ntldr从注册表的HKEY_LOCAL_MACHINE\System\CurrentControlSet键下读取这台机器安装的驱动程序,然后依次加载驱动程序。初始化底层设备驱动,在注册表HKEY_LOCAL_MACHINE\System\CurrentControl\Services键下查找“Strat”键的值为0或1的设备驱动。
“Start”键的值可以为0,1,2,3,4,数值越小,启动越早。SERVICE_BOOT_START(0)表示内核刚刚初始化,此时加载的都是系统核心有关的重要驱动程序,如磁盘驱动;SERVICE_SYSTEM_START(1)稍晚一些;SERVICE_AUTO_STRAT(2)是从登陆界面出现的时候开始,如果登录速度较快,可能驱动还没加载就登陆了;SERVICE_DEMAND_START(3)表示在需要的时候手动加载;SERVICE_DISABLE(4)表示禁止加载。
1.3.6Windows的会话管理启动
驱动程序加载完成,内核会启动会话管理器。这是一个名为smss.exe的程序,是Windows系统中第一个创建的用户模式进程,其作用如下。
- 创建系统环境变量
- 加载win32k.sys,它是Windows子系统的内核模式部分
- 启动csrss.exe,它是Windows子系统的用户模式部分
- 启动winlogon.exe
- 创建虚拟内存页面文件
- 执行上次系统重启前未完成的重命名工作(PendingFileRename)
1.3.7登陆阶段
Windows子系统启动的winlogon.exe系统服务提供对Windows用户的登录和注销的支持,可以完成如下工作。
- 启动服务子系统(services.exe),也称服务控制管理器(SCM)
- 启动本地安全授权(LSA)过程(lsass.exe)
- 显示登陆界面
登陆组件将用户的账号和密码安全的传送给LSA进行认证处理。
1.3.8Windows7和WindowsXP启动过程的区别
- BIOS通过自检后,将MBR载入内存并执行,引导代码找到启动管理器Bootmgr
- Bootmgr寻找活动分区boot文件夹中的启动配置数据BCD文件,读取并组成相应文件的启动菜单,然后再屏幕上显示多操作系统选择画面
- 选择Windows7系统后,Bootmgr读取系统文件windows\system32\winload.exe并将控制权交给winload.exe
- winload.exe加载windows7的内核,硬件,服务,然后加在桌面等信息,从而启动整个windows7系统。
1.3.9新一代系统引导方式UEFI与GPT
以上介绍的依靠BIOS和MBR完成系统的引导和启动。其有局限性,如磁盘逻辑块地址(logical block address,LBA)是32位的,最多表示2^32个山区,每个扇区一般是512字节,所以最多支持2TB寻址。而且MBR最多支持4个主分区或者3个主分区和1个扩展分区,扩展分区下可以有多个逻辑分区。在BIOS中,启动操作系统必须从硬盘上的指定扇区中读取系统启动代码(包含在MBR中),然后从活动分区引导并启动操作系统。对扇区的操作远比不上对分区中文件的操作那样直观和简单。
UEFI(Unified Extensible Firmware Interface,统一的可扩展固件接口)的出现主要用于替换BIOS。在UEFI中,用于表示LBA的地址是64位的。
UEFI本身已经相当于一个微型操作系统。UEFI具备文件系统的支持能力,能够直接读取FAT分区中的文件。开发人员可以开发出直接在UEFI下运行的应用程序,这类程序文件通常以“efi”结尾。在UEFI下只需要将安装文件复制到一个FAT32(主)分区或U盘中,通过这个分区或U盘即可安装和启动Windows。
与传统的MBR分区相比,GPT(GUID Partition Table,全局唯一标识分区表)对分区数量没有限制,但Windows在实现GPT的时候,将分区的个数限制在128个GPT分区以内,GPT可管理磁盘大小达到了18EB。
1.4WindowsR3与R0的通信
Windows分为应用层和内核层。应用程序调用的API函数实际被封装在某个DLL文件中,DLL动态库中的函数更底层的函数包含在ntdll.dll中,也就是最终会调用ntdll.dll中的Native API函数。ntdll.dll中的Native API函数是成对出现的,分别以“Nt”和“Zw”开头。在ntdll.dll中,它们本质是一样的,知识名字不同。
当kernel32.dll中的API通过ntdll.dll执行时,会完成参数的检查工作,在调用一个中断(int 2Eh或者SysEnter指令),从R3进入R0曾。在内核ntoskrnl.exe中有一个SSDT(系统服务描述符表),里面存放了与ntdll.dll中对应的SSDT系统服务处理函数,即内核态Nt*系列函数,它们与ntdll.dll中的函数一一对应。
1.4.1从用户模式调用Nt*和Zw*API,连接ntdll.lib
二者没有区别,都是通过设置系统服务表中的索引和在栈中设置参数,经由SYSENTER(或syscall)指令进入内核态(而不是像Windows2000中通过int 0x2e指令终端),并最终由KiSystemService跳转到KiServiceTable对应的系统服务例程中。由于是从用户模式进入内核模式的,代码会严格检查用户空间传入的参数。
1.4.2从内核模式调用Nt和ZwAPI,连接ntoskrnl.lib
Nt系列API将直接通过调用对应的函数代码,而Zw系列API则通过KiSystemService最终跳转到对应的函数代码处。重要的是两种调用对内核Previos Mode的改变:如果从用户模式调用Native API,则Previous Mode为用户态;如果从内核模式调用Natvie API,则Previous Mode是内核态。当Previous为用户态的时候,Native API会对传递的参数进行严格的检查。
在调用用户模式NtAPI时,不会改变Previous Mode的状态;再调用ZwAPI的时候,会将Previous Mode改为内核态,因此使用后者会提升效率避免额外的参数检查。当通过int 2EH(windowsXP以前)或者SYSENTER(WindowsXP以后版本;在AMD中为syscall)的KiFastCallEntry例程时,将要调用的函数所对应的服务号(在SSDT数组中的索引值)存放到EAX寄存器中,再根据存放在EAX的索引值在SSDT数组中调用指定服务(Nt系列函数)。
在这个过程中,应用层的命令和数据会被系统的I/O管理器封装在一个叫做IRP的结构中。之后,IRP会将R3发下来的数据和命令逐层发送给下层的驱动创建的设备对象进行处理。完成对应的功能。
CreateFile函数过程
内核主要由各种驱动(在磁盘上时.sys文件)组成,这些驱动有的是Windows系统自带的(例如ntfs.sys,tcpip.sys,win32k.sys),有的是由第三方软件厂商提供的。驱动加载之后,会生成对应的设备对象,并可以选择像R3提供一个可供访问和打开的符号链接,常见的盘符C,D,E其实都是文件系统驱动创建的设备对象的符号链接,对应的符号链接名分别是“\??\C:\”,"\??\D:\"等。
应用层程序可以根据内核驱动的符号链接名调用CreateFile函数打开。在获得一个句柄后,程序就可以调用应用层函数与内核驱动进行通信了,例如ReadFile,WriteFile,DeviceIoControl。
内核驱动一旦执行了DriverEntry入口函数,就可以接受R3层的通讯请求。在内核驱动中专门有一组分发派遣函数用来分别响应应用层的调用请求。每一个应用层负责I/O的API都对应于内核的一个分发派遣函数,如CreateFile对应于DispathCreate。API被调用之后,传递给API的数据和命令会通过IRP直接传递给对应的驱动分发派遣函数来处理。当驱动的分发派遣函数处理完整个IRP请求之后,驱动可以结束(或允许,或阻止)这个IRP,或者把这个IRP发给下层驱动继续处理。
1.5 内核函数
内核层的函数都以固定的前缀开始,分别属于内核中不同的管理模块。
- Ex:管理层。Executive
- Ke:核心层。Kernel
- HAL:硬件抽象层。Hardware Abstract Layer
- Ob:对象管理。Object
- MM:内存管理。Memory Manage
- Ps:进程(线程)。Process
- Io:I/O管理
- Se:安全管理。Security
- Fs:文件系统。File System
- Cc:文件缓存管理。Cache
- Cm:系统配置管理。Configuration Manager
- Pp:即插即用管理。PnP
- Rtl:运行时程序库。Runtime Library
- Zw/Nt:对应SSDT中的服务函数,例如文件或者注册表相关的操作函数
- Flt:Minifilter文件过滤驱动中调用到函数
- Ndis:Ndis网络框架中调用的函数
与应用层函数不同的是,在调用内核函数的时候要注意它的IRQL(Interrupt Request Level 中断请求级别)要求。内核在不同情况下会运行在不同的IRQL级别上,因此在不同的IRQL级别上,必须嗲用符合该IRQL级别要求的内核函数。
#define PASSIVE_LEVEL 0
#define LOW_LEVEL 0
#define APC_LEVEL 1
#define DISPATCH_LEVEL 2
#define PROFILE_LEVEL 27
#define CLOCK1_LEVEL 28
#define CLOCK2_LEVEL 28
#define IPI_LEVEL 29
#define POWER_LEVEL 30
#define HIGH_LEVEL 31
- PASSIVE_LEVEL:IRQL的最低级别,没有被屏蔽的中断。在这个级别上,线程执行用户模式,可以访问分页内存。线程运行在该终端级别上,对所有终端都做出相应。用户模式代码都是运行在该中断级别上的。
- APC_LEVEL:在这个级别上,只有APC级别的中断被屏蔽,可以访问分页内存。当只有APC发生的时候,将处理器提升至APC级别,就能屏蔽其他APC了。为了与APC同步,驱动程序可以手动提升到这个级别。分页调度管理就运行在该级别上。
- DISPATCH_LEVEL:在这个级别上,DPC(延迟过程)和更低的中断被屏蔽,不能访问分页内存,所有被访问的内存都不能分页。因为只能处理不可分页的内存,所以这个级别能访问的API大大减少。线程调度和DPC例程都运行在该级别上。为了执行多任务,系统必须允许线程调度,而线程调度是由时钟中断保证的,因此该级别的中断就是调度终端。代码运行的IRQL级别提升到DISPATCH_LEVEL时,就意味着代码不再受线程中断的影响。代码会一直运行到将IRQL设置为低于DISPATCH_LEVEL时为止。期间如果发生缺页错误之类的IRQL级别在DISPATCH_LEVEL之下的严重中断,这些中断均不会被处理,这时代码将无法正常运行。
- DIRQL(Device IRQL):出于高层的驱动通常不会使用该级别,在该级别上所有的中断都会被忽略。这是IRQL的最高级别,通常用它来判断设备的优先级。
1.6内核驱动模块
内核驱动在硬盘上是一个扩展名为.sys的文件,遵循PE格式规范。
#include <ntddk.h>
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
DbgPrint("Goodbye world!\n");
}
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
DbgPrint("Hello, world\n");
pDriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
编译好的驱动加载与执行过程:
- 创建服务(注册表)。在注册表的Services键下建立一个与驱动名称相关的服务键。这个服务键规定了驱动的一些属性,例如启动GROUP与StartType决定了驱动加载的先后,StartType为0的比StartType为1的先启动。
- 对象管理生成驱动对象(DriverObject)并传递给DriverEntry函数,执行DriverEntry函数。
- 创建控制设备对象
- 创建控制设备符号链接
- 如果是过滤驱动,创建过滤设备对象并绑定
- 注册特定的分发派遣函数
- 其他初始化动作,如Hook,过滤,回调框架等的注册和初始化。
0x02内核重要数据结构
2.1内核对象
应用层的进程,线程,文件,驱动模块,时间,信号量等对象或者打开的句柄在内核中都有与之对应的内核对象。
一个windows内核对象可以分为对象头和对象体两部分。在对象头中至少有一个OBJECT_HEADER和对象额外信息,对象体紧跟着对象头,一个对象指针总是指向对象体而不是对象头。如果要访问对象头,需要减少特定的偏移值。对象体内部一般会有一个type和一个size成员,用来表示对象的类型和大小。
Windows内核对象可分为如下三种类型
- Dipatcher对象
- 这种对象在对象体开始位置放置了一个共享的公共数据结构DISPATCHER_HEADER,其结构代码如下。包含DIPATCHER_HEADER的内核对象名字都以K开头,表明这是一个内核对象,例如KPROCESS,KTHEARD,KEVENT,KSEMAPHORE,但是以K开头的内核对象不一定是Dispatcher对象。包含DISPATCHER_HEADER结构的内核对象都是可以等待的(waitabal),也就是说,这些内核对象可以作为参数传递给KeWaitForSingleObject和KeWaitForMultipleObjects函数,以及应用层的WaitForSingleObject和WaitForMultiple Objects函数。
- I/O对象
- I/O对象在对象体的开始并未放置DISPATCHER_HEADER结构,但通常会放置一个与type和size有关的证书成员,以标识该内核对象的类型(例如文件内核对象的类型为26)和大小。常见的I/O对象包括DEVICE_OBJECT,DRIVER_OBJECT,FILE_OBJECT,IRP,VPB,KPROFILE等。
- 其他对象
- 除了上述两个对象之外都是其他内核对象。其中有两个常用的内核对象分别是进程对象EPROCESS和线程对象ETHEARD。
- EPROCESS用于在内核中管理进程的信息,每一个进程都对应一个EPROCESS结构,用于记录进程执行期间的各种数据。尽管EPROCESS结构非常大,但它是一个不透明的结构(opaque strucure),具体成员并未导出,并随着操作系统版本的变化而变化。因此想要查看其结构成员,需要查阅资料或者windbg加载内核符号后执行。
- 所有进程的EPROCESS内核结构都被放入了一个双向链表,R3在枚举系统进程的时候,便利整个列表。因此rootkit可以通过将自己的EPROCESS从该链表中拆卸以达到隐藏目的。
- PsLookupProcessByProcessId可以通过进程pid获取EPROCESS,PsGetCurrentProcess函数可以直接获取当前进程的EPROCESS。
- ETHREAD结构的内核管理对象。每个线程都有一个对应的ETHREAD结构。ETHREAD结构也是一个不透明结构,具体成员未导出。在ETHREAD结构中第一个成员就是KTHREAD成员,所有的ETHREAD也会被放在一个双向链表中进行管理。
- EHTREAD部分成员有KTHREAD Tcb 线程内核对象;CLINET_ID Cid 进程PID。
2.2SSDT
SSDT(System Services Descriptior Table 系统服务描述表),在内核中的实际名称是KeServiceDescriptorTbale。这个表通过内核ntoskrnl.exe导出(x64里不导出)。
SSDT用于处理应用层通过kernel32.dll下发的API操作请求。ntdll.dll中的API是一个简单的包装函数,当kernel32.dll中的API通过ntdll.dll的时候会先完成对参数的检查,再调用一个中断,从R3层进入R0层,并将要调用的服务号(也就是SSDT数组中的索引号index值)存放到寄存器EAX中,最后通过EAX的值在SSDT数组中调用指定的服务(Nt系列函数)
SSDT表的定义如下
其中最重要的成员为ServiceTableBase(SSDT表的基地址)和NumberOfServices(标识系统中SSDT服务函数的个数)。SSDT表实际上就是一个开逆序存放这个函数指针的数组。
SSDT表的导入方法如下。
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
由此可以知道SSDT的及地址(数组的首地址)和SSDT函数的索引号(index),从而求出对应服务函数的地址。在x86平台下:FuncAddr=KeServiceDescriptortable+4*index。
x64平台上SSDT存放到索引号是对应的SSDT函数地址和SSDT表基地址x16(左移四位)的值。所以FuncAddr=([KeServiceDescriptortable+4*index]>>4+KeServiceDescriptortable)
shadow SSDT的原理与SSDT类似,它对应的表名是KeServiceDescriptorTableShadow,是内核为导出的另一张表,包含Ntoskrnl.exe和win32k.sys服务函数,主要处理来自User32.dll和GDI32.dll的系统调用。与SSDT不同,Shadow SSDT是未导出的,因此不能再自己的模块中导入和直接引用。
挂钩该表中的NtGidBitBlt,NtGdiStretchBlt可以实现截屏保护;挂钩NtUserSetWindowsHookEx函数可以防止或保护键盘钩子;挂钩与按键相关的函数NtUserSendInput可以防止模拟按键...
shadow SSDT的挂钩原理和SSDT的挂钩原理差不多,只不过由于未导出,需要使用不同的方法来获取该表的地址和服务函数的索引号。例如,硬编码与KeServiceDescriptorTbale在不同系统中的位置偏移,搜索KeAddSystemServiceTable,KTHREAD.ServiceTable,以及有效内存搜索等。
KeServiceDescriptorTableShadow实际上也是一个SSDT结构数组。Win XP中其表位于KeServiceDescriptorTable表上0x40偏移处。
KeServiceDescriptorTableShadow包含4个子结构,示例如下。
2.3TEB
TEB和PEB一样,不在系统内核空间,是在应用层中的结构。
TEB(Thread environment block 线程环境快)结构包括系统频繁使用的与线程相关的数据,进程中的每一个线程(系统线程除外)都有一个自己的TEB。一个进程所有的TEB都存放在从0x7FFDE000开始的线性内存空间中,每4kb为一个完整的TEB。
2.3.1TEB结构体
R3级的应用程序中,fs:[0]的地址指向TEB结构,这个结构开头是一个NT_TIB结构
32位
64位
32位TEB
64位TEB
2.3.2TEB访问
可以通过NtCurrentTeb函数调用和FS段寄存器这两种方法访问TEB结构。
- NtCurrentTeb函数调用
- FS段寄存器访问
- FS为段寄存器,当代码运行在R3级时,基地址即为当前的线程的TEB。fs:[18h]
2.4PEB
PEB存在于用户地址空间中,记录了进程的相关信息。
2.4.1PEB访问
TEB中的ProcessEnvironmentBlock就是PEB结构的地址,其结构的0x30处有一个指向PEB的指针。PEB的0x2偏移处时一个UChar成员,名叫“BeingDebugged”,进程被调试时值为1,为被调试时值为0。
获取PEB:
- 直接获取fs:[30h]
- 通过TEB获取fs:[18h],随后找其偏移0x30处。
2.4.2PEB结构
PEB结构随着Windows系统版本的变化而略有差异。通过查阅MSDN或Winternl.h,可以知道PEB的结构定义如下。
MSDN中定义的PEB是不完整的,微软隐藏了很多细节,需要自己去逆向与挖掘。
32位
64位
BeingDebugged成员用于指定该进程是否处在被调试状态,CheckRemoteDebuggerPresent函数用于判断进程是否出于被调试状态。ProcessParameters是一个RTL_USER_PROCESS_PARAMETERS,即用于记录进程的参数信息(如命令行参数)。
EPROCESS和ETHREAD结构位于内核空间,题目分别有一个指针指向应用层空间的PEB结构和TEB结构,而在TEB中也有一个指针指向PEB结构。
0x03 内核调试基础
这里详细看实验吧。
文章评论