Windows架构入门






2.50/5 (2投票s)
一篇关于原生 API 的深度解析文章。
引言
本文旨在帮助读者更深入地理解 Windows 系统架构。文章仅涉及少数几个主题,但对其中一些主题的深入理解,可能有助于更深刻地理解 rootkits 及其检测方法。
关于 Win32 子系统的一点说明
Win32 子系统是 Windows 所有用户界面的入口。需要注意的是,Win32 子系统中的组件并不负责整个 Win32 API,只负责其中的 USER 和 GDI 部分。原生 API 才是与 Windows NT 系统交互的真正接口。在 Windows NT 中,Win32 API 仅仅是原生 API 之上的一层。由于 NT 内核与 GUI 无关,原生 API 不包含任何图形相关的服务。从功能上看,原生 API 是与 Windows 内核最直接的接口,提供了与 Windows 内存管理器、I/O 系统、对象管理器、进程及其执行的线程等进行直接交互的接口。Win32 子系统是负责 Windows 用户界面的所有方面的组件。这从底层的图形设备接口 (GDI) 开始,到 USER 组件结束,后者负责更高级别的 GUI 构造,如窗口、菜单以及处理输入。应用程序不应该直接调用原生 API。这就是为什么微软不打算发布关于原生 API 大量文档的原因:应用程序被期望只使用与系统交互的 Win32 API。技术上来说,原生 API 使用 Win32 API 来代表它进行通信,并且是一组从 NTDLL.dll(针对用户模式调用者)和 NTSOKRNL.exe(针对内核模式调用者)导出的函数。例如,一个正常执行的线程有两个线程堆栈:一个用于用户模式,另一个用于内核模式。因此,如果调用一个用户模式函数,例如 CreateFile
,它必须转换到内核模式,在获得 ring 0 内核模式权限后,它会转换为 NtCreateFile
。也就是说,原生 API 中的 API 前缀总是有一个或两个:Nt
或 Zw
。在 NTDLL.dll 中的用户模式实现中,这两组 API 是相同的,并且指向相同的代码。在内核模式下,它们有所不同:Nt
版本是 API 的实际实现,而 Zw
版本是经过系统调用机制的存根。在早期版本的 NT 中,堆 API 由 NTDLL.dll 管理,它取代了 Kernel32.dll。在某些堆 API 例程中,kernel32.dll 会“存根”到 NTDLL.dll。这被称为“快照”。当一个模块的导出“快照”到另一个模块的导出时,就被称为“快照”到该模块。
系统调用机制
几乎不可避免地会遇到调用操作系统 API 的代码。当用户模式代码需要调用内核模式函数时,就会发生系统调用。这通常发生在应用程序调用操作系统 API 时。API 的用户模式部分通常执行基本的参数检查、验证检查,然后调用内核来实际执行请求的操作。这可以定义操作系统的强度,其稳定性取决于系统软件与应用程序软件的交互和通信程度。请注意,如果用户模式代码可以直接调用内核,那么安全性将不复存在。从用户模式直接调用内核模式函数可能导致严重的漏洞,因为应用程序可以调用内核内的无效地址并导致系统崩溃,甚至调用允许它们控制系统的地址。这就是为什么操作系统使用一种在用户模式和内核模式之间切换的机制。总的来说,用户模式代码调用一个特殊的 CPU 指令,该指令告诉处理器切换到特权模式(用于内核模式执行)并调用一个特殊的服务分派例程。然后,该分派例程调用用户模式请求的特定系统函数。
在 Windows NT 操作系统中,所有起源于用户模式并需要由系统内核处理的系统调用都必须通过内核本身的大门,在那里它们将被分派和执行。这个大门被称为中断 INT 2Eh
。虽然 ntdll.dll 在库的用户端处理系统调用,但在通过大门后,ntsokrnl.exe 会接管并调用内部函数 KiSystemService()
,并将原始系统调用传递给它。KiSystemService
使用查找表来获取有关应该使用哪个原生 API 调用来分配原始调用的信息。这个系统服务表称为 SST。以一个典型的 Windows 2000 系统调用为例;在 WinXP 中,ntdll.dll 中的 NtReadFile
/ZwReadFile
反汇编为
Ntdll!ZwReadFile:
77f8c552 mov eax, 0xa1
77f8c557 lea edx, [esp+4]
77f8c55b int 2e
77f8c55d ret 0x24
EAX
寄存器加载了服务号,EDX
寄存器指向内核模式函数接收的第一个参数。这就是为什么我们给堆栈指针增加 32 位。当调用 int 2e
指令时,处理器使用中断描述符表 (IDT) 来确定调用哪个处理程序。IDT(以前称为中断向量表)是一个处理器拥有的表,它告诉处理器在发生中断或异常时调用哪个例程。处理程序号通常是中断号的 4 倍的十六进制倍数。中断 2e 的 IDT 条目指向一个名为 KiSystemService
的内部 NTOSKRNL
函数,它是内核服务分派器。KiSystemService
验证服务号和堆栈指针是否有效,然后调用请求的特定内核函数。现在,我们来考虑 NtWriteFile
/ZwWriteFile
的反汇编
MOV EAX, 112h
MOV EDX, 7FFE0300h
CALL DWORD PRT DS:[EDX]
RETN 24
请注意,值 7FFE0300h 首先被移到 EDX
。因此,7FFE0300h 是指向以下函数的指针
MOV EDX, ESP
SYSENTER
请注意,系统现在使用特殊的 SYSENTER
指令来执行到内核模式的切换,而不是调用中断。
异常处理
异常是程序中的一种特殊情况,它会立即跳转到一个称为异常处理程序的特殊函数。异常处理程序随后决定如何处理异常,它可以纠正问题,或者使程序从相同的代码位置继续执行,或者从另一个位置恢复执行。因此,中断和异常是操作系统条件,它们将处理器重定向到脱离正常控制流程的代码。硬件或软件都可以检测到它们。术语“陷阱”是指当发生异常或中断并且控制转移到操作系统的一个固定位置时,处理器捕获正在执行线程的一种机制。在 Windows 中,处理器将控制转移到一个陷阱处理程序,该处理程序是一个特定于某个中断或异常的函数。异常有两种基本类型:硬件异常和软件异常。硬件异常是由处理器生成的异常:例如,当程序访问无效内存页(页面错误)时,或者发生除以零时。
内核区分中断和异常的方法如下。中断是异步事件(可能随时发生),与处理器正在执行的操作无关。中断主要由 I/O 设备、处理器时钟或计时器生成,并且可以通过更改位值来启用或禁用。异常是由特定指令的执行产生的同步条件。因此,硬件或软件都可以生成异常和中断。例如,总线错误异常是由硬件问题引起的。如果您将系统带给技术人员,看看他是否能检查与 PCI 总线相关的硬件问题,然后执行检查 PCI 总线合规性的测试。对于 IRQ/DMA,让技术人员检查软件 IRQ。这种功能可以报告当前加载到内存中的中断(从 0 到 FFh)。您可以使用此信息来找出通过系统 BIOS 连接了哪些中断以及每个中断指向的地址。这些数据对于解决 BIOS 和软件驱动程序之间的冲突非常有用。同时,尝试将系统线程映射到设备驱动程序。使用网络路径访问您的 C: 驱动器,对您的整个 C: 驱动器执行目录列表。例如,如果您的计算机名称是 STATION1,键入“dir \\system1\c$ /s”。现在,启动 Process Explorer(由 Sysinternals 的 Mark Russinovich 编写的强大进程实用程序,来自 Technet)。确保您已选择“CPU 上下文切换增量”,使其成为您的列之一。在“dir”命令运行时,双击“System”选项卡上的线程。查找 srv.sys!WorkerThread 并单击查看其属性。如果您看到一个系统线程正在运行,并且您不确定驱动程序是什么,请在高亮显示显示中的 Srv.sys 中的线程时,按“Module”按钮。
参考文献
- 《Windows 内部原理》,作者:Mark Russinovich 和 David Solomon。