现在的位置: 首页 > 综合 > 正文

《Undocumented Windows 2000 Secrets》翻译 — 第四章(3)

2013年10月13日 ⁄ 综合 ⁄ 共 5448字 ⁄ 字号 评论关闭

第四章  探索Windows 2000的内存管理机制

翻译:Kendiv (fcczj@263.net )

更新:Sunday, February 17, 2005

 

声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。

 

Memory Spy Device示例

微软对Windows NT2000说的最多的就是它们是安全的操作系统。它们不但在网络环境中加入了用户验证系统,同时还加强了系统的稳健性(robustness),以进一步降低错误应用程序危及系统完整性的概率,这些错误的程序可能使用了非法的指针或者在其内存数据结构以外的地方进行了写入操作。这些在Windows 3.x上都是十分让人头疼得问题,因为Windows 3.x系统和所有的应用程序共享单一的内存空间。Windows NT为系统和应用程序内存以及并发的进程提供了完全独立的内存空间。每个进程都有其独立的4GB地址空间,如4-2所示。无论何时发生任务切换,当前的地址空间都会被换出(switch out),同时另一个被映射进来,它们各自使用不同的段寄存器、页表和其他内存管理结构。这种设计避免了应用程序无意中修改另一个程序所使用的内存。由于每个进程必然会要求访问系统资源,所以在4GB空间中总是包含一些系统数据和代码,并采用了一个不同的技巧来保护这些内存区域不被恶意程序代码所覆写(overwritten)。

 

Windows 2000的内存分段

Windows 2000继承了Windows NT 4.0的基本内存分段模型,默认情况下,该模型将4GB地址空间划分为相等的两块。低一半的地址范围是:0x00000000 ---- 0x7FFFFFFF,其中包含运行于用户模式(用Intel的术语来说是,是特权级3Ring 3)的应用程序的数据和代码。高一半的地址范围是:0x80000000 --- 0xFFFFFFFF,默认全部保留给系统使用,位于这一范围的代码运行于内核模式(即特权级为0Ring 0)。特权级决定了代码可以执行什么操作以及可以访问那一个块内存。这意味着对于低特权级的代码来说,会被禁止执行某些CPU指令或访问某些内存区域。例如,如果一个用户模式下的程序触及了任何0x80000000(即4GB地址空间中的高一半)以上的地址,系统会抛出一个异常并同时终止该程序的运行,不会给其任何机会。

 


4-5.  用户模式下不能访问0x80000000以上的地址

 

4-5展示了程序试图读取0x80000000地址时的情况。这种严格的访问限制对于系统的完整性来说是好事,但对于调试工具就不是什么好消息了,因为调试工具需要访问所有可用内存。幸运的是,存在着一个简单的方法:采用内核驱动程序,和系统本身类似,它也运行于高特权级(即Ring 3),因此它们可以执行所有的CPU指令,可访问所有的内存区域。这其中的诀窍就是将一个Spy驱动程序注入系统,用它来访问需要的内存,并将读到的内容发送到它的搭档程序,该搭档程序会在用户模式下等待。当然,内核驱动程序不能读取虚拟内存地址,而且得不到分页机制的支持。因此,这样的驱动程序必须在访问一个地址之前小心的检查它,以避免出现蓝屏死机(Blue Screen Of DeathBSOD)。相对于应用程序引发的异常(仅会终止出现问题的程序),驱动程序引发的异常会停止整个系统,并强迫进行重启。

 

设备I/O控制DispatcherDevice I/O Control Dispatcher

本书光盘上有一个通用Spy Device的源代码,该Spy Device作为内核驱动程序实现。可以在/src/w2k_spy目录下找到它的源代码。这个设备基于第三章的驱动向导所产生的驱动程序骨架。其用户模式下的接口为w2k_spy.sysw2k_spy.sys采用Win32的设备I/O控制(IOCTL),在第三章中曾简要的谈过IOCTLSpy Device定义了一个名为/Device/w2k_spy的设备和一个符号链接/DosDevices/w2k_spy,定义符号链接是为了能在用户模式下访问该设备。非常可笑的是符号链接的名字空间居然是/DosDevice,而在这儿,我们使用的可不是一个DOS设备驱动。这就像历史上有名的root,原本是叫做石头的J。安装好符号链接后,驱动程序就可以被用户模式下的任何模块打开了,方法是:使用Win32 API函数CreateFile(),路径为//./w2k_spy。字符串//./是通用转义符,表示本地设备。例如,//./C:指向本地硬盘上的C:分区。从SDK的文档中可了解CreateFile()的更多细节。

 

该驱动程序的头文件有一部分已经由列表4-2列表4-5给出。这个文件有些像DLL的头文件:它包含在编译过程中,模块所需的定义,而且还为客户端程序提供了足够的接口信息。DLL和驱动程序以及客户端程序都包含相同的头文件,但每个模块会取出各自所需的定义以完成正确的操作。不过,头文件的这种两面性给内核驱动程序带来的麻烦要远多于给DLL带来的,这都是因为微软给驱动程序提供的特殊开发环境所致。不幸的是,DDK中的头文件并不能和SDK中的Win32文件兼容。至少在C工程,二者的头文件是不能混合使用的。这样的结果就是陷入了僵局,此种情况下,内核驱动可以访问的常量、宏和数据类型对于客户端程序来说是却是无法使用的。因此,w2k_spy.c定义了一个名为_W2K_SPY_SYS_的标志常量,w2k_spy.h通过#ifdef…..#else…..#endif来检查该常量是否出现,以决定需要补充哪些缺少的定义。这意味着,所有出现在#ifdef _W2K_SPY_SYS_ 之后的定义仅可被驱动代码看到,位于#else之后的则专用于客户端程序。w2k_spy.h中条件语句之外的所有部分被这两个模块同时使用。

 

