Windows内核学习


记录一下加密与解密对于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.dlluser32.dll

而更底层的函数包含在ntdll.dll文件中。

ntdll.dll中,NativeAPI是成对出现的,分别以ZwNt开头,他们实质上是一样的,只是名字不同。

kernel32.dll中的API通过ntdll.exe执行时,会完成参数的检查工作,再调用一个中断(int 2EhSysEnter),从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

    image-20220311172738935

    服务键内会规定驱动的一些属性

  • 对象管理器生成驱动对象(DriverObject)并传递给DriverEntry()函数。

    执行DriverEntry()函数(驱动执行的第一个主函数)

  • 创建控制设备对象

  • 创建控制设备符号链接(R3可见)

  • 如果是过滤驱动,则创建过滤设备对象并绑定。

  • 注册特定的分发派遣函数

  • 其他初始化动作例如Hook、过滤、回调框架等的注册和初始化

0x02 内核重要数据结构

内核对象

一个内核对象可分为对象头和对象体两部分。

image-20220311175147230

访问对象头需要从对象体减去特定偏移

内核对象可分为如下三种类型

  • 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 2EhSysEnter,实现从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记录进程接受的参数信息


Author: Luluting
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Luluting !
  TOC