今天我们探索一个问题: 64位的ntdll是如何被加载到WoW64下的32位进程?今天的旅程将会带领我们进入到Windows内核逻辑中的未知领域,我们将会发现32位进程的内存地址空间是如何被初始化的。
来自MSDN:
WOW64是允许32位Windows应用程序无缝运行在64位Windows的模拟器。
换句话说,随着64位版本Windows的引进,Microsoft需要拿出一种允许在32位时代的Windows程序与64位Windows新的底层组件无缝交互的解决方案。特别是64位内存寻址和与内核直接交流的组件。
在32位的Windows系统中,要调用Windows API的应用程序需要经过一系列的动态链接库(DLL)。然而,所有的系统调用最终会定向到ntdll.dll,它是在用户模式下将用户模式API传递给内核的最高层。以调用CreateFileW为例,这个API调用源于用户模式下的kernel32.dl,随后它以NtCreateFile传递给ntdll,随后NtCreateFile通过系统调度程序将控制权传递给内核。
在32位Windows下这是非常简单的,然而,在WoW64下需要额外的步骤。32位的ntdll不可以直接将控制权交给内核,因为内核是64位的,只接受遵循64位ABI的类型(译者注:ABI,Application Binary Interface,应用二进制接口)。正因为如此,一个翻译层以几个标准的命名为wow64.dll,wow64cpu.dll和wow64win.dll的DLL的形式被添加到64位Windows。这几个DLL负责将32位调用转换成64位调用。那些调用最终被定向到映射到每个32位进程中的64位ntdll。许多关于这种从32位系统调用到64位系统调用(1)的神奇转换的信息是可获得的,所以我们不会从这里进入。我们最关注的是内核何时和怎样将64位版本的ntdll映射到一个32位进程。看起来像这样:
我们特别关注倒数第二项。我们能发现ntdll被映射到地址是64位地址范围(7FFFFED40000-7FFFFEF1FFFF),而且它的位置在Windows 64位系统文件所在的System32\路径下。然而,我们知道32位进程不可以访问或者运行在64位内存空间。
为了理解上面输出的内容,我们首先讨论VAD(Virtual Address Descriptor,虚拟地址描述符)是什么和它将如何帮助我们理解加载64位dll到32位进程的机制的。
VAD是Windows操作系统跟踪系统中可用物理内存的许多方法之一。VAD专门跟踪每个进程用户模式范围的保留的和提交的地址。任何时候一个进程请求一些内存,一个新的VAD实力被创建用来跟踪内存。
VAD被构造成一个自平衡树,每个节点描述了一段内存范围。每个节点至多包含两个子节点,左边是低地址,右边是高地址。每个进程被分配一个VadRoot,之后通过遍历VadRoot来分辨额外用来描述保留或提交的虚拟地址范围的额外节点。我们需要关注WindDBG中的!vad命令的输出,因为这是我们将大量使用来跟踪64位Windows中32位进程的映射的输出。对于这个练习,不是所有的域对我们来说都是特别有趣的。我们考虑测试程序HelloWorld.exe的输出。通过!process ProcessObject 命令的输出来分辨我们进程的VadRoot。
一旦我们确定了VadRoot,我将地址输入到 !vad 命令。(输出为了容易分析已被截断)
我们看到五列: "VAD", "Level", "Start", "End", 和"Commit".!vad命令 接受VAD实例的地址;在我们的例子中,我们已经为它提供了在此进程中通过使用!process命令获得的VadRoot。
VAD地址是当前VAD结构体或实例的地址:
进程初始化的早期,在主可执行文件被映射和初始化之前,Windows为特殊区域确定和保留一些地址范围。其中包含初始进程地址空间,共享系统空间(_KUSER_SHARED_DATA),控制流守护位图区域,和NT本地子系统(ntdll)。由于进程初始化整体的复杂性,我们只关注最后一块,它包含32位ntdll和64位ntdll加载到32位进程地址空间的逻辑。我们关注一系列的API调用和在每个点的内存区域的虚拟地址描述符(VAD)。为了让内核区分怎样映射一个新进程,它需要知道是否这是一个WoW64进程。当进程对象最初被创建,内核通过读取名为_EPROCESS.Wow64Process的未文档化结构体_EPROCESS结构体的值来实现此操作。
PspAllocateProcess是我们探索开始的地方,但是更具体的说,我们开始在MmInitializeProcessAddressSpace()。MmInitializeProcessAddressSpace()负责与一个新进程地址空间有关的初始化。它调用MiMapProcessExecutable,该函数创建了定义初始进程可寻址内存空间的VAD项,随后将新创建的进程映射到它的基虚拟地址。
一个特别有趣的函数是PspMapSystemDlls。我们关注在调用PspMapSystemDlls之前的进程地址空间的样子。在WinDBG中确保我们当前处于我们测试应用程序的上下文中(.process),并寻找当前VadRoot(!vad output)。
到目前为止我们可以观察到,我们的进程在32问地址空间中被映射和分配了一个基地址(1200),内核共享内存(0x7FFE0000-0x7FFE0FFF) 和64KB保留内存区域(0x7FFE1000-0x7FFEFFFF) 也已经被映射到他们各自的虚拟地址。
PspMapSystemDlls通过一个包含多个平台子系统模块的全局指针迭代。对于x86和x64Windows,这些是分别位于C:\Windows\SysWow64 和C:\Windows\System目录中的ntdll.dll。
一旦PspMapSystemDlls发现要加载的DLL,它调用PspMapSystemDll 来映射他们(DLLs)到进程的地址空间。该函数非常简短,下面展示了一个片段。为了正确映射本地子系统,需要满足一些条件。
PspMapSystemDll通过调用MmMapViewOfSection实现实际的本地DLL的映射,并保存所占的基地址。在这两个DLL映射完成并且他们的VAD项初始化完成后,我们的32位进程地址空间看起来像这样:
所以现在,我们映射完我们的进程(0xc40000-0xcf2fff),内核共享内存空间(0x7ffe0000-0x7ffe0fff),32位地址空间的有效结束区域(0x7ffe1000-0x7ffeffff),和我们的两个NT子系统DLL。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-3-17 20:25
被sudozhange编辑
,原因: