该文章我最早post到了看雪论坛:https://bbs.pediy.com/thread-221236.html 本文是重新修改的版本。
问题引出
在桌面级CPU中,Intel 从2004年末Pentium 4的 5xx系列开始向大众提供基于x86指令集扩展而来的x86-64指令的 64位CPU(这里要提一句,第一款x86-64指令集的CPU是AMD最先给出的),而桌面64位操作系统在此之后才可以被使用。Windows操作系统作为PC上最普及的操作系统,面向的用户各种各样,历史版本也十分复杂,因此在版本升级时,对比其它操作系统如linux,对旧程序的兼容性都要做得好,不需要用户费神DIY处理一些BUG。一个众所周知的感受是64位windows操作系统中以前32位的程序大都可以正常地运行,当然这里是指用户态程序(ring3)。 问题来了,微软的工程师用什么方式让64位windows支持32位程序运行的呢?之前在看《windows核心编程》一书时只知道这个机制的名字叫 wow64 及其作用的概括,但对具体实现机制一无所知。为此我上网查阅资料,结果相关文章都讲得很笼统,包括Microsoft官方文档也是从很上层架构上进行了介绍,对我来说,只了解架构不看代码就像牙疼一样难受必须处理。既然Windows系统如此易得常见,何不亲自研究窥探一把庐山面目呢?于是我就简单逆向了一下,从汇编代码的角度看一看这个wow64的机制。
这里插一句,其实在着手了解wow64机制前,是另外一个问题先引起了我的好奇,继而引发我对Wow64机制的探究:我们知道64位CPU比32位CPU除了将每个已有的通用寄存器宽度由32bit扩展到64bit,还增加了几个通用寄存器:R8, R9, R10, R11, R12, R13, R14, R15,那32位程序在win64上运行时这些新加的寄存器有没有用?有什么用? 最终这个问题会被解答。
简单理论分析
首先,先从理论大体分析一下,32位程序在64位操作系统上运行应该需要软硬件两个大方面的支持:
1)硬件上,CPU的解码模式需要兼容32位代码且能够来回切换。机器要理解一串01二进制存储的代码,需要知道这串二进制的编码规则从而进行解码,在32位时代,程序的代码是32位CPU的编码规则,而64位时代是64位CPU的编码规则,尽管64位CPU编码规则绝大部分兼容32位的规则。所以在对不同位数的代码执行时CPU一定需要进行解码模式切换。Intel 64位CPU(我暂时只熟悉INTEL的)是通过GDT(Global Descriptor Table) 表中CS段所对应的表项中L标志位来确定当前解码模式的,所以可以推测在运行32位程序时,该标志位应该是处于32位模式的,而回到系统代码(64位)时,L标志位的值又会切换到64位模式。根据此分析,经查阅相关资料,我发现64位操作系统下GDT表的确含有为32位程序运行准备的表项,当32位代码被CPU执行时,该表项会被加载以指导CPU的解码及其它行为。读者可参考cnblogs 的博客大体了解CPU架构。
2)软件上,操作系统需要提供32位的用户态运行时环境(如32位C运行时库,32位WINDOWS API等)对32位程序的运行提供支持,其次因为64位windows的内核肯定是64位模式的,所以32位用户态运行时环境在与64位内核交互时需要有状态转换,我们这里就以32位程序与64位内核交互(即模式切换)为方向进行探索。当然在软件层面另外肯定还有大量其它的兼容32位软件所需要实现的功能,比如资源管理,句柄管理,结构化错误管理等等,这些本质属于对模式切换机制的高级应用,就不进行研究了。
循迹探索
好了,接下来针对上面的分析进行探索。关于32位运行时环境这点,可以在 C:/windows/syswow64 中发现许多和 C:/windows/system32 下同名的动态链接库,如kernel32.dll, ntdll.dll, msvcrt.dll, ws2_32.dll等,其实这些都是32位的版本。(注意:system32的意思并不是说操作系统是32位的,64位操作系统的运行时环境依然保存在system32下,可参看下这篇文章) 像wow64名字所传达的含义一样,syswow64文件夹下的这些库相当于在64位windows中构建了一个32位windows子系统环境,我们32位的程序能正常在win64上运行正是靠这个子环境负责与64位环境进行了交互和兼容,所以需要重点探究下这个32位子环境是如何与win64环境交互的。
我这里用到的工具是PCHunter与调试器MDebug,静态分析工具IDA。
了解windows的读者都知道ntdll.dll是用户态与内核态交互的桥梁,所以我选择从ntdll.dll入手,选择了逻辑简单的NtAllocateVirtualMemory
函数。首先看一下原生32位操作系统里这个函数是什么样的。我手头有个win8 32bit版本,利用MDebug直接转到NtAllocateVirtualMemory
函数查看反汇编,可以看到,在设置好调用号0x19B
之后直接就使用sysenter
进行了系统调用,中间没有其它操作,下面是相应的反汇编代码:
1 | NtAllocateVirtualMemory: |
看完原生32位操作系统里的样子,win64中运行一个32位程序时它的进程空间里的NtAllocateVirtualMemory
是一番什么情景呢?我手头有win7 64bit版,运行的一个32bit程序进行调试,可以看到NtAllocateVirtualMemory
的形式如下:
1 | NtAllocateVirtualMemory: |
区别很明显,wow64中的ntdll.dll与原生32位windows中的ntdll.dll有了变动,它不再是与内核交互的最后一个用户态模块,而是call
进了fs:[C0]
处的函数,隐约感觉这里就是解密Wow64底层机制的入口。那么fs:[C0]
是什么呢?Windows操作系统中,fs
寄存器用于记录线程环境块TEB,根据TEB结构体定义可以看出0xC0
偏移处的定义为:
PVOID WOW32Reserved; // 0xC0
原来在wow64之前还有wow32机制,类似地,它是用于在32位windows上兼容运行旧16位的windows程序,这不是刚好可以旧药新用吗?所以windows系统在wow64中就利用这个保留位置用于进行32位64位环境切换的跳板。当然,这个“鹊巢鸠占“行为也就意味着64位windows无法直接支持16位程序了(但是可以通过安装虚拟机在64windows上运行16位程序,那是完全不同的原理)。
继续,单步跟进,发现fs:[C0]
处只有一行代码:
1 | 752B2320 jmp 0033:752B271E |
,但却是极为关键的一步。这里是一个长跳转( far jump ),CS段寄存器由当前的值变换为0x33
,当前是多少呢? 通过调试器很容易知道当前CS段寄存器是0x23
(实际上64位windows在运行32位程序时为其分配的默认CS值就是0x23
)。在64位windows操作系统中中,0x23
和0x33
所对应的GDT表项的CPU解码模式分别为32位与64位!所以,CPU解码模式自此由32位切换为64位!感觉有了很大发现!跳转目的地址是内存752B271E
处, 但是MDebug调试器显示752B271E
处于未知模块,这是因为调试器本身也运行于某个特定模式下,我们调试32位的程序使用的是32位调试器,必然运行于32位模式下,而752B271E
处所在的世界则是64位的,从对用户透明设计的原则出发操作系统自然不会将这里的结构信息主动提供给调试器。这时需要借助PCHunter,通过PcHunter发现该地址其实位于加载到内存中的一个叫wow64cpu.dll的模块中,而该模块是来自system32而非syswow64目录,也就是说:wow64下32位程序的用户态进程空间内同时加载了32位与64位的文件模块!其实在这个32位程序的进程空间里一共有4个来自system32目录的“客人”:
而且,这里有了真正的64位ntdll.dll的出现,即真正可以与内核直接交互的接口。所以很容易可以推断,wow64.dll, wow64win.dll, wow64cpu.dll提供了模式转换的运行环境,而最终依然是ntdll.dll负责与内核交互,wow64中这4个模块在默默地在后台支持着32位程序的运行。
当然,故事还未结束。不过由于调试器是32位,无法准确捕获接下来发生的事情了,单步跟进也无济于事,故我们转为使用IDA静态分析。
找到wow64cpu.dll中752B271E
所对应的位置进行静态反汇编:
1 | 00000000752B271E mov r8d,[esp] //取出返回地址 |
注释是我自行添加,非IDA提供。第一句读取[esp]
的值其实是把返回地址取出,接着保存到了r13
所指向的位置,同时还保存了esp
,然后重新赋值了rsp
。看了这一小段,我们基本可以猜测到,在wow64中那个幕后的64位运行子环境里其实是有自己的堆栈和执行上下文的,在CPU模式由32位切换到64位后,堆栈也相应切换。好了,下面要搞明白最后一句跳向了哪里,也就是[r15+rcx*8]
的值,我们上面考察的NtAllocateVirtualMemory
在77C8FAD5
处有xor ecx, ecx
的操作,所以很明显到这里时rcx = 0
,所以就我们考察的例子而言,最后就是跳转到了[r15]
。r15
的值是多少?
这是有点棘手的问题。Wow64中32位程序只能由32位调试器调试,但是32位调试器下又无法获得64位模式下才可见的r15
的值,怎么办?我这里使用shellcode的方式,利用调试器直接调试精心准备的一段shellcode,这段shellcode的作用独特而简单:让CPU强行从32位切换到64位模式,当再次到32位模式被我们重新捕获时,r8
~r15
的值已经被记录到了堆栈中。
shellcode的二进制为:
1 | \x6A\x33\xE8\x00\x00\x00\x00\x83\x04\x24\x05\xCB\x48\xB8\x88\x77\x66\x55\x44\x33\x22\x11\x50\x41\x50\x41\x51\x41\x52\x41\x53\x41\x54\x41\x55\x41\x56\x41\x57\x50\xE8\x00\x00\x00\x00\xC7\x44\x24\x04\x23\x00\x00\x00\x83\x04\x24\x0D\xCB\x90 |
它的汇编意义如下:
1 | /*注意:起始时CPU处于32位模式,shellcode的反汇编以32位进行解码查看*/ |
虽然32位调试器无法对64位代码运行时下断,但是可以在切换回32位模式后的地方下断点。所以在这段代码的最后 nop
处设置断点,运行代码。执行完毕后,查看一下堆栈上的收获:
1 | 地址 内容 |
Bingo!我们成功获得到了32位程序运行时r8
~r15
的值。还记得我们上面的分析中断在了r15
的值是多少?所以此时可以我们根据r15
的值定位了,它就是位于wow64cpu.dll文件模块内,是指向了一堆函数指针:
1 | 78B62450 dq offset TurboDispatchJumpAddressEnd //r15指向处 |
这里注意IDA的内存值和动态运行时会有偏移,因为模块是经过了内存地址随机化加载。其实在第一次打开wow64cpu.dll寻找那个最开始的64位环境中的位置752B271E
时,就可以看到它附近有一个名为CpuSimulate
的函数,里面有这样的操作:
1 | 78B625F9 mov r12, gs:30h |
可以看到r15
是指向了偏移78B62450
处,对应随机化动态加载后就是752B2450
。所以这也印证了我们的实验结果。这里还可以看到的一点是r12
指向了64位运行子环境下的TEB,因为64位下gs
段寄存器用于记录TEB结构。
回到r15
的跳转去向问题上,已经得知[r15]
是指向了TurboDispatchJumpAddressEnd
处,就是上文jmp qword ptr [r15+rcx*8]
所要跳转到的地方(ecx = 0
),看一下它的代码:
1 | TurboDispatchJumpAddressEnd: |
上面的代码最后跳转到78B62611
,loc_78B62611
的代码如下:
1 | loc_78B62611: |
可以看到,在TurboDispatchJumpAddressEnd
代码片段中,调用了一个外部函Wow64SystemServiceEx
,由这个函数再继续未完成的工作,它最终调用64位的ntdll.dll的NtAllocateVirtualMemory
来完成真正的系统调用与内核交互。TurboDispatchJumpAddressEnd
最后跳转至78B62611
,将CPU主要寄存器值恢复至之前保存好的32位环境中的值,同时在堆栈中排布好返回地址,cs段寄存器值,eflags
值,执行iretq
,返回至32位环境中,在我们的例子中,即返回到NtAllocateVirtualMemory
中call dword ptr fs:[C0]
的下一句,从32位用户的视角来看就像执行了一个普通函数一样。上面讲到跳转的函数指针表是根据r15+rcx*8
来得到的,在32位子空间的ntdll.dll里面的函数在call dword ptr fs:[C0]
前都有对ecx
的赋值,我们可以推测在wow64中,系统调用被分成多类,类别号存在于rcx
中(注意不是系统号),64位子环境根据rcx
的值来进行不同类别的模拟转换。
Wow64SystemServiceEx
做的事情就暂时不详细研究了,感兴趣的人可以细细钻研。
对这次简单的wow64之旅做个小总结:
windows/syswow64目录下的大量DLL库与SYSTEM32目录下的wow64.dll, wow64cpu.dll, wow64win.dll, ntdll.dll支撑着wow64机制
Wow64下32位进程中实际有32位和64位两个逻辑子空间,每个子空间都 有各自的数据结构、堆栈,64位子空间负责与操作系统内核交互:
32位用户态模式 <————-> 64位用户态模式 <—————————> 64位内核
- Wow64模式下,那些不可见的寄存器并不都是闲置不用的,实际上它们在切换到64位子环境后全部启用,和正常64位程序无差别。且经过分析可以知道有确切作用的寄存器有:
R12
: 指向64位环境的TEB结构体
R13
:指向保存32位环境CPU的状态的位置
R15
: 指向系统服务跳转函数指针列表的起始
上面是针对win7下做的一个wow64机制小探索,我也简单看了下在win8和win10下的wow64过程,在反汇编代码上有些小不同,但是逻辑原理是完全相同的,感兴趣的可以实践。
PS:windbg可以通过wow64的扩展组件更好地调试wow64程序。然而我在写文章前却不知道。。。。。。