7 《Undocumented Windows 2000 Secrets》翻译 --- 第四章

第四章 探索 Windows 2000 的内存管理机制
翻译: Kendiv( fcczj@263.net)
更新: Sunday, February 17, 2005
声明:转载请注明出处 , 并保证文章的完整性 , 本人保留译文的所有权利 。
内存 Dump 工具 ---- 本书示例程序
现在你已经学完了复杂和让人困惑的内存 Spy 设备的 IOCTL 函数的代码 , 你可能想看这些函数运行起来是什么样子 。因此 , 我创建了一个控制台模式的工具 , 名为:“ SBS Windows 2000 Memory Spy ” , 该工具会加载 Spy 驱动程序 , 根据命令行出入的参数 , 它会调用多个 IOCTL 函数 。该程序的可执行文件为: w2k_mem.exe , 其源代码位于本书光盘的 srcw2k_mem 目录下 。
命令行格式
你可以从光盘中运行内存 Spy 工具: d:binw2k_mem.exe , 这里 d: 应该由你的 CD-ROM 盘符代替 。如果无参数启动 w2k_mem.exe , 将会列出冗长的命令信息 , 如 示列 4-1 所示 。W2k_mem.exe 基本的命令体系是:一个命令包含一个或多个数据请求 , 每个命令都至少提供一个线性基址 , 内存 Dump 将从该地址开始 。如果你愿意还可指定内存块的大小 , 不过这是可选的 , 内存块的默认大小是 256。命令中的内存大小必须以“ # ”开始 。可通过增加多个选项来改变命令的默认行为 。一个选项包括一个单字符的选项 ID 和一个“”或“ - ”前缀 。“”或“ - ”表示允许或禁止该选项 。默认情况下 , 所有选项都是允许的 。
// w2k_mem.exe
// SBS Windows 2000 Memory Spy V1.00
// 08-27-2000 Sven B. Schreiber
// sbs@orgon.com
Usage: w2k_mem { { [ option|-option] [/] } [#[[0]x]] [[0]x] }
specifIEs a module to be loaded into memory.
Use thex/-x switch to enable/disable its startup code.
Ifis missing, the default size is 256 bytes.
Display address options (mutually exclusive):
z -z zero-based display on / OFF
r -r physical RAM addresses on / OFF
Display mode options (mutually exclusive):
w -w Word data formatting on / OFF
d -d DWORD data formatting on / OFF
q -q QWORD data formatting on / OFF
Addressing options (mutually exclusive):
t -t TEB-relative addressing on / OFF
f -f FS-relative addressing on / OFF
u -u user-mode FS:[] on / OFF
k -k kernel-mode FS:[] on / OFF
h -h handle/object resolution on / OFF
a -a add bias to last base on / OFF
s -s sub bias from last base on / OFF
p -p pointer from last block on / OFF
System status options (cumulative):
o -o display OS information on / OFF
c -c display CPU information on / OFF
g -g display GDT information on / OFF
i -i display IDT information on / OFF
b -b display contiguous blocks on / OFF
Other options (cumulative):
x -x execute DLL startup code on / OFF
Example: The following command displays the first 64
bytes of the current Process Environment Block (PEB)
in zero-based DWORD format, assuming that a pointer to
the PEB is located at offset 0x30 inside the current
Thread Environment Block (TEB):
w2k_memt #0 0pzd #64 0x30
Note: Specifying #0 aftert causes the TEB to be
addressed without displaying its contents.
示列 4-1. 内存 Spy 工具的帮助信息
每个命令行所执行的数据请求不等同于选项 , 数据大小的说明 , 路径或任何其他的命令修饰成分 。命令中的每个无格式的数字都被假定是一个线性地址 , 并且将从该地址开始 , 按 16 进制显示其内容 。数字默认按 10 进制格式解释 , 如果有前缀“ 0x ”或“ x. ”则按照 16 进制格式解释 。
如果提供一些简单的示例 , 很容易掌握 w2k_mem.exe 采用的复杂命令行选项 , 下面就给出一些:
l w2k_mem 0x80400000 显示从线性地址 0x80400000 开始的 256 个字节 , 产生的内容可能会类似于示列 4-2。顺便说一下 , 这是 ntoskrnl.exe 的 Dos stub (注意开始的“ MZ ” ID ) 。
l w2k_mem #0x40 0x80400000 显示从线性地址 0x80400000 开始的 64 个字节 , #0x40 表示要显示的块大小为 64
l w2k_memd #0x40 0x80400000 在前一命令的基础上 , 按照 32 位的 DWORD Chunk 来显示 , 这就是d 选项的作用 。在同一个命令中 , 首先出现的选项将会一直有效 , 除非使用相应的 - 选项或使用其互斥选项 。如d 的互斥选项为:w 、q。
l w2k_memwz #0x40 0x10000d –z 0x200000 包含两个数据请求 。首先 , 线性地址范围: 0x10000----0x1003F 中的内容将按照 16 位 WORD 格式来显示 , 随后的 0x20000---0x2003F 按照 32 位 DWORD 格式显示(见 示列 4-3 ) 。第一个请求中还包含一个z 选项 , 该选项将使“ Address ”列的数字从 0 开 。在第二个请求中 , 通过 -z 选项 , 禁用了从 0 开始的显示模型 。
l w2k_memrd #4096 0xC0300000 以 DWORD 格式显示起始于 0xC0300000 的系统页目录 。r 选项表示在“ Address ”列中以物理内存地址代替线性地址 。
现在 , 你应该基本上明白命令行格式是如何工作的了 。在下一小节中 , 将详细讨论一些比较特别的选项和特性 。它们中的大多数会改变对出现在它们之前的地址的解释方式 。在默认情况下 , 指定的地址是一个线性基址 , 内存 Dump 将从那里开始 。选项:t 、f 、u 、k 、h 、a 、s 和p 将以多种方式改变这种默认解释方式 。
示列 4-2. 数据请求示列
示列 4-3. 以指定格式显示数据
与 TEB 相关的地址
进程中的每个线程都有其自己的线程环境块( Thread Environment Block , TEB ) , 系统在此 TEB 中保存频繁使用的线程相关的数据 。在用户模式下 , 当前线程的 TEB 位于独立的 4KB 段 , 可通过 CPU 的 FS 寄存器来访问该段 。而在内核模式下 , FS 却指向不同的段 , 下面将解释之 。一个进程的所有 TEB 都以堆栈的方式 , 存放在从 0x7FFDE000 开始的线性内存中 , 每 4KB 为一个完整的 TEB , 不过该内存区域是向下扩展的 。这意味着 , 第二个线程的 TEB 的地址将是 0x7FFDC000 , 这和堆栈类似 。在第七章 , 我们会详细讨论 TEB 的内容和进程环境块( Process Environment Block , PEB )的地址 0x7FFDF000 (参见 列表 7-18 和 7-19 ) 。这里知道 TEB 的存在 , 而且知道其地址由 FS 寄存器给出就足够了 。
如果在一个地址之前出现了t 选项 , w2k_mem.exe 将自动把 FS 段的基地址加到该地址上 , 示列 4-4 展示了 w2k_memdt #0x38 0 命令执行后的输出 。这一次我省略了 w2k_mem.exe 输出的标题和状态信息 。
示列 4-4. 显示第一个线程环境块( TEB )
与 FS 相关的地址
我前面已经提到过 , 在用户和内核模式下 , FS 将指向不同的段 。t 选项将选择用户模式下 FS 所指向的地址 , f 选项则使用在内核模式下 FS 指向的地址 。当然 , Win32 应用程序没有办法获取该地址 , 因此 , 需要再次请求 Spy 设备 。w2k_mem.xe 调用 IOCTL 函数 SPY_IO_CPU_INFO , 来读去 CPU 的状态信息 , 这包括所有段寄存器在内核模式下的值 。从此开始 , 所有的事情和t 选项相同 。
内核模式的 FS 指向另一个线程相关的结构 , Windows 2000 内核回经常使用该结构 , 其名称为:内核的处理器控制区域( Kernel"s Processor Control Region , KPCR ) 。该结构在讨论 IOCTL 函数 SPY_IO_OS_INFO 时已经提及过 , 在第七章我们还会再次提到它(见 列表 7-16 ) 。再次强调 , 现在你只需要知道该结构存在于线性地址 0xFFDFF000 处即可 , 使用f 选项就可访问它 。在 示列 4-5 中 , 我使用命令: w2k_memdf #0x54 0 来演示 , 在实际情况下 , 使用f 选项的结果 。
示列 4-5. 显示内核的处理器控制区域( KPCR )
FS:[Base] 寻址方式
在察看 Windows 2000 内核代码时 , 你会经常遇到像 MOV EAX, FS:[18h] 这样的指令 。这些指令用于取出属于 TEB 或 KPCR 的成员的值 , 或者是属于其他包含在 FS 段中的结构体的成员的值 。它们中的大多数都指向其他的内部结构 。命令行选项u 和k 允许你;u 表示使用用户模式下的 FS 段;k 表示使用内核模式下的 FS 段 。例如 , 命令: w2k_memdu #0x1E8 0x30 (见 示列 4-6 )将在用户模式下 , 从位于 FS:[30h] 处的内存块中转储( dump ) 488 个字节 。而命令: w2k_memdk #0x1C 0x20 (见 示列 4-7 )将显示由内核模式下的 FS:[20h] 指向的内存块的前 28 个字节 , 这实际上是指向 KPRCB 的一个指针 。如果你不知道 PEB 或 KPRCB 是什么 , 不要着急 , 读完本书你就会一目了然了 。
示列 4-6. 显示进程环境块( PEB )
示列 4-7. 显示内核的处理器控制区域( KPRCB )
句柄 / 对象 解析
假设你有一个对象句柄 , 而且你想要看看该句柄对应的对象在内存中是什么样子 。如果你使用h 选项 , 你就会发现完成这一任务太简单了 , 该选项将调用 Spy 设备的 SPY_IO_HANDLE_INFO 函数(见 列表 4-26 )来查找给定句柄的对象体( Object Body ) 。Windows 2000 对象世界是一个令人惊讶的主题 , 我将在第七章深入剖析它 。所以 , 现在先把它丢掉一边去 。
相对寻址
有时使用这种寻址方式可以很容易显示一系列内存块 , 这些内存块间隔相同大小的字节 。这很有可能 , 比如 , 一个数组结构 , 像朵线程程序中的 TEB 堆栈 。a 和s 选项通过将给定的地址解释为一个偏移量 , 来进行对寻址 。这两个选项的区别是:a ( add bias )将产生一个正的偏移量 , s ( subtract bias )则产生一个负的偏移量 。示列 4-8 展示了命令: w2k_memd #32 0xC0000000a 4096 4096 的输出结果 。它将取出三个连续 4KB 页中的前 32 个字节 , 起始地址为: 0xC0000000 , 系统的页表就位于此处 。注意 , a 选项接近命令的结尾处 。它将使随后的“ 4096 ”将被解释为偏移量 , 该偏移量将被加到前面的基地址上 。
示列 4-8. 页表样本
示列 4-8 还展示了如果传入一个无效的线性地址会发生什么 。显然 , 第一对页表涉及的 4MB 地址范围: 0x00000000----0x003F0000 和 0x00400000-----0x007F0000 是有效的 。而第三对页表则是无效的 。w2k_mem.exe 会通过显示一个空表来反映这一现实 。程序知道那个地址范围是有效的 , 因为 Spy 设备的 SPY_IO_MEMORY_DATA 函数将此信息放入作为结果的 SPY_MEMORY_DATA 结构中(参见 列表 4-25 ) 。
间接寻址
我所钟爱的命令选项之一就是:p , 因为在我准备这本书的时候 , 它为我节省了很多打字的时间 。该选项和u 和k 的工作方式类似 , 但不使用 FS 段 , 而是使用先前显示过的数据块 。这是一个很棒的特性 , 如果你想向下寻找链表上的对象 , 例如 , 读取下一个成员的地址 , 随该命令一起 , 键入一个新的命令等等 , 通过简单在命令中加入p 选项和一系列偏移量 , 就可以指定下一个对象的链接在前一个 16 进制 Dump 表中的位置 。
在 示列 4-9 中 , 我使用该选项来向下遍历当前活动进程的链表 。首先 , 我告诉通过内核调试器获取系统内部变量 PsActiveProcessHead 的地址 , 该地址是一个 LIST_ENTRY 结构 , 用于标识进程链表的开始 。LIST_ENTRY 结构中包含一个 Flink (向前指针)成员和一个 Blink (向后)成员 。Flink 成员位于偏移量 0 处 , Blink 成员位于偏移量 4 处(参见 列表 2-7 ) 。命令: w2k_mem #8d 0x8046A180p 0 0 0 0 首先转储 PsActiveProcessHead (这是一个 LIST_ENTRY 结构) , 然后从p 选项出开始转为间接寻址 。选项后的四个 0 是用来告诉 w2k_mem.exe 提取前一个数据块中偏移量为 0 的值 , 这正是 Flink 所在的位置 。注意 , 示列 4-9 中的 Blink 成员在偏移量为 4 的位置上 , 它指向前一个 LSIT_ENTRY 之后 , 就像我们期望的那样 。
译注:
对于 w2k_mem #8d 0x8046A180p 0 0 0 0 命令
0x8046A180 需要由你自己系统中的 PsActiveProcessHead 的地址来替代 。
可通过内核调试器来查找 PsActiveProcessHead 的地址 , 我在这里使用的是 livekd , 命令为: ln PsActiveProcessHead
如果命令中加入了足够的值为 0 的参数 , 16 进制转储最终会回到 PsActiveProcessHead , 它用来标识进程链表的开始和结束 。就像第二章里解释的那样 , Windows 2000 维护的双向链表实际上是一个环;也就是说 , 链表中最后一个成员的 Flink 将指向链表中的第一个成员 , 而链表中第一个成员的 Blink 指向最后一个成员 。
示列 4-9. 向下遍历活动进程链表
加载模块
有时你可能会想 dump 一个模块在内存中的映像 , 但是该模块还没有映射到 w2k_mem.exe 进程的线性地址空间 。通过使用 / 和x 选项来显示的加载一个指定模块就可解决这一问题 。每个前缀为斜线(“ / ”)的命令项将被解释为模块的全路径名 , w2k_mem.exe 将尝试使用 Win32 API 函数 LoadLibraryEx() 从该路径出加载模块 。默认情况下 , 将使用加载选项 DON"T_RESOLVE_DLL_REFERENCES , 这会使模块被加载到内存中 , 但不会被初始化 。对于一个 DLL , 这意味着它的 DllMian() 入口点将不会被调用 。同样 , 在该 DLL 的导入节中指定的依赖模块也都不会被加载 。然而 , 如果你在路径参数之前 , 指定了x 选项 , 那么模块将在加载后进行完整的初始化 。注意 , 有些模块可能会拒绝在 w2k_mem.exe 进程的上下文环境中被初始化 。例如 , 内核模式的设备驱动程序就不能在使用x 选项的情况下 , 被加载到内存中 。
加载和显示一个模块一般需要经过两个操作步骤 , 如 示列 4-10 所示 。首先 , 你应该加载模块 , 而不显示任何数据 , 以找出系统分配给该模块的基地址 。幸运的是 , 只要在此期间 , 没有其他的模块加入到进程中 , 模块的加载地址就将是唯一的 , 因此 , 接下来尝试通过相同的基地址来加载模块 。在 示列 4-10 中 , 我加载了内核模式的设备驱动程序 nwrdr.sys , 它是微软的 NetWare 重定向器 。在我的系统里没有使用 IPX/SPX , 因此 , 默认没有加载该驱动程序 。
【7 《Undocumented Windows 2000 Secrets》翻译 --- 第四章】列表 4-10. 加载和显示一个模块映像( Module Image )