Windows学习笔记(1)-WoW64机制初探

WoW64(Windows 32-bit on Windows 64-bit)是Windows x64提供的一种兼容机制,可以认为WoW64是64位Windows系统创建的一个32位模拟环境,使得32位可执行程序能够在64位的操作系统上正常运行。本文的主要内容是探索WoW64进程的初始化过程。

0x00. WoW64进程初始化过程

目前在网上能搜到的有关WoW64实现原理文章很少,我找到了一篇非常详细的深度好文WoW64 internals
…re-discovering Heaven’s Gate on ARM
,Microsoft也给了一篇不太详细的官方文档WOW64 Implementation Details,只能说是聊胜于无,但我们仍然可以从有限的文档中简单地了解一下WoW64的实现过程。

首先WoW64使用了几个DLL,简单来说可以归为以下两类:

  • 提供函数调用转换支持的DLL:
    • wow64.dll: 将32位的 Nt*系列的系统调用转化为64位 (ntoskrnl.exe / ntdll.dll)
    • wow64win.dll: 将32位的NtGdi*, NtUser*系列的系统调用和其他GUI相关的系统调用转化为64位(win32k.sys / win32u.dll)
  • 提供64位指令模拟支持的DLL:
    • wow64cpu.dll: 在x64架构上模拟x86指令
    • wowarmhw.dll: 在ARM64架构上模拟ARM32指令
    • xtajit.dll: 在ARM64架构上模拟x86指令

本文以x64为例讲解,所以忽略最后两个其他架构下的DLL,此时WoW64使用的主要就是Wow64.dll, Wow64Win.dllWow64Cpu.dll三个DLL。

从下面这一段中我们了解到对于32位环境下的系统调用,WoW64会将指针和栈操作扩充为64位,并切换到64位模式执行:

Instead of using the x86 system-service call sequence, 32-bit binaries that make system calls are rebuilt to use a custom calling sequence. This calling sequence is inexpensive for WOW64 to intercept because it remains entirely in user mode. When the custom calling sequence is detected, the WOW64 CPU transitions back to native 64-bit mode and calls into Wow64.dll. Thunking is done in user mode to reduce the impact on the 64-bit kernel and to reduce the risk of a bug in the thunk that might cause a kernel-mode crash, data corruption, or a security hole. The thunks extract arguments from the 32-bit stack, extend them to 64 bits, then make the native system call.

从官方文档里能获取的信息大概就是这么多了,为了了解更多的实现细节,我们需要在Windows上进行调试。

写一段非常简单的程序,并在VS上编译:

1
2
3
4
5
#include <Windows.h>

int main() {
MessageBox(NULL, L"Hello WoW64!", L"Just for test", MB_OK);
}

用Windbg调试,首先会断在ntdll!LdrpDoDebuggerBreak处,此时输入lm(list modules)指令可以查看当前加载的模块,主要就是ntdll、wow64和wow64win三个模块,对应Microsoft文档里的说法:

image-20211103193425042

上方被标红的几个DLL很明显为64位,因为其地址空间是64位的,也可以通过dh(dump headers)指令验证:

image-20211103193739614

并且此时程序被断下来的位置也是在64位的ntdll中,可以从地址空间判断:

image-20211103193847741

再用k指令看看函数调用栈,由此可以推断进程加载时会先在64位模式完成一些初始化操作

image-20211103194124546

输入g(go)指令执行到下一个断点,发现还是断在了ntdll!LdrDoDebuggerBreak处,不同的是此时已经进入了32位模式:

image-20211103195609450

并且加载了各种32位模块:

image-20211103195809709

image-20211103195829205

为了了解进程是如何从64位模式切换到32位模式,以及进程是如何加载这些32位模块的,我们重新从第一个断点开始调试。

Step Over单步步过,退出ntdll!LdrDoDebuggerBreak函数调用的第一个函数是wow64!Wow64LdrpInitialize,根据函数名推断应该是跟WoW64初始化有关的函数。步过这个函数,立马就弹出了黑框框并切换到了32位模式,所以这个函数是我们要重点分析的关键函数:

