第四章 探索Windows 2000的内存管理机制
翻译:Kendiv( fcczj@263.net )
更新:Sunday, February 17, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
IOCTL函数SPY_IO_PDE_ARRAY
SPY_IO_PDE_ARRAY是另一个普通的函数,它只是简单的把整个页目录(开始于地址0xC0300000)复制到调用者提供的输出缓冲区中。该缓冲区采用列表4-21所示的SPY_PDE_ARRAY结构。你可能已猜到,该结构的大小正好是4KB,它由1,024个32位的PDE组成。X86_PE结构将在这里使用,X86_PE结构代表一个一般化的页项(page entry),可在列表4-3中找到该结构的定义,常量X86_PAGES_4M定义在列表4-5。SPY_PDE_ARRAY的结构体成员总是页目录项(PDE),X86_PE结构可以是X86_PDE_4M类型,也可以是X86_PDE_4KB类型,这取决于PDE的PS位的取值。
在无法保证源数据页存在于物理内存时,就开始复制内存中的数据通常并不是一个好主意。不过,页目录是少数列外中的一个。在当前任务处于运行状态时,它的页目录总是存在于物理内存中。它不会被置换到页面文件中,除非另一个任务被置换进来。这就是为什么CPU的页目录基地址寄存器(PDBR)没有P(present)位的原因,PDE和PTE也类似。请参考列表4-3中的X86_PDBR结构的定义,以验证这一点。
typedef struct _SPY_PDE_ARRAY
{
X86_PE apde [X86_PAGES_4M];
}
SPY_PDE_ARRAY, *PSPY_PDE_ARRAY, **PPSPY_PDE_ARRAY;
#define SPY_PDE_ARRAY_ sizeof (SPY_PDE_ARRAY)
列表4-21. SPY_PDE_ARRY结构的定义
IOCTL函数SPY_IO_PAGE_ENTRY
如果你对给定线性地址的page entry感兴趣的话,这个函数就是一个很好的选择。列表4-22给出了SpyMemoryPageEntry()的内部细节,该函数就是用来处理SPY_IO_PAGE_ENTRY请求的。该函数返回的SPY_PAGE_ENTRY结构本质上是一个X86_PE page entry(定义于列表4-3),不过这里增加了两个新成员(为了使用方便):dSize和fPresent。其中dSize成员用于说明页的大小(以字节为单位),其值不是X86_PAGE_4KB(4,096字节)就是X86_PAGE_4MB(4,194,304字节);fPresent成员用来说明页是否存在于物理内存中。这个标志必须和SpyMemoryPageEntry()自身的返回值进行对比,即使fPresent为FALSE,函数自身的返回值也可为TRUE。此时,提供的线性地址时有效的,但它指向的数据页已被置换到了页面文件中。这种情况可通过设置page entry的第10位(即列表4-22中出现的PageFile)来表示。当P位(该位属于X86_PNPE结构)被置0时,PageFile就会被设置。请参考本章稍早讨论过的X86_PNPE结构的细节。X86_PNPE结构代表一个page-not-persent entry,该结构定义于列表4-3。
SpyMemoryPageEntry()首先假定目标页是4MB页,然后,从系统的PDE数组(此数组起始于0xC0300000)中复制指定线性地址的PDE到SPY_PAGE_ENTRY结构体的pe成员。如果P位不为0,则肯定存在下一级的页或页表,所以接下来检查PS位以确定页面大小。如果PS位不为0,则表示此PDE指向一个4MB数据页,工作到此就可结束了------SpyMemoryPageEntry()返回TRUE,并且SPY_PAGE_ENTRY结构体的fPresent成员也同时被设为TRUE。如果PS位为0,则PDE指向的是一个PTE,所以代码从起始于0xC0000000的数组中提取该PTE,并检查它的P位。如果不为0,则包含指定线性地址的4KB页存在于物理内存中,此时,SpyMemoryPageEntry()和fPresent都会报告TRUE。否则,找到的必定是一个page-not-present entry,因此SpyMemoryPageEntry()返回TRUE,不过仅当PageFile位不为0时,fPresent成员才会被设为FALSE。
typedef struct _SPY_PAGE_ENTRY
{
X86_PE pe;
DWORD dSize;
BOOL fPresent;
}
SPY_PAGE_ENTRY, *PSPY_PAGE_ENTRY, **PPSPY_PAGE_ENTRY;
#define SPY_PAGE_ENTRY_ sizeof (SPY_PAGE_ENTRY)
// -----------------------------------------------------------------
BOOL SpyMemoryPageEntry (PVOID pVirtual,
PSPY_PAGE_ENTRY pspe)
{
SPY_PAGE_ENTRY spe;
BOOL fOk = FALSE;
spe.pe = X86_PDE_ARRAY [X86_PDI (pVirtual)];
spe.dSize = X86_PAGE_4M;
spe.fPresent = FALSE;
if (spe.pe.pde4M.P)
{
if (spe.pe.pde4M.PS)
{
fOk = spe.fPresent = TRUE;
}
else
{
spe.pe = X86_PTE_ARRAY [X86_PAGE (pVirtual)];
spe.dSize = X86_PAGE_4K;
if (spe.pe.pte4K.P)
{
fOk = spe.fPresent = TRUE;
}
else
{
fOk = (spe.pe.pnpe.PageFile != 0);
}
}
}
if (pspe != NULL) *pspe = spe;
return fOk;
}
列表4-22. 查询PDE和PTE
需要注意的是,SpyMemoryPageEntry()不能识别被置换出物理内存的4MB页。如果PDE指向的4MB页并不存在,将无法判断给定的线性地址是否有效的,以及该页是否还保存在当前页面文件中。4MB页仅用于内核内存范围:0x80000000----0x9FFFFFFF。不过我从来没见过这样的一个页被置换出去,即使物理内存极端少的时候也没有过,因此我不需要检查任何与此相关的page-not-present entries。
IOCTL函数SPY_IO_MEMORY_DATA
SPY_IO_MEMORY_DATA函数是重量级函数中的一个,因为它可以复制任意数量的内存数据到调用者提供的缓冲区中。正如你可能还记得的那样,用户模式下的应用程序很容易传入一个无效的地址。因此,该函数在触及源地址之前,会非常谨慎的检验这些地址的有效性。记住,蓝屏可以潜伏在内核模式的任何地方。
调用程序通过传入一个SPY_MEMORY_BLOCK结构来请求一个内存块中的数据,在列表4-23的顶部给出了该结构体的定义,该结构体会指定内存块的地址和大小。为了方便,此处的地址被定义为一个union,以允许将其解释为一个字节类型的数组(PBYTE pbAddress)或解释为一个无类型的指针(PVOID pAddress)。列表4-23中的SpyInputMemory()函数将从IOCTL的输入缓冲区中复制该结构。其搭档函数SpyOutputMemory()(在列表4-23的末尾处)只是SpyMemoryReadBlock()的一个外包而已,列表4-24给出了SpyMemoryReadBlock()函数。SpyOutputMemory()的主要职责是在SpyMemoryReadBlock()读取数据后,返回适当的NTSTATUS值。
SpyMemoryReadBlock()通过一个SPY_MEMORY_DATA结构返回它读到的内存数据。该结构定义于列表4-25。我选择了一中不同的定义方式,因为SPY_MEMORY_DATA是一个针对变量大小的数据类型。基本上,它包含一个名为smb的SPY_MEMORY_BLOCK结构,随后是一个WORD类型的数组,名为awData[]。该数组的长度由smb的dBytes成员给出。为了允许方便的按预定大小定义SPY_MEMORY_DATA的全局或局部实体,该结构的定义采用了一个宏----SPY_MEMORY_DATA_N()。该宏的唯一参数用于指定awData[]数组的大小。实际的结构体定义在宏定义之后,它提供的结构体中包含一个长度为0的awData[]数组。SPY_MEMORY_DATA__()宏首先计算SPY_MEMORY_DATA结构的全部大小,然后按这一大小分配结构中的数组,剩下的定义允许将WORD型的数据加入数组或从数组中取出。显然,每个WORD的低半位包含内存数据的字节数,高半位作为标志位。现在,仅有第8位有意义,用于表示位于0—7位的内存字节数是否有效。
typedef struct _SPY_MEMORY_BLOCK
{
union
{
PBYTE pbAddress;
PVOID pAddress;
};
DWORD dBytes;
}
SPY_MEMORY_BLOCK, *PSPY_MEMORY_BLOCK, **PPSPY_MEMORY_BLOCK;
#define SPY_MEMORY_BLOCK_ sizeof (SPY_MEMORY_BLOCK)
// -----------------------------------------------------------------
NTSTATUS SpyInputMemory (PSPY_MEMORY_BLOCK psmb,
PVOID pInput,
DWORD dInput)
{
return SpyInputBinary (psmb, SPY_MEMORY_BLOCK_, pInput, dInput);
}
// -----------------------------------------------------------------
NTSTATUS SpyOutputMemory (PSPY_MEMORY_BLOCK psmb,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
NTSTATUS ns = STATUS_BUFFER_TOO_SMALL;
if (*pdInfo = SpyMemoryReadBlock (psmb, pOutput, dOutput))
{
ns = STATUS_SUCCESS;
}
return ns;
}
列表4-23. 处理内存块
DWORD SpyMemoryReadBlock (PSPY_MEMORY_BLOCK psmb,
PSPY_MEMORY_DATA psmd,
DWORD dSize)
{
DWORD i;
DWORD n = SPY_MEMORY_DATA__ (psmb->dBytes);
if (dSize >= n)
{
psmd->smb = *psmb;
for (i = 0; i < psmb->dBytes; i++)
{
psmd->awData [i] =
(SpyMemoryTestAddress (psmb->pbAddress + i)
? SPY_MEMORY_DATA_VALUE (psmb->pbAddress [i], TRUE)
: SPY_MEMORY_DATA_VALUE (0, FALSE));
}
}
else
{
if (dSize >= SPY_MEMORY_DATA_)
{
psmd->smb.pbAddress = NULL;
psmd->smb.dBytes = 0;
}
n = 0;
}
return n;
}
// -----------------------------------------------------------------
BOOL SpyMemoryTestAddress (PVOID pVirtual)
{
return SpyMemoryPageEntry (pVirtual, NULL);
}
// -----------------------------------------------------------------
BOOL SpyMemoryTestBlock (PVOID pVirtual,
DWORD dBytes)
{
PBYTE pbData;
DWORD dData;
BOOL fOk = TRUE;
if (dBytes)
{
pbData = (PBYTE) ((DWORD_PTR) pVirtual & X86_PAGE_MASK);
dData = (((dBytes + X86_OFFSET_4K (pVirtual) - 1)
/ PAGE_SIZE) + 1) * PAGE_SIZE;
do {
fOk = SpyMemoryTestAddress (pbData);
pbData += PAGE_SIZE;
dData -= PAGE_SIZE;
}
while (fOk && dData);
}
return fOk;
}
列表4-24. 复制内存块中的数据
SpyMemoryTestAddress()用于测试数据的有效性,SpyMemoryReadBlock()针对要读取的每个字节都会调用SpyMemoryTestAddress()。SpyMemoryTestAddress()在列表4-24的下半部分给出,该函数只是简单的调用SpyMemoryPageEntry(),不过传入的第二个参数为NULL。SpyMemoryPageEntry()在讨论SPY_IO_PAGE_ENTRY时已经介绍过(列表4-22)。将其PSPY_PAGE_ENTRY指针参数设为NULL,意味着调用者不关心指定线性地址对应的page entry,因此,如果线性地址有效,函数将返回TRUE。在SpyMemoryPageEntry()的上下文中,仅当一个线性地址对应的数据页存在于物理内存中,或者位于页面文件中,该地址才是有效的。注意,这种行为与ntoskrnl.exe中的API函数MmIsAddressValid()并不一致,当指定的页不存在于物理内存中时,MmIsAddressValid()总是返回FALSE,即使这个有效的数据据页位于页面文件中也会如此。列表4-24中的另一个函数SpyMemoryTestBlock()是SpyMemoryTestAddress()的增强版。它可测试一个内存区域的有效性,它每次可测试指定块中的4,096个字节,直到测试完区域中的所有页为止。
#define SPY_MEMORY_DATA_N(_n)
struct _SPY_MEMORY_DATA_##_n
{
SPY_MEMORY_BLOCK smb;
WORD awData [_n];
}
typedef SPY_MEMORY_DATA_N (0)
SPY_MEMORY_DATA, *PSPY_MEMORY_DATA, **PPSPY_MEMORY_DATA;
#define SPY_MEMORY_DATA_ sizeof (SPY_MEMORY_DATA)
#define SPY_MEMORY_DATA__(_n) (SPY_MEMORY_DATA_ + ((_n) * WORD_))
#define SPY_MEMORY_DATA_BYTE 0x00FF
#define SPY_MEMORY_DATA_VALID 0x0100
#define SPY_MEMORY_DATA_VALUE(_b,_v)
((WORD) (((_b) & SPY_MEMORY_DATA_BYTE ) |
((_v) ? SPY_MEMORY_DATA_VALID : 0)))
列表4-25. SPY_MEMORY_DATA的定义
将置换出去的页作为有效的地址范围有一个很重要的好处:当SpyMemoryReadBlock()试图读取这些页中的第一个字节时,这些页就会被很快的再次调入内存中。稍后给出的内存Dump工具如果依赖MmIsAddressValid(),有时就会拒绝显示指定地址范围中的数据(即使5分钟之前,它还可以显示这些数据),而这仅仅是因为这些页可能已被传送到了页面文件中。
IOCTL函数SPY_IO_MEMORY_BLOCK
SPY_IO_MEMORY_BLOCK依赖于SPY_IO_MEMORY_DATA,因为它也是从任意地址复制内存块到调用者的缓冲区中。主要的区别是:SPY_IO_MEMORY_DATA试图复制所有可读取的字节,而对于SPY_IO_MEMORY_BLOCK来说,只要请求的范围中包含无效地址它就会失败,一个字节也不会复制。第6章中需要这个函数来将位于内核空间中的数据结构传递给用户模式下的程序。这一要求显然会大大限制这个函数,所以若一个结构体中包含无法读取的字节,就跳过它们,仅复制可读取的字节。
和SPY_IO_MEMORY_DATA类似,SPY_IO_MEMORY_BLOCK期望输入一个SPY_MEMORY_BLOCK结构来指定要复制的内存块的基地址和大小。返回的数据,将是原始数据的1:1复制品。输出缓冲区必须足够容纳要复制的全部内容。否则,将会报告一个错误,并且不会返回任何数据。
IOCTL函数SPY_IO_HANDLE_INFO
和前面介绍的SPY_IO_PHSICAL类似,这个函数允许用户模式下的程序调用其他途经无法调用的内核模式API。内核驱动程序可通过ntoskrnl.exe导出的obReferenceObjectByHandle()来获取由句柄描述的对象的指针。而在Win32下没有对等的函数。不过,应用程序可以命令Spy设备执行这一函数,并返回对象的指针。列表4-26展示了由SpyDispatcher()调用的SpyOutputHandleInfo()函数。可通过SpyInputHandle()获(定义于列表4-10)取输入的句柄。
列表4-26顶部的SPY_HANDLE_INFO结构包含与句柄相关的对象体的指针,以及该句柄的属性,这两个都会由ObReferenceObjectByHandle()返回。特别重要的一点是:如果ObReferenceObjectByHandle()调用成功,就必须调用ObDereferenceObject()来将对象的引用计数器恢复到先前的值。如果没有这样做,将会导致“对象引用漏洞”。
typedef struct _SPY_HANDLE_INFO
{
PVOID pObjectBody;
DWORD dHandleAttributes;
}
SPY_HANDLE_INFO, *PSPY_HANDLE_INFO, **PPSPY_HANDLE_INFO;
#define SPY_HANDLE_INFO_ sizeof (SPY_HANDLE_INFO)
// -----------------------------------------------------------------
NTSTATUS SpyOutputHandleInfo (HANDLE hObject,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_HANDLE_INFO shi;
OBJECT_HANDLE_INFORMATION ohi;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
if (hObject != NULL)
{
ns = ObReferenceObjectByHandle (hObject,
STANDARD_RIGHTS_READ,
NULL, KernelMode,
&shi.pObjectBody, &ohi);
}
if (ns == STATUS_SUCCESS)
{
shi.dHandleAttributes = ohi.HandleAttributes;
ns = SpyOutputBinary (&shi, SPY_HANDLE_INFO_,
pOutput, dOutput, pdInfo);
ObDereferenceObject (shi.pObjectBody);
}
return ns;
}
列表4-26. 通过句柄引用一个对象
………………待续…………………