在第三章中,在讨论我的驱动向导时,我给出了向导生成的驱动程序骨架,如列表3-3所示。由该驱动向导生成的新的驱动工程均开始于DeviceDispatcher()函数。该函数接受一个设备上下文指针,以及一个指向IRPI/O请求包)的指针,该IRP随后将会被分派。向导的样板代码已经处理了基本的I/O请求:IRP_MJ_CREATEIRP_MJ_CLEANUPIRP_MJ_CLSE,当客户要关闭一个设备时,会给该设备发送这些I/O请求。DeviceDispatcher()针对这些请求只是简单的返回STATUS_SUCCESS,因此设备可以被正确的打开和关闭。对于某些设备,这种动作已经足够,但有些设备还需要初始化和清理代码,这些代码多少都有些复杂。对于其他的请求,第三章中的驱动程序骨架总是返回STATUS_NOT_IMPLEMENTED。扩展该骨架代码的第一步是修改默认的动作,以便处理更多的I/O请求。就像w2k_spy.sys的主要任务之一:通过IOCTL调用将在用户模式下无法访问的数据发送给Win32应用程序,因此首先需要在DeviceDispatcher()中添加处理IRP_MJ_DEVICE_CONTROL的函数。列表4-6给出了更新后的代码。

 

NTSTATUS DeviceDispatcher (PDEVICE_CONTEXT pDeviceContext,

                           PIRP            pIrp)

    {

    PIO_STACK_LOCATION pisl;

    DWORD              dInfo = 0;

    NTSTATUS           ns    = STATUS_NOT_IMPLEMENTED;

 

    pisl = IoGetCurrentIrpStackLocation (pIrp);

 

    switch (pisl->MajorFunction)

        {

        case IRP_MJ_CREATE:

        case IRP_MJ_CLEANUP:

        case IRP_MJ_CLOSE:

            {

            ns = STATUS_SUCCESS;

            break;

            }

        case IRP_MJ_DEVICE_CONTROL:

            {

            ns = SpyDispatcher (pDeviceContext,

                                pisl->Parameters.DeviceIoControl.IoControlCode,

                                pIrp->AssociatedIrp.SystemBuffer,

                                pisl->Parameters.DeviceIoControl.InputBufferLength,

                                pIrp->AssociatedIrp.SystemBuffer,

                                pisl->Parameters.DeviceIoControl.OutputBufferLength,

                                &dInfo);

            break;

            }

        }

    pIrp->IoStatus.Status      = ns;

    pIrp->IoStatus.Information = dInfo;

 

    IoCompleteRequest (pIrp, IO_NO_INCREMENT);

    return ns;

    }

列表4-6.  Dispatcher增加处理的IRP_MJ_DEVICE_CONTROL函数

 

列表4-6中的IOCTL处理代码非常简单,它仅调用了SpyDispatcher(),并将一个扩展后的IRP结构和当前I/O堆栈位置作为参数传递给SpyDispatcher()SpyDispatcher()列表4-7中给出,该函数需要如下的参数:

l         pDeviceContext一个驱动程序的设备上下文指针。驱动程序向导提供了的基本Device_Context结构,该结构中包含驱动程序和设备对象指针(参见列表3-4)。不过,Spy驱动程序在该结构中增加了一对私有成员。

l         dCode指定了IOCTL编码,以确定Spy设备需要执行的命令。一个IOCTL编码是一个32位整数,它包含4个位域,如4-6所示。

l         pInput指向一个输入缓冲区,用于给IOCTL提供输入数据。

l         dInput 输入缓冲区的大小。

l         pOutput指向用来接收IOCTL输出数据的输出缓冲区。

l         dOutput输出缓冲区的大小

l         pdInfo指向一个DWORD变量,该变量保存写入输出缓冲区中的字节数。

 

4-6.   设备I/O控制编码的结构

 

根据所用的IOCTL使用的传输模式,输入/输出缓冲区会以不同的方式从系统传递给驱动程序。Spy设备使用已缓存的I/Obuffered I/O),系统将输入数据复制到一个安全的缓冲区(此缓冲区由系统自动分配)中,在返回时,将指定数目的数据从同样的系统缓冲区中复制到调用者提供的输出缓冲区中。一定要牢记:在这种情况下,输入和输出缓冲区是重叠的,因此IOCTL的处理代码必须在向输出缓冲区中写入任何数据之前,保存所有它稍后可能需要使用的输入数据。系统I/O缓冲区的指针保存在IRP结构中的SystemBuffer成员中(参见ntddk.h)。输入/输出缓冲区的大小保存在一个不同的地方,它们是IRP的参数成员DeviceIoControl的一部分,分别为InputBufferLengthOutputBufferLengthDeviceIoControl子结构还通过其IoControlCode成员提供了IOCTL编码。有关Windows NT/2000IOCTL的传输模式的信息以及它们如何传入/传出数据,请参考我在Windows Developer’s Journal(Schreiber 1997)发表的文章“A Spy Filter Driver for Windows NT”。

 

NTSTATUS SpyDispatcher (PDEVICE_CONTEXT pDeviceContext,

                        DWORD           dCode,

                        PVOID           pInput,

                        DWORD           dInput,

                        PVOID           pOutput,

                        DWORD           dOutput,

                        PDWORD          pdInfo)

    {

抱歉!评论已关闭.