在本文中,我们将为读者详细介绍WOW64子系统运行机制及其Hooking技术。
Microsoft公司一直以其向后兼容性而闻名。几年前,当他们推出Windows的64位版本时,他们需要提供与现有32位应用程序的兼容性。为了实现32位和64位应用程序的无缝衔接,WoW(Windows on Windows)系统便应运而生了;在本文中,我们将其称为“WOW64”,它负责将所有Windows API调用从32位用户空间转换为64位操作系统内核。本文主要分为两个部分。首先,我们将深入研究WOW64系统本身。为此,我们将考察一个来自32位用户空间的调用,并跟踪它最终过渡到内核的步骤。在本文的第二部分,我们将考察两种hooking技术及其有效性。我将介绍该系统的工作原理,恶意软件滥用它的方式,并详细介绍一种机制,通过这个机制可以从用户空间钩住所有WoW系统调用。请注意,文中的所有信息对于Windows 10, version 2004之后的版本来说起都是正确的;而在某些情况下,与较旧的Windows版本的实现方式可能存在差异。
声明
首先,这是一个已有多位作者研究过的课题。也就是说,虽然本文对高效探索WOW64内部机制至关重要,但是,如果这些作者不公开发布其出色的研究成果,那么,我们将不得不花费更多的研究时间。本文参考的文献包括:
(Wbenny): 关于ARM架构WOW64内部结构及其运行机理的详细阐述。
(ReWolf):一个PoC天堂之门的实现代码。
(JustasMasiulis):一个非常简洁的C++天堂之门实现代码。
(MalwareTech):详细解释了WOW64的分段机制。
WOW64系统的内部工作原理
为了了解WOW64系统的内部运行机制,我们需要考察来自32位用户模式的调用序列,是如何通过系统DLL映射到内核空间的对应代码的。在这些系统DLL中,操作系统将检查相关的参数,并最终将其传递给名为syscall stub的存根函数。这个syscall stub负责将用户空间的调用转换为内核中的API调用。在64位系统中,syscall stub的实现方法非常简单,因为它会直接执行syscall指令,具体如图1所示:
图1 WOW64系统的Syscall Stub
图2显示了运行在WOW64上的32位进程的syscall stub:
图2 本地x64 syscall stub
注意,在WOW64版本中,调用的是Wow64SystemServiceCall函数,而不是syscall指令。在WOW64系统中,由于通常情况下是会进入内核空间的,所以改为调用一个用户模式例程。我们可以在图3中看到,在这个Wow64SystemServiceCall函数中,会立即通过一个名为Wow64Transition的指针进行间接跳转:
图3:Wow64SystemService通过指针Wow64Transition进行跳转
需要注意的是,Wow64SystemServiceCall函数是在标为ntdll_77550000的ntdll模块中找到的;在这里,一个WOW64进程加载了两个ntdll模块,一个是32位的,一个是64位的。在WinDbg中,32位模块后面会带有其地址,以示区分。通常情况下,64位的ntdll模块位于%WINDIR%\System32目录中,而32位的模块则位于%WINDIR%\SysWOW64目录中。在PDB中,64位和32位的ntdll模块分别称为ntdll.pdb和wntdll.pdb,读者不妨尝试用反汇编器加载它们! 继续进行调用跟踪,如果我们查看Wow64Transition指针保存的内容,会发现其目标是wow64cpu!KiFastSystemCall。顺便说一下,请注意wow64cpu!KiFastSystemCall的地址是通过成员WOW32Reserved保存在32位TEB(线程环境块)中的,虽然它与这里的跟踪操作无关,但了解这一点还是很有用的。在图4中,我们看到的是KiFastSystemCall的主体代码:
图4 KiFastSystemCall通过段选择器0x33切换到x64模式。
KiFastSystemCall使用0x33段选择器跳转到指令后的内存位置处。这个0x33段通过GDT条目将CPU切换到64位模式,具体如(MalwareTech)所述。
让我们回顾一下对于调用的跟踪情况。我们首先从ntdll中的一个NtResumeThread调用开始出发。这个函数将调用Wow64SystemServiceCall函数,而后者会执行Wow64Transition函数。最后,由KiFastSystemCall函数完成从32位运行模式到64位运行模式的过渡,具体流程如图5所示:
图5 从32位到64位的过渡过程
用于实现CPU模式切换的跳转指令的目标是图6所示的64位代码:
图6 KiFastSystemCall的目标代码
图6显示了到目前为止,我们在这个调用跟踪中看到执行的第一条64位指令。为了理解它,我们需要看看WOW64系统是如何初始化自己的。关于这方面的详细解释,请参考(wbenny)。现在,我们可以看看wow64cpu!RunSimulatedCode中的重要部分:
图7 保存在RunSimulatedCode中的64位寄存器
图7描述了对64位TEB的检索,它用于访问槽位索引(slot index)1处的Thread Local Storage。然后,将一个函数指针表移入寄存器r15中。实际上,检索到的TLS数据是一个未公开的数据结构WOW64_CPURESERVED,它包含了WOW64层用来设置和恢复32位和64位边界的寄存器数据和CPU状态信息。在这个结构体中,还有一个WOW64_CONTEXT结构体,微软网站上提供了部分文档。我在本篇文章的最后给出了这两个结构体。我们将在后面介绍如何使用这个上下文结构,但是为了理解前面的jmp指令,我们只需要知道r15存放的是一个函数指针表的地址即可。
现在,我们还需要留意WOW64层的架构。从64位内核的角度来看,32位(Wow64)用户模式应用程序的运行过程本质上就是一个大的while循环。这个循环在处理器的32位执行模式下执行x86指令,并偶尔退出循环来运行系统调用。因为内核是64位的,所以这时处理器模式会暂时切换到64位,以提供系统调用服务,然后,处理器将切换回原来的模式,从暂停的地方继续循环。也就是说,WOW64层的作用就像一个模拟器,但是其中的指令都是在物理CPU上执行的。
回到图6中我们看到的jmp指令,现在我们知道发生了什么。指令jmp [r15 + 0xF8] 相当于C代码jmp TurboThunkDispatch[0xF8/sizeof(uint64_t)]。观察这个索引处的函数指针,我们可以找到函数wow64cpu!CpupReturnFromSimulatedCode(图8)。
图8 TurboThunk表的最后一个函数指针条目是一个exit例程这个例程负责将32位寄存器的状态保存到我们之前提到的WOW64_CONTEXT结构体中,并获取syscall的参数。这里有一些棘手的问题,所以让我们来详细研究一下。首先,通过xchg将堆栈指针移到r14中,这个位置的值将是调用Wow64SystemServiceCall的syscall stub的返回地址。然后,堆栈指针r14递增4,以获得一个指针,该指针指向在恢复所有这些上下文值时堆栈应该重置的位置。然后,这两个值分别保存到上下文的EIP和ESP变量中。然后,让r14堆栈指针再递增一次,以得到__stdcall参数的位置(记住stdcall是通过堆栈传递所有参数的)。这个参数数组对以后很重要,所以要记住它。之后,将参数指针移到r11中,所以在C语言中,这意味着r11相当于一个堆栈槽的数组,每个槽是一个参数uint32_t r11[argCount]。最后,保存其余的寄存器和EFlags。
一旦保存了32位上下文,WOW64层就会通过获取syscall编号的高16位来计算要调用的TurboThunk,并派发到该thunk。需要注意的是,在这个数组的开头是图9所示的函数TurboDispatchJumpAddressEnd,它是为不支持TurboThunks的函数而调用的。
图9 TurboThunk表的第一个函数指针条目是一个entry例程
关于TurboThunks,建议读者仔细阅读wbenny撰写的相关文章。为了照顾没有读过这篇文章的读者,我们总结一下这篇文章的大意:对于那些参数宽度<= sizeof(uint32_t)的简单函数,WOW64层会直接通过零扩展或带符号的扩展将这些参数拓宽到64位,然后直接通过系统调用进入内核模式。这一切都发生在wow64cpu中,而不是执行如下所述的更复杂的路径。这就起到了优化的作用。对于不支持TurboThunks的复杂函数,可以使用TurboDispatchJumpAddressEnd存根函数,以派遣到wow64!SystemServiceEx执行系统调用,具体如图10所示。
图10 复杂的系统调用要经过Wow64SystemServiceEx例程
稍后,我们将介绍这个例程,因为它是本文的重点内容,但现在让我们跟踪调用流程。当Wow64SystemServiceEx运行系统调用并返回后,eax寄存器中的返回值就会被移动到WOW64_CONTEXT结构体中,并恢复32位寄存器的状态。为此,有两种路径,一种是常见的情况,一种是似乎只存在于NtContinue和其他WOW64内部的情况。至于选择哪种恢复路径,要视从TLS槽中检索到的WOW64_CPURESERVED结构体开头的一个标志而定,具体如图11所示:
图11 系统调用完成后恢复CPU状态,这时,有一个简单的路径和一个复杂的路径来处理XMM寄存器
更简单的情况是创建一个jmp,在恢复WOW64_CONTEXT中所有已保存的寄存器后,将使用段选择器0x23转换为32位模式。在较为复杂的情况下,还会额外恢复一些段、xmm值以及WOW64_CONTEXT结构中保存的寄存器,然后通过iret转换为32位模式。在正常情况下,构建的jmp如图12所示:
图12 通过动态构建的jmp切换到32位模式
至此,我们的调用跟踪已完成。WOW64层已切换回32位模式,并将在我们开始使用的syscall stub中的Wow64SystemServiceCall之后的ret处继续执行。在了解了WOW64层本身的执行流程后,接下来,让我们开始考察之前提到的Wow64SystemServiceEx调用。
图13展示了Wow64SystemServiceEx例程中的一些逻辑,我们稍后将用到它们:
图13 分派系统调用之前和之后调用的日志记录例程
该例程首先会查询服务表,该服务表保存有指向例程的指针,这些例程将传递的参数数组转换为常规64位系统模块期望的64位类型。这个参数数组正是存放在r14中的栈槽(stack slot)。
这里有两个对LogService函数的调用,但是只有在动态库%WINDIR%\system32\wow64log.dll已被加载且导出了Wow64LogInitialize、Wow64LogSystemService、Wow64LogMessageArgList和Wow64LogTerminate函数的情况下,这些调用才能正常生效。不过,在默认情况下,Windows系统中并没有包含这个动态库;要将这个动态库放入上述目录,需要具有管理员权限。
下一节将详细介绍如何使用该日志记录DLL来hook通过这个wow64layer进行模式切换的系统调用。因为在服务系统调用前后都会调用日志记录例程LogService,所以,我们可以实现一种标准的内联hook风格的回调函数,来检查其参数和返回值。
小结
在本文中,我们为读者详细介绍了WOW64子系统的内部运行机制,在下一篇文中,我们将继续为读者介绍WOW64子系统的Hooking技术。
译文声明
译文仅供参考,具体内容表达以及含义原文为准。
戳“阅读原文”查看更多内容