汇编里看Wow64的原理(浅谈32位程序是怎样在windows 64上运行的?)
Windows操作系统作为PC上最普及的操作系统,面向的用户各种各样,因此在版本升级时,对比其它操作系统,兼容性都要做得好,不需要用户费神DIY处理一些BUG。64位windows上市后大多以前32位的程序依旧正常地运行,当然这里主要指用户态程序。那64位windows是如何支持32位程序运行的呢?之前在看《windows核心编程》一书时只知道这个机制的名字叫wow64,但是具体如何实现的一无所知。为此我上网查阅资料,结果相关文章都讲得很笼统,包括Microsoft官方文档也是从很上层架构上进行了介绍,对于搞逆向的人来说,只了解架构不看代码怎么能忍,windows就在手边,何不亲自研究窥探一把庐山面目呢?说搞就搞,从代码的角度看一下这个wow64的大概。
这里插一句,其实在着手了解wow64机制前,是另外一个问题先引起了我的好奇:64位CPU比32位CPU除了每个寄存器宽度多了32bit,还多了几个通用寄存器:R8, R9, R10, R11, R12, R13, R14, R15,那32位程序在win64上运行时这些新加的寄存器就没用了吗?这个问题最后也会得到解决。
首先,先从宏观分析一下,32位程序的运行需要软硬件两个大方面的支持:
1)硬件上,CPU的解码模式需要是32位模式。64位CPU(我只熟悉INTEL的)是通过GDT表中CS段所对应的表项中L标志位来确定当前解码模式的。这里不展开描述GDT表与CPU运行模式的关系,感兴趣的可以参看http://www.secbox.cn/hacker/program/9875.html
2)软件上,操作系统需要提供32位的用户态运行时环境(C库,WINDOWS API)对32位程序支持,其次因为win64内核是64位模式的,所以32位运行时环境在与64位内核交互时需要有状态转换。
当然另外肯定还有大量其它的兼容32位软件所需要实现的功能,比如资源管理,句柄管理,结构化错误管理等等,这些属于细节就不进行研究了,我这里先看一个大体。
好了,接下来针对上面的分析进行探索。关于32位运行时环境这点,可以在c:/windows/syswow64中发现许多和c:/windows/system32下同名的动态链接库,如kernel32.dll, ntdll.dll, msvcrt.dll, ws2_32.dll等,其实这些都是32位的版本。像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进行了系统调用,中间没有其它操作,下面是相应的反汇编代码:
NtAllocateVirtualMemory:
7778F048 mov eax, 0x19B
7778F04D call sub_7778F055(7778F055)
7778F052 ret 0x18
sub_7778F055:
7778F055 mov edx, esp
7778F057 sysenter
7778F059 ret
看完原生32位操作系统里的样子,win64中运行一个32位程序时它的进程空间里的NtAllocateVirtualMemory是一番什么情景呢?我手头有win7 64bit版,运行的一个32bit程序进行调试,可以看到NtAllocateVirtualMemory的形式如下:
NtAllocateVirtualMemory:
77C8FAD0 mov eax,0x15
77C8FAD5 xor ecx,ecx
77C8FAD7 lea edx,[esp+0x4]
77C8FADB call dword ptr fs:[000000C0]
77C8FAE2 add esp,4
77C8FAE5 ret 0x18
OK,区别很明显,wow64中的ntdll.dll与原生32位windows中的ntdll.dll有了变动,它不再是与内核交互的最后一个用户态模块,而是call 进了fs:[C0]处的函数,隐约感觉这里就是打开wow64秘密的入口。fs:[C0]是什么呢?Windows操作系统中,fs寄存器用于记录线程环境块TEB,根据TEB结构体定义可以看出0xC0偏移处的定义为:
PVOID WOW32Reserved; // 0C0
其实在wow64之前还有wow32机制,用于兼容16位程序在32位windows上运行,与wow64异曲同工。所以windows系统在wow64中直接也拿这个保留位置用于进行32位64位环境切换的跳板。单步跟进,发现fs:[C0]处只有一行代码:
752B2320 jmp 0033:752B271E
这里是一个长跳转,目的地址是内存752B271E处,但是MDebug调试器显示752B271E处于未知模块。这时需要借助PCHunter,通过PcHunter发现该地址其实位于一个叫wow64cpu.dll的模块中,值得注意的一点是,该模块来自system32而非syswow64目录,是64位的文件模块,也就是说,wow64下32位程序的进程空间内同时加载了32位与64位的可执行文件模块!在这个32位程序的进程空间里一共有4个来自SYSTEM32目录64位的“客人”:
终于,这里有了真正的64位ntdll.dll的出现。所以很容易可以推断,wow64.dll, wow64win.dll, wow64cpu.dll组成了环境转换模块,而最终依然是ntdll.dll负责与内核交互,wow64中这4个模块在默默地在后台支持着32位程序的运行。
上面说到这里经历了一个长跳转,段寄存器由0x23变换为0x33,在win64中,0x23和0x33所对应的GDT表项中CPU的模式分别为32位与64位。自此,CPU解码模式由32位切换为64位。当然,故事还没结束。不过由于调试器是32位,无法准确捕获接下来发生的事情,单步跟进也没用了,我们转为使用IDA静态分析。
找到752B271E所对应的wow64cpu.dll中的位置:
00000000752B271E mov r8d,[esp] //取出返回地址
00000000752B2723 mov [r13+0xBC],r8d //保存返回地址
00000000752B272A mov [r13+0xC8],esp //保存32位环境堆栈指针
00000000752B2731 mov rsp,[r12+0x1480] //切换至64位环境堆栈
00000000752B2739 and qword ptr [r12+0x1480],0x0
00000000752B2742 mov r11d,edx
00000000752B2745 jmp qword ptr [r15+rcx*8]
第一句读取[esp]的值其实是把返回地址取出,接着保存到了r13所指向的地方,同时还保存了esp,然后重新赋值了rsp。看了这一小段,我们基本可以猜测到,在wow64中那个幕后的64位环境里其实是有自己的堆栈和执行上下文的,在CPU由32位切换到64位后,堆栈也相应切换。好了,下面要搞最后一句跳向了哪里,也就是[r15+rcx*8]的值,我们上面考察的NtAllocateVirtualMemory有xor ecx, ecx的操作,所以到这里时rcx = 0,所以就我们考察的例子而言,最后就是跳转到了[r15]。那,r15的值是多少?
这是有点棘手的问题。Wow64中32位程序只能由32位调试器调试,但是32位调试器下又无法获得64位模式下才可见的r15的值,怎么办?我这里使用shellcode的方式,利用32位MDebug调试shellcode的功能调试精心准备的一段shellcode,这段shellcode的作用独特而简单:让CPU切换到64位模式下“潇洒走一回”,将R8 ~R15的值记录到堆栈中,接着切换回32位模式。
shellcode的二进制为:
\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
它的反汇编如下:
/*开始时CPU处于32位模式*/
Push 0x33 // cs = 0x33
Call L1
L1:
add [esp], 5
retf // far ret,切换CPU状态
/*此时CPU处于64位模式*/
mov rax, 1122334455667788h //将r8~r15用特殊值与周边数据隔开,方便查看
push rax
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
push rax
Call L2:
L2:
mov [esp + 4], 0x23 // cs = 0x23
add [esp], 0xd
retf
虽然32位调试器无法对64位代码运行时下断,但是可以在切换回32位模式后的地方下断点。所以在这段代码后下一个断,运行代码。执行完毕后,查看一下堆栈上的收获:
地址 内容
0018FEA0 1122334455667788
0018FEA8 00000000752B2450 r15
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!