image-20211103201126353

wow64!Wow64LdrpInitialize

这回我们Step into步入wow64!Wow64LdrpInitialize函数,根据上面提到的WoW64 internals这篇文章,wow64!Wow64LdrpInitialize函数的内容大致如下:

  • wow64!Wow64LdrpInitialize
    • wow64!Wow64InfoPtr = (NtCurrentPeb32() + 1)
    • NtCurrentTeb()->TlsSlots[/* 10 */ WOW64_TLS_WOW64INFO] = wow64!Wow64InfoPtr
    • ntdll!RtlWow64GetCpuAreaInfo
    • wow64!ProcessInit
    • wow64!CpuNotifyMapViewOfSection // Process image
    • wow64!Wow64DetectMachineTypeInternal
    • wow64!Wow64SelectSystem32PathInternal
    • wow64!CpuNotifyMapViewOfSection // 32-bit NTDLL image
    • wow64!ThreadInit
    • wow64!ThunkStartupContext64TO32
    • wow64!Wow64SetupInitialCall
    • wow64!RunCpuSimulation
      • emu!BTCpuSimulate

其中调用的每个函数实在不太好一个个去调试,所以就结合原文简单解释一下每个函数的作用。

wow64!Wow64InfoPtrwow64.dll中第一个被初始化的变量,包含了一些在32位和64位环境共享的数据。

image-20211103232414581

RtlWow64GetCpuAreaInfo函数的作用是读取当前CPU的架构以及模拟进程的上下文(Context)。该函数读取的信息被封装到一个名为WOW64_CPU_AREA_INFO的结构体中,并传递给之后的ProcessInit函数:

image-20211103234321237

ProcessInit函数首先取消wow64.dll.mrdata区段的保护,.mrdata区段存储的是一些易变的只读数据,这个区段在模块加载时被标记为PAGE_READONLY,理论上无法修改。但是有些模块加载时需要修改 .mrdata 区段中的某些数据。为此 Windows 提供了一个新的 API LdrProtectMrdata( bProtect )函数用于设置 .mrdata区段是否开启保护 ,参数传入0表示unprotect,传入1表示 protect。

然后调用ntdll!LdrLoadDll函数尝试加载%SystemRoot%\system32\wow64log.dll,这个DLL在发行版Windows上都不存在,所以正常情况下尝试加载这个DLL都会失败。但是如果我们自己写了一个wow64log.dll,那么WoW64就会加载我们自己的DLL,这一点可以被用来注入DLL到WoW64进程,mark一下this post by Walied Assar

image-20211105113010253

CpuNotifyMapViewOfSection函数会进行如下检查:

  • Checks if the mapped image is executable
  • Checks if following conditions are true:
    • NtHeaders->OptionalHeader.MajorSubsystemVersion == USER_SHARED_DATA.NtMajorVersion
    • NtHeaders->OptionalHeader.MinorSubsystemVersion == USER_SHARED_DATA.NtMinorVersion

image-20211105114749331

Wow64DetectMachineTypeInternal函数返回当前进程的machine type,Wow64SelectSystem32PathInternal 函数根据返回的machine type选取System32 路径。e.g. SysWOW64 for x86 processes or SysArm32 for ARM32 processes.

image-20211105120041736

ThreadInit函数完成一些线程的初始化操作。随后调用 ThunkStartupContext64TO32(CpuArea.MachineType, CpuArea.Context, NativeContext)Wow64SetupInitialCall(&CpuArea) 为32位模拟进程完成一些必要的初始化工作。

image-20211105120814782

最后调用RunCpuSimulation开始整个32位进程模拟过程,并且不再返回。

image-20211105120742941

0x01. 参考资料

  1. WoW64 internals

  2. WOW64 Implementation Details

  3. Bypass CFG Through MRDATA