首页 >> 操作系统 >> Windows 2000

《Undocumented Windows 2000 Secrets》翻译 --- 第六章(3)

作者:Kendiv

第六章  在用户模式下调用内核API函数

翻译:Kendiv( fcczj@263.net )

更新:Friday, May 06, 2005

 

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

 

将系统模块和驱动程序加载到内存中

NtQuerySystemInformation()Windows 2000系统编程中主要API函数之一,几乎所有内建的管理工具都使用了该函数,但是你不会在DDKDevice Driver Kit)文档的任何地方找到它。唯一提及该函数的就是ntddk.hCONFIGURATION_INFORMATIONS结构的注释,这证实了这一函数的存在。如果存在“无正式文档系数”的话,并且按照在微软文档中出现频率来划分函数的有用程度,那么NtQuerySystemInformation()将当仁不让的位居榜首。随同很多其他让人振奋的功能,该函数还可返回已加载的系统模块的列表,这包括所有的系统核心组件和内核模式的驱动程序。

 

Spy driver的源文件中包含最少的直接代码和类型定义,以从NtQuerySystemInformation()获取已加载模块的列表。从调用者角度来看,这是一个非常简单的函数。该函数需要四个参数,如列表6-6所示。SystemInformationClass是一个从0开始的数值,它用来指定要查询的信息的类型。这里的Information可以是变长的,其具体大小依赖于所查询的信息的类型,查询到的信息将被复制到调用者提供的SystemInformation指向的缓冲区中。缓冲区的长度由SystemInformationLength参数指定。如果调用成功,复制到缓冲区的实际字节数将被写入ReturnLength指向的变量中。这个函数的问题是,在它发现缓冲区太小的情况下,它不会报告它实际想要复制多少字节。因此,调用者必须在一个循环中不断尝试,直到函数的返回代码从STATUS_INFO_LENGTH_MISMATCH0xC0000004)变为STATUS_SUCCESS0x00000000)。

 

NTSTATUS NTAPI ZwQuerySystemInformation( DWORD SystemInformationClass,

                                         PVOID  SystemInformation,

                                         DWORD SystemInformationLength,

                                         PDWORD ReturnLength);

列表6-6.  NtQuerySystemInformation()的原型

 

列表6-6没有给出NtQuerySystemInformation()自身,但给出了该函数的另一个“兄弟”:ZwQuerySystemInformation(),这一函数除了函数名前缀不同之外,其运行机理与NtQuerySystemInformation()是相同。你或许还记得第二章中的Nt*Zw* Native API函数集合。如果从用户模式进行调用的话,这两种函数的工作方式非常相似,在用户模式下,这两组函数都将通过ntdll.dll到达相同的INT 2Eh Stub。不过,在内核模式下,情形就有些不同了。此时,ntoskrnl.exe将控制对Native API的调用,Nt*()Zw*()的执行路径将不再相同。Zw*()函数还是通过INT 2Eh中断门,这和ntdll.dll的处理方式相同。而Nt*()却绕过了此中断门。在DDK文档的术语表中,微软是这样描述Zw*()函数集的(Microsoft 2000f):

 

一组与执行体的系统服务(executive’s system services)平行的入口点。从内核模式的代码()中调用一个ZwXxx入口点将获得相应的系统服务,只是在使用Zw*()函数时,不会检查调用者的访问权限和参数的有效性,而且调用不会将先前模式(previous mode)切换到用户模式Windows 2000 DDK\Kernel-Mode Drivers\Design Guide\Kernel-Mode Glossary \Z\Zw routines.

 

上文最后一句中提到的“先前模式(previous mode)”非常重要。Peter G. ViscarolaW. Anthony Mason

“尽管任意一组函数都可以从内核模式调用,但如果用Zw*()函数来代替Nt*()函数,则可将先前模式(此后的模式才是请求被发出的模式)切换到内核模式。”(ViscarolaMason 1999p.18

 

对先前模式(previous-mode)的处理带来的副作用是,在没有任何附加预防措施的情况下,从内核模式的驱动程序中调用NtQuerySystemInformation()函数将返回一个出错状态代码:STATUS_ACCESS_VIOLATION0xC0000005),而对ZwQuerySystemInformation()的调用则可成功,或者返回STATUS_INFO_LENGTH_MISMATCH

 

列表6-7中给出了SystemInformationClass所需的常量和类型的定义。已加载模块的列表将通过一个MODULE_LIST结构返回,每个模块均包含一个32位的模块计数和一个MODULE_INFO类型的数组。

 

#define SystemModuleInformation 11 // SYSTEMINFOCLASS

 

typedef struct _MODULE_INFO

    {

    DWORD dReserved1;

    DWORD dReserved2;

    PVOID pBase;

    DWORD dSize;

    DWORD dFlags;

    WORD  wIndex;

    WORD  wRank;

    WORD  wLoadCount;

    WORD  wNameOffset;

    BYTE  abPath [MAXIMUM_FILENAME_LENGTH];

    }

    MODULE_INFO, *PMODULE_INFO, **PPMODULE_INFO;

 

#define MODULE_INFO_ sizeof (MODULE_INFO)

// -----------------------------------------------------------------

typedef struct _MODULE_LIST

    {

    DWORD       dModules;

    MODULE_INFO aModules [];

    }

    MODULE_LIST, *PMODULE_LIST, **PPMODULE_LIST;

 

#define MODULE_LIST_ sizeof (MODULE_LIST)

列表6-7.  SystemModuleInformation定义

 

现在调用ZwQuerySystemInformation()所需的一切都已准备好。列表6-8给出了SpyModuleList()函数的实现方式,该函数使用一个trial-and-error循环(指,出错-尝试方式的循环,在第一章提及过),和两个简单的内存管理函数---SpyMemoryCreate()SpyMemoryDestroy(),这两个函数内部将调用Windows 2000执行体函数(Executive functionExAllocatePoolWithTag()ExFreePool()SpyModuleList()函数在开始时将使用4,096Byte的缓冲区,如果ZwQuerySystemInformation()的返回值为STATUS_INFO_LENGTH_MISMATCH,则将缓冲区扩大一倍,然后再次尝试调用ZwQuerySystemInformation()。如果ZwQuerySystemInformation()返回了其他的值,将终止循环。SpyModueList()的可选参数pdDatapns,将关返回更详细的信息。如果SpyModueList()返回NULL,则表示调用失败,此时pns指向的缓冲区中将保存一个错误代码,*pdData将被设为0。如果SpyModueList()调用成功,*pdData将保存复制到缓冲区中的字节数,*pns的值将为STATUS_SUCCESS

 

#define SPY_TAG             ''''>YPS''''             // SPY>

 

PVOID SpyMemoryCreate (DWORD dSize)

    {

    return ExAllocatePoolWithTag (PagedPool, max (dSize, 1),

                                  SPY_TAG);

    }

 

// -----------------------------------------------------------------

PVOID SpyMemoryDestroy (PVOID pData)

    {

    if (pData != NULL) ExFreePool (pData);

    return NULL;

    }

 

// -----------------------------------------------------------------

PMODULE_LIST SpyModuleList (PDWORD    pdData,

                            PNTSTATUS pns)

    {

    DWORD        dSize;

    DWORD        dData = 0;

    NTSTATUS     ns    = STATUS_INVALID_PARAMETER;

    PMODULE_LIST pml   = NULL;

 

    for (dSize = PAGE_SIZE; (pml == NULL) && dSize; dSize <<= 1)

        {

        if ((pml = SpyMemoryCreate (dSize)) == NULL)

            {

            ns = STATUS_NO_MEMORY;

            break;

            }

        ns = ZwQuerySystemInformation (SystemModuleInformation,

                                       pml, dSize, &dData);

        if (ns != STATUS_SUCCESS)

            {

            pml   = SpyMemoryDestroy (pml);

            dData = 0;

 

            if (ns != STATUS_INFO_LENGTH_MISMATCH) break;

            }

        }

    if (pdData != NULL) *pdData = dData;

    if (pns    != NULL) *pns    = ns;

    return pml;

}

列表6-8.  使用ZwQuerySystemInformation()获取模块列表

 

剩下的操作将用来获取给定模块的基地址,这将非常简单。列表6-9定义了两个函数:SpyModuleFind()SpyModuleList()的增强版,它可以根据指定的模块文件名来扫描ZwQuerySystemInformation()返回的模块列表,SpyModuleBase()反复调用SpyModuleFind(),从模块的MODULE_INFO结构中提取出模块的基地址。SpyModuleHeader()函数调用SpyModuleBase()并将获取的模块基地址传递给RtlImageNtHeader()。该函数是进入已加载模块导出节(export section)的第一步。

 

PMODULE_LIST SpyModuleFind (PBYTE     pbModule,

                            PDWORD    pdIndex,

                            PNTSTATUS pns)

    {

    DWORD        i;

    DWORD        dIndex = -1;

    NTSTATUS     ns     = STATUS_INVALID_PARAMETER;

    PMODULE_LIST pml    = NULL;

 

    if ((pml = SpyModuleList (NULL, &ns)) != NULL)

        {

        for (i = 0; i < pml->dModules; i++)

            {

            if (!_stricmp (pml->aModules [i].abPath +

                           pml->aModules [i].wNameOffset,

                           pbModule))

                {

                dIndex = i;

                break;

                }

            }

        if (dIndex == -1)

            {

            pml = SpyMemoryDestroy (pml);

            ns  = STATUS_NO_SUCH_FILE;

            }

        }

    if (pdIndex != NULL) *pdIndex = dIndex;

    if (pns     != NULL) *pns     = ns;

    return pml;

    }

 

// -----------------------------------------------------------------

PVOID SpyModuleBase (PBYTE     pbModule,

                     PNTSTATUS pns)

    {

    PMODULE_LIST pml;

    DWORD        dIndex;

    NTSTATUS     ns    = STATUS_INVALID_PARAMETER;

    PVOID        pBase = NULL;

 

    if ((pml = SpyModuleFind (pbModule, &dIndex, &ns)) != NULL)

        {

        pBase = pml->aModules [dIndex].pBase;

        SpyMemoryDestroy (pml);

        }

    if (pns != NULL) *pns = ns;

    return pBase;

    }

 

// -----------------------------------------------------------------

PIMAGE_NT_HEADERS SpyModuleHeader (PBYTE     pbModule,

                                   PPVOID    ppBase,

                                   PNTSTATUS pns)

    {

    PVOID             pBase = NULL;

    NTSTATUS          ns    = STATUS_INVALID_PARAMETER;

    PIMAGE_NT_HEADERS pinh  = NULL;

 

    if (((pBase = SpyModuleBase (pbModule, &ns)) != NULL) &&

        ((pinh  = RtlImageNtHeader (pBase))      == NULL))

        {

        ns = STATUS_INVALID_IMAGE_FORMAT;

        }

    if (ppBase != NULL) *ppBase = pBase;

    if (pns    != NULL) *pns    = ns;

    return pinh;

    }

列表6-9.  查找指定模块的信息

 

解析导出函数、变量的符号

前一小节解释了如何搜索一个PE文件映像中导出函数和变量的符号化名称,以及如何确定已加载系统模块或驱动程序的基地址。现在,是时候将这些零碎的东西整理一下了。基本上,查找一个给定模块的导出符号需要如下三个步骤:

1.         找到模块的线性基地址

2.         搜索模块导出节中的符号

3.         将找到的符号的相对偏移量和模块基地址相加

 

第一步已经讨论过。列表6-10提供了剩余步骤地实现细节。SpyModuleExport()需要一个文件名,如ntoskrnl.exehal.dllntfs.sys等等,pbModule参数返回一个指向模块的IMAGE_EXPORT_DIRECTORY结构的指针。可选参数ppBasepns返回附加的信息:*ppBase在成功的情况下返回模块的基地址,*pns在出错的情况下返回错误状态的诊断信息。首先,SpyModuleExport()调用SpyModuleHeader()来定位IMAGE_NT_HEADERS;然后,它计算PE DataDirectory数组中第一个元素(该元素是一个IMAGE_DATA_DIRECTORY结构)所保存的有关导出节的信息。如果IMAGE_DATA_DIRECTORY结构的VirtualAddress成员不是NULL,并且Size成员是一个合理的值,则可以断定该PE Image包含至少一个导出节。此时,SpyModuleExport()使用PTR_ADD()宏(见列表6-10)将VirtualAddress加上模块基地址,从而得到IMAGE_EXPORT_DIRECTORY的绝对线性地址。否则,SpyModuleExport()将返回NULL,并将状态代码设为STATUS_DATA_ERROR0xC000003E)。

 

#define PTR_ADD(_base,_offset) \

        ((PVOID) ((PBYTE) (_base) + (DWORD) (_offset)))

 

// -----------------------------------------------------------------