Windows架构分为系统空间和内核空间。
x86平台:0x00000000 -> 0xFFEFFFF
x64平台:0x0000000000000000 -> 0xFFFFFFFFFFFEFFFF
(1. 为什么不是0x00000000 -> 0xFFEFFFF?系统存在64KB的保护页,即0x10000大小。防止通过指针运算侵入内核导致系统崩溃或漏洞,防止内核探测或错误操作侵入内核空间)
值得注意的是,这些地址也可以通过符号MmHighestUserAddress (0x7FFEFFFF) 和MmSystemRangeStart (0x80000000)来引用
从上图我们可以看到,驱动和内核位于HAL的顶部,运行于内核模式,共享同一个内核地址空间,无隔离。驱动和内核都依赖HAL服务,如操作寄存器,配置中断等。驱动也是唯一一个允许第三方(如硬件厂商)向操作系统添加新功能的方式。
通常,OS API(kernel32.dll和ntdll.dll)用作用户模式下OS 的访问接口,然后会被转化为一个系统调用,随后由内核处理
我们举一个例子,例如程序使用Kernel32.dll的ReadFile函数:
1. kernel32.dll将解析参数
2. 调用ntdll.dll中的NtReadFile函数。ntdll.dll将系统调用号,例如NtReadFile是0x00存入eax,然后执行YSENTER执行(64位中是SYSCALL)。
; ntdll!NtReadFile 的伪代码 mov eax, 0x00 ; 设置系统调用编号 (NtReadFile=0x00) mov edx, esp ; 参数指针 sysenter ; 触发模式切换 (x86)
3. 触发YSENTER或SYSCALL指令,特权从Ring3 切换到Ring 0,跳转到预设的内核入口点(KiFastCallEntry或KiSystemCall64)
KiFastCallEntry: ; 内核入口函数 swapgs ; 切换内核GS段 (x64) save_user_context ; 保存用户态寄存器 load_kernel_stack ; 载入内核栈
4. 入口函数KiFastCallEntry根据EAX中的调用号,跳转到Ntoskrnl.exe的NtReadFile函数(或ZwReadFile),NtReadFile执行检查
// 内核中的 NtReadFile 实现 (ntoskrnl.exe) NTSTATUS NtReadFile(...) { // 1. 验证参数和权限 // 2. 查找文件对象 // 3. 构建I/O请求包(IRP) IofCallDriver(device_object, irp); // 转发给驱动 }
5. NtReadFile调用驱动层执行,协作文件系统驱动(例如NTFS.sys)来解析文件路径,计算扇区位置。协作磁盘驱动(disk.sys)发送ATA/SCSI命令到硬盘控制器。
6. HAL执行硬件操作
7. 驱动完成IO操作后触发中断、内核将数据复制到用户缓冲区,通过SYSEXIT指令恢复用户模式,ntdll.dll接收状态码,并返回给kernell32.dll
当驱动被加载时,它可以访问所有物理内存,以及内核空间的虚拟内存和用户空间进程的用户空间内存。当然,它也可以更改内核结构以隐藏自身或系统中其他恶意软件。
Windows XP/Vista x64引入了KPP(内核补丁保护),以防止驱动修改内核结构。
Patch Guard是一种机制,来保护内核结构免遭篡改。它会定期检查每个内核结构是否发生更改,如果更改就蓝屏。因此,必须绕过Patch Guard。
1. Patch Guard是周期性,攻击者修改后,然后再下次检查前撤销修改,也不会出现蓝屏。
2. Turla的Urobrous使用KeBugCheckEx中的钩子,在检查前恢复。随后又挂钩RtlCaptureContext
3. Kento Oki 在 2021 年发表的最新文章中描述了另一种绕过 Patch Guard 的方法。此外,CyberArk Labs几年前就发现了一种绕过 Patch Guard 的方法。
Windows又引入了驱动程序DSE(驱动程序签名验证)
1. 使用带有漏洞的签名驱动