记录一下加密与解密对于Windows内核的学习
0x01 内核基础理论
Windows与内核启动过程
启动自检查
从BIOS载入指令,进行一系列自检操作,进行硬件的初始化检查。
初始化自动阶段
根据CMOS设置,BIOS载入启动盘。
将主引导记录(MBR)中的引导代码载入内存。
MBR会搜索MBR分区表中的活动分区,载入第1个扇区的引导代码。
该代码会检测当前操作系统,将控制权转交给操作系统。
Boot加载阶段
设置内存模式,启动操作系统。
检测和配置硬件
内核加载
操作系统首先加载Windows内核
Ntoskrnl.exe
和硬件抽象层(HAL)HAL会对硬件底层进行隔离,为OS提供统一的调用接口。
接着操作系统从注册表读取该机器安装的驱动并依次加载。
Windows会话管理启动
驱动加载完成后,内核会启动会话管理器
smss.exe
。这是Windows第一个启动的用户态进程,作用为:
- 创建系统环境变量
- 加载
win32k.sys
,支持Windows子系统内核模式。 - 加载
csrss.exe
,支持Windows子系统用户模式。 - 启动
winlogon.exe
- 创建虚拟内存页面文件
- 执行重启前的一些任务
登录阶段
winlogon.exe
系统服务提供对Windows用户的登录和注销的支持。- 启动服务子系统
services.exe
,也称服务控制管理器(SCM)。 - 启动本地安全授权(LSA)过程
lsass.exe
,该组件负责将用户的账号密码进行验证,通过认证就允许用户对系统进行访问。 - 显示登录界面。
- 启动服务子系统
WinXP启动通过ntldr文件,Win7启动时开始用启动管理器Bootmgr
Win7中通过Windload.exe
加载系统内核、硬件、服务等。
上述过程都是借助BIOS和MBR完成系统的引导,新一代的系统引导方式有UEFI和GPT。
UEFI用于替换BIOS,突破了BIOS+MBR技术方案中分区容量2TB的限制(32寻址位数x512扇区大小=2TB)
其相当于一个微型的操作系统,具备文件系统能够读取文件,甚至开发一些在上面运行的应用。
这使得新系统只需要通过U盘启动即可安装。
GPT替换的是MBR的分区表,对分区数量没有限制,Windows限制在128个分区,范围也比MBR大很多了。
不过只有基于UEFI平台的主板才支持GPT分区引导启动。
Ring3和Ring0通信
Windows分应用层(R3)和内核层(R0),它们之间如何进行通信呢?
比如一个应用调用I/O相关的API时(如Writefile),这个API在kernel32.dll
或user32.dll
中
而更底层的函数包含在ntdll.dll
文件中。
在ntdll.dll
中,NativeAPI是成对出现的,分别以Zw
和Nt
开头,他们实质上是一样的,只是名字不同。
当kernel32.dll
中的API通过ntdll.exe
执行时,会完成参数的检查工作,再调用一个中断(int 2Eh
或SysEnter
),从R3层进入到R0层。
在内核ntoskrnl.exe
中,有一个SSDT,其中存放了对应系统服务处理函数,即内核态的Nt*系列函数,它们与ntdll中的函数一一对应。
(1)从用户态调用Nt和Zw系列API
从用户态调用Nt和Zw系列API,连接ntdll.lib。
两者无区别,都是通过栈中设置参数,由sysenter或syscall指令进入内核态,最终由KiSystemServeice
跳转到KiServiceTable
对应的系统服务例程中。
由于是用户模式进入内核模式,代码会严格检查用户空间传入的参数,否则会发生严重错误。
(2)从内核态调用Nt和Zw系列API
从内核态调用Nt和Zw系列API,连接ntoskrnl.lib。
Nt系列API直接调用对应的函数代码
Zw系列API则通过KiSystemService
最终跳转到对应的函数代码处。
需要理解调用对内核中Previous Mode
的改变
- 如果从用户模式调用Native API,则
Previous Mode
是用户态,NativeApi将对传递的参数进行严格的检查。 - 用过从内核模式调用Native API,则
Previous Mode
是内核态,不会对参数进行检查。
所以调用Nt和Zw系列API在内核模式中就有差别
- 调用用户模式NT*API时,不会改变
Previous Mode
的状态 - 调用Zw*API时,
Previous Mode
会被改为内核态。
因此在驱动开发时,使用Zw*系列的API能够避免额外的参数列表检查,从而提高效率。
也就是通过中断或SYSENTER的KiFastCallEntry()
例程时,将要调用的函数所对应的服务号(对应SSDT数组中的索引)存放到寄存器EAX中,从而调用指定的服务(NT系列函数)。、
在该过程中,R3层的命令和数据会被系统的I/O管理器封装存入到一个叫做IRP的结构中,之后IRP会将R3中的数据和命令逐层发送给驱动创建的设备对象进行处理,进而完成相应功能。
内核主要由各种驱动(.sys文件)组成,有Windows系统自带的,有的是由第三方软件厂商提供的。
驱动加载之后,会生成相应的设备对象,并可以选择向R3提供一个可供访问和打开的符号链接。常见的C盘等其实都是文件系统驱动创建的设备对象的符号链接。
应用层程序可以根据内核驱动的符号链接名调用CreateFile()
函数打开,获得句柄之后,程序就可以调用应用层函数与内核驱动进行通信了。
内核驱动一旦执行了DriverEntry()
入口函数,就可以接受R3层的通信请求了。在内核驱动中专门有一组分发派遣函数用来响应应用层的调用请求。
API被调用时,数据和命令通过IRP传递给回调函数来处理。
内核函数
不同前缀可以代表内核中不同的管理模块。这些前缀主要如下:
- Ex: 管理层Executive
- Ke: 核心层Kernel
- Hal: 硬件抽象层Hardware Abstraction Layer
- Ob: 对象管理Object
- Mm: 内存管理Memory Manager
- Ps: 进程线程管理Process
- Se: 安全管理Security
- Io: I/O管理
- Fs: 文件系统FileSystem
- Cc: 文件缓存管理Cache
- Cm: 系统配置管理Configuration Manager
- Pp: 即插即用管理PnP
- Rtl: 运行时程序库Runtime Library
- Zw/Nt: 对应于SSDT中的服务函数
- Flt: Minifilter文件过滤驱动中调用的函数
- Ndis: Ndis网络框架中调用的函数
调用内核函数的时候需要注意IRQL(中断请求级别Interrupt Request Level)
在不同的IRQL级别需要调用符合级别的内核函数
内核驱动模块
编译好的驱动如何被系统加载并执行呢?
创建一个服务(在注册表里),注册表的Services键下创建一个与驱动名称相关的服务键
位置位于
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services
服务键内会规定驱动的一些属性
对象管理器生成驱动对象(DriverObject)并传递给
DriverEntry()
函数。执行DriverEntry()函数(驱动执行的第一个主函数)
创建控制设备对象
创建控制设备符号链接(R3可见)
如果是过滤驱动,则创建过滤设备对象并绑定。
注册特定的分发派遣函数
其他初始化动作例如Hook、过滤、回调框架等的注册和初始化
0x02 内核重要数据结构
内核对象
一个内核对象可分为对象头和对象体两部分。
访问对象头需要从对象体减去特定偏移
内核对象可分为如下三种类型
Dispatcher对象
这种对象在对象体开始位置放置了一个共享的公共数据结构
DISPATCHER_HEADER
。包含此结构的内核对象名字都以字母‘K’开头,表明这是一个内核对象。但‘K’开头的内核对象不一定是Dispatcher对象。包含此结构的对象都是可以等待的,即可作为内核的KeWaitForSingleObject()
和KeWaitForMultipleObjects()
函数以及应用层的WaitForSingleObject()
和WaitForMultipleObjects()
函数的参数。I/O对象
该类对象体在开始的位置通常会防止一个与type和size相关的整型成员,以表示内核对象的类型。
常见包括
DEVICE_OBJECT
/DRIVER_OBJECT
/FILE_OBJECT
/IRP
/VPB
/KPROFILE
等等其他对象
除了上述两类都是其他内核对象,其中常见的有
进程对象(EPROCESS)
和线程对象(ETHREAD)
。EPROCESS
该结构内涵大量进程相关信息,但其数据结构是不透明的,所有进程的内核结构都被放入一个双向链表,R3在枚举系统进程的时候,通过遍历这个链表可以获得所有的进程列表。因此有的Rootkit会试图将自己进程的内核结构从链表中摘掉从而隐藏自身。
ETHREAD
该结构是线程的内核管理对象
EPROCESS内包含KPROCESS结构 即内核对象
KPROCESS内包含一个指向ETHREAD结构的指针
ETHREAD结构内含KTHREAD结构
SSDT
系统服务描述符表 System Services Description Table
SSDT用于处理应用层通过Kernel32下发的API请求
经过ntdll时,会进行参数检查,最后通过将调用号(即SSDT中服务对应的调用号)存在EAX寄存器中,使用中断操作int 2Eh
或SysEnter
,实现从R3层到R0层的转变。
Shadow SSDT时内核未导出的一张表
主要处理来自User32和GDI32的系统调用,与SSDT不同,该表未导出,无法在自己的模块中导入和直接引用。
知道SSDT表的位置和服务号就能够HOOK对应服务函数 实现SSDT HOOK
截屏保护/防止键盘HOOK/防止模拟按键/防止搜索窗口等功能都可以从这里实现。
Shadow SSDT同理,由于表未导出,要搜索偏移一般都需要内存搜索。
TEB
TEB结构为线程环境快
不在系统内核空间中,而是应用层中的结构。
进程中每个线程都有自己的TEB,一个进程中的所有TEB都存放在0x7FFDE000
开始的线性内存中。
每4kb为一个完整的TEB
随Windows版本不同,该结构也有所差异。
TEB访问有两种方式
NtCurrentTeb函数调用
Ntdll中导出了该函数,可以返回当前线程TEB结构体的地址。
FS寄存器访问
当代码运行在R3级时,基地址即为当前线程的县城环境快,所以该段也称为TEB段。运行如下代码可获得TEB的指针。
MOV EAX,DWORD PTR FS:[30H]
PEB
进程环境块,存在于用户地址空间中。
TEB结构中包含其对应PEB结构的指针
访问方式
通过FS寄存器直接访问
MOV EAX,DWORD PTR FS:[30H]
通过TEB偏移访问
MOV EAX,DWORD PTR FS:[18H] MOV EAX,DWORD PTR [EAX+30H]
此外内核EPROCESS结构中也记录了PEB结构的地址
PEB结构中
BeingDebugged
可以用于判断进程是否处于被调试的状态
ProcessParameters
记录进程接受的参数信息