第三章 编写内核模式驱动程序
翻译: Kendiv
更新: Thursday, February 10, 2005
设备 I/O 控制
就像在本章开头的简介中提到的,在本书中,我们不会构建某一具体硬件的驱动程序。替代的是,我们将利用功能强大的内核驱动程序来研究 Windows 2000 的秘密。从实际结果来看,驱动程序的强大之处在于它们能在 CPU 的最高特权级别上运行。这意味着内核驱动可以访问所有的系统资源,可以读取所有的内存空间,而且也被允许执行 CPU 的特权指令,如,读取 CPU 控制寄存器的当前值等。而处于用户模式下的程序如果试图从内核空间中读取一个字节或者试图执行像 MOV EAX,CR3 这样的汇编指令都会被立即终止掉。不过,这种强大的底线是驱动程序的一个很小的错误就会让整个系统崩溃。即使是非常小的错误发生,也会让系统蓝屏,因此开发内核程序的人员必须比 Win32 应用程序或 DLL 的开发人员更加仔细的处理错误。还记得我们在第一章里使用的导致系统蓝屏的 Windows 2000 Killer device driver 吗?它所作的一切只是触及了虚拟内存地址 0x00000000 ,然后就 ---Boom !!!你应该意识到在开发内核驱动时,你会比以往更频繁的重启你的机器。
在随后章节中,我给出的驱动程序代码将采用称为设备 I/O 控制( IOCTL )的技术,以允许用户模式下的代码实现一定程序的“远程控制”。如果应用程序需要访问在用户模式下无法触及的系统资源,那内核驱动程序将可很好的完成此项工作,而 IOCTL 则是联系二者的桥梁。事实上, IOCTL 并不是 Windows 2000 采用的新技术。即使旧的操作系统 ---DOS 2.11 也具有这种能力, 0x44 函数及其子函数构成了 DOS 的 IOCTL 。基本上, IOCTL 是通过控制通路和设备通讯的一中手段,控制通路在逻辑上独立于数据通路。想象一个硬盘设备通过其主数据通路传递磁盘扇区中的内容。如果客户想获取当前设备使用的媒体信息,它就必须使用另一个不同的通路。例如, DOS 函数 0x44 ,其子函数 0x0d 、 0x66 构成了 DOS 的 IOCTL ,调用这些函数就可读取磁盘的 32 位连续数据(参考 Brown and Kyle 1991 , 1993 )。
设备 I/O 控制根据要控制的设备,可以有多种实现方式。就其一般形式来说, IOCTL 有如下几类:
l 客户端通过一个特殊的进入点来控制设备。在 DOS 中,这个进入点为 INT 21h 、函数号 0x44 。在 Windows 2000 中,则通过 Kernel32.dll 导出的 Win32 函数 DeviceIoControl() 。
l 客户端通过提供设备的唯一标识符、控制代码以及一个存放输入数据的缓冲区、一个存放输出数据的缓冲区来调用 IOCTL 的进入点。对于 Windows 2000 ,设备标识符是成功打开的设备的句柄( HANDLE )。
l 控制代码用于告诉目标设备的 IOCTL 分派器( dispatcher ),客户端请求的是哪一个控制函数。
l 输入缓冲区中可包含任意地附加数据,设备可能需要这些数据来完成客户所请求的操作。
l 客户所请求的操作产生的任何数据,都会保存在客户端提供的输出缓冲区中。
l IOCTL 操作的整体结果通过返回给客户端的状态代码来表示
很 显然这是一种强大的通用机制,这种机制可以适用于很大范围的控制请求。例如,应用程序在访问系统内核所占用的内存空间时会被禁止,这是因为当程序触及该内 存空间时会立即抛出一个异常,不过程序可以通过加载一个内核驱动程序来完成此项工作,这样就可避免出现异常。交互的两个模块都需遵循 IOCTL 协议来管理数据的传输。例如,程序可能通过给驱动程序发送控制代码 0x80002000 来读取内存或发送 0x80002001 来向内存中写入数据。对于读取请求, IOCTL 输 入缓冲区或许要提供基地址和要读取的字节数。内核驱动程序能获取这些请求并通过控制代码来判断是读取操作还是写入操作。对于读取请求,内核驱动程序会将请 求的内存范围内的数据复制到调用者提供的输出缓冲区中,如果输出缓冲区足够容纳这些数据,则返回成功代码。对于写入请求,驱动程序会将输入缓冲区中的数据 复制到指定的内存中(该内存的起始位置也由输入缓冲区指定)。在第四章,我将提供一个 Memory Spy 的示列代码。
现在,可以看出 IOCTL 是 Win32 应用程序的一种后门,通过 IOCTL ,程序可以执行几乎所有的操作,而在此之前,这些操作仅允许特权模块执行。当然,这需要首先编写一个特权级的模块,但是,一旦你拥有一个运行于系统中的 Spy 模块,一切就变得很简单了。本书的两个目标是:详细展示如何编写内核模式的驱动程序以及一个可以完成很多让人惊异的事的驱动程序的示例代码。
Windows 2000 的 Killer Device
在开始更高级的驱动程序工程之前,让我们先看看一个非常简单的驱动程序。在第一章中,我介绍了 Windows 2000 的 Killer Device----w2k_kill.sys ,它被设计为引发一个良性的系统崩溃。这个驱动程序并不需要 示例 3-3 中的大多数代码,因为它在有机会收到第一个 I/O 请求包之前就会使系统崩溃。 示例 3-7 给出了它的实现代码。这里没有给出 w2k_kill.h 文件,因为它不不包含任何我们感兴趣的代码。
示列 3-7 中的代码没有在 DriverEntry() 中执行初始化操作,因为系统会在 DriverEntry() 返回前就崩溃,所以没有必要进行这些额外的工作。
#define _W2K_KILL_SYS_
#include <ddk\ntddk.h>
#include "w2k_kill.h"
// =================================================================
// DISCARDABLE FUNCTIONS
// =================================================================
NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath);
#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverEntry)
#endif
// =================================================================
// DRIVER INITIALIZATION
// =================================================================
NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath)
{
return *((NTSTATUS *) 0);
}
// =================================================================
// END OF PROGRAM
// =================================================================
示列 3-7. 一个小巧的系统崩溃者
加载 / 卸载驱动程序
在完成一个内核驱动程序之后,你可能会想立即执行它。怎么做呢?典型的做法是,在系统启动时加载驱动程序并执行之。但这是不是就意味着我们每次更新驱动程序后,都必须重新启动系统呢?很幸运,这并不是必须的。 Windows 2000 的一个特色就是提供了一个 Win32 接口以允许在运行时加载或卸载驱动程序。这是由服务控制管理器( Service Control Manager , SCM )完成的,下面的将详细介绍它的用法。
服务控制管理器
“服务控制管理器”这个名字容易让人误解,因为它暗示该组件仅用于服务的管理。服务( Service )是 Windows 2000 的一类非常强大的模块,它们在后台运行配套的程序,并且不需要用户交互(也就是说没有常见的用户界面或者控制台)。换句话说,一个服务就是一个始终运行于系统中的 Win32 进程,即使没有用户登陆进来也如此。尽管开发服务是一个令人兴奋的话题,但它并不属于本书的范畴。想进一步了解服务的开发,请阅读 Windows Developer's Journal ( WDJ )( Tomlinson 1996a )中 Paula Tomlinson 提供的非常不错的教程,以及随后在她的 WDJ 专栏 ----Understanding NT 中发表的有关服务的论文。
SC 管理器(即服务控制管理器)可以控制服务和驱动程序。为了简单起见,我在这里使用“服务”一词来代表 SC 管理器控制的所有对象,这包括严格意义上的服务和内核驱动程。 SC 的接口对于 Win32 程序是可用的,它由 Win32 子系统组件 ----advapi32.dll 提供,这个 DLL 还提供了很多有趣的 API 函数。 表 3-3 给出了用于加载、控制和卸载服务的 API 函数的名称,同时还给出了简单的描述。在你可以加载或访问任何服务之前,你必须获取 SC 管理器的句柄(通过调用 OpenSCManager() ),在随后的讨论中,该句柄将被称为:管理器句柄。 CreateService() 和 OpenService() 都需要此句柄,而这些函数返回的句柄将被称为:服务句柄。这种类型的句柄可以传递给需要引用一个服务的函数,如 ControlService() 、 DeleteService() 和 StartService() 。这两种类型的 SC 句柄都通过 CloseServiceHandle() 函数来释放。
名 称
描 述
CloseServiceHandle
关闭来自 OpenSCManager() 、 CreateService() 或 OpenService() 的句柄
ControlService
停止、暂停、继续、查询或通知已加载的服务 / 驱动程序
CreateService
加载一个服务 / 驱动程序
DeleteService
卸载一个服务 / 驱动程序
OpenSCManager
获取 SC 管理器的句柄
OpenService
获取一个已加载的服务 / 驱动程序的句柄
QueryServiceStatus
查询一个服务 / 驱动程序的属性和当前状态
StartService
启动一个已加载的服务 / 驱动程序
表 3-3. 基本的服务控制函数
加载和运行一个服务需要执行的典型操作步骤:
1. 调用 OpenSCManager() 以获取一个管理器句柄
2. 调用 CreateService() 来向系统中添加一个服务
3. 调用 StartService() 来运行一个服务
4. 调用 CloseServiceHandle() 来释放管理器或服务句柄
要确保当一个错误发生时,要回滚到最后一个成功的调用,然后再开始。例如,你在调用 StartService() 时 SC 管理器报告了一个错误,你就需要调用 DeleteService() 。否则,服务将保持在一个非预期的状态。另一个使用 SC 管理器 API 易犯的错误是,必须为 CreateService() 函数提供可执行文件的全路径名,否则,如果该函数在当前目录中没有找到可执行文件的话,就会失败。因此,你应该使用 Win32 函数 ---GetFullPathName() 来规格化传递给 CreateService() 的所有文件名,除非可以保证它们已经是全路径的。
高层的驱动程序管理函数
为了更容易的和 SC 管理器进行交互,本书附带的 CD 提供了多个更高级的外包函数,这些函数屏蔽了原有的一些不方便的特殊要求。这些函数是本书提供的庞大的 Windows 2000 工具库(位于随书 CD 中的 \src\w2k_lib )中的一部分。 w2k_lib.dll 导出的所有函数都有一个全局的名字前缀 w2k ,服务和驱动程序管理函数都使用 w2kService 前缀。 列表 3-8 给出了本书提供的工具库中实现的加载、控制和卸载服务 / 驱动程序的函数的细节。
// =================================================================
// SERVICE/DRIVER MANAGEMENT
// =================================================================
SC_HANDLE WINAPI w2kServiceConnect (void)
{
return OpenSCManager (NULL, NULL, SC_MANAGER_ALL_ACCESS);
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceDisconnect (SC_HANDLE hManager)
{
if (hManager != NULL) CloseServiceHandle (hManager);
return NULL;
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceManager (SC_HANDLE hManager,
PSC_HANDLE phManager,
BOOL fOpen)
{
SC_HANDLE hManager1 = NULL;
if (phManager != NULL)
{
if (fOpen)
{
if (hManager == NULL)
{
*phManager = w2kServiceConnect ();
}
else
{
*phManager = hManager;
}
}
else
{
if (hManager == NULL)
{
*phManager = w2kServiceDisconnect (*phManager);
}
}
hManager1 = *phManager;
}
return hManager1;
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceOpen (SC_HANDLE hManager,
PWORD pwName)
{
SC_HANDLE hManager1;
SC_HANDLE hService = NULL;
w2kServiceManager (hManager, &hManager1, TRUE);
if ((hManager1 != NULL) && (pwName != NULL))
{
hService = OpenService (hManager1, pwName,
SERVICE_ALL_ACCESS);
}
w2kServiceManager (hManager, &hManager1, FALSE);
return hService;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceClose (SC_HANDLE hService)
{
return (hService != NULL) && CloseServiceHandle (hService);
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceAdd (SC_HANDLE hManager,
PWORD pwName,
PWORD pwInfo,
PWORD pwPath)
{
SC_HANDLE hManager1, hService;
PWORD pwFile;
WORD awPath [MAX_PATH];
DWORD n;
BOOL fOk = FALSE;
w2kServiceManager (hManager, &hManager1, TRUE);
if ((hManager1 != NULL) && (pwName != NULL) &&
(pwInfo != NULL) && (pwPath != NULL) &&
(n = GetFullPathName (pwPath, MAX_PATH, awPath, &pwFile)) &&
(n < MAX_PATH))
{
if ((hService = CreateService (hManager1, pwName, pwInfo,
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
awPath, NULL, NULL,
NULL, NULL, NULL))
!= NULL)
{
w2kServiceClose (hService);
fOk = TRUE;
}
else
{
fOk = (GetLastError () ==
ERROR_SERVICE_EXISTS);
}
}
w2kServiceManager (hManager, &hManager1, FALSE);
ret