<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

文章分类

导航

订阅

WoW进程的异常分发过程

以WoW方式运行在64位Windows系统的32位进程(简称WoW进程)与普通的32位进程相比,很多行为都保持不变,但是也有个别时候会显露出差异,让人感觉困惑,比如发生异常时,可能就把一些本来藏而不漏的“

内丑”曝露出来了。
举例来说,当我的AcsVio小程序(熟悉《软件调试》的朋友都知道)在64位环境下运行,JIT机制启动WinDBG64后,本来“自动定位到异常”的功能就不工作了,执行k命令看到的是下面一幕:
0:000:x86> k
ChildEBP RetAddr 0018fa80 773c014d ntdll_773b0000!ZwRaiseException+0x12
0018fa90 c0000005 ntdll_773b0000!KiUserExceptionDispatcher+0x29
WARNING: Frame IP not in any known module. Following frames may be wrong.
0018ff88 763933aa 0xc0000005
0018ff94 773e9ef2 KERNEL32!BaseThreadInitThunk+0xe
0018ffd4 773e9ec5 ntdll_773b0000!__RtlUserThreadStart+0x70
0018ffec 00000000 ntdll_773b0000!_RtlUserThreadStart+0x1bv
有一次在武汉讲课,曾有同行对这个问题大倒苦水,而且讲出一大堆有关的故事来。本来想在《软件调试》的第二版中详细讨论WoW的问题。但最近又有网友在论坛中提出希望了解wow的异常分发细节,于是就先

写篇博客吧,也算给书中的内容打个草稿。
从哪里讲起呢? 就从NTDLL说起吧。要理解下面的内容,首先必须清楚的是,在WoW进程中,有两个NTDLL.DLL,一个是64位的,一个是32位的。正常情况下,WinDBG会把32位建立个别名叫ntdll32,也就是这个样

子:
00000000`771d0000 00000000`77379000 ntdll
00000000`773b0000 00000000`77530000 ntdll32
注意它们都在2GB地址空间中,这样是为了方便32位代码中的32位指针可以直接指向64位的NTDLL。
某些情况下,比如JIT调试,WinDBG的别名机制可能不工作,于是32位的ntdll的名字就会被附加上基地址以便区别,也就是:
771d0000 77379000 ntdll (deferred)
773b0000 77530000 ntdll_773b0000 (pdb symbols)
另一个背景是,对于WoW线程来说,每个线程都有两个栈,一个是32位代码使用的,另一个是64位的转接层代码所使用的。
32 bit, StackBase   : 0x190000
        StackLimit  : 0x18e000
        Deallocation: 0x90000

64 bit, StackBase   : 0x8fd20
        StackLimit  : 0x8c000
        Deallocation: 0x50000

有了以上基础后,我们开始讲异常分发过程,假设以WoW方式直接运行(不在调试器中)的AcsVio进程中访问违例,CPU飞进内核态,开始执行异常处理和分发的代码。主要的步骤如下:
1)内核函数KiDispatchException分发这个异常,先试图分发给调试器,但如果没有调试器,便会将这个异常的信息复制到用户态栈,然后将程序指针指向NTDLL64中的KiUserExceptionDispatcher。从这一步来

看,内核对WoW进程没有什么特殊处理,就是按照一般规则来做,准备回到64位NTDLL64中寻找用户态的异常处理器(try/catch)。
2)当CPU刚开始回到用户态执行时,虽然代码是64位的KiUserExceptionDispatcher,但使用的栈却是32位代码用的,因此,在这个函数一开始,就有一个专门针对WoW的特殊动作,伪代码如下:

VOID
KiUserExceptionDispatcher(
   __in PCONTEXT ContextRecord,
   __in PEXCEPTION_RECORD ExceptionRecord,
  )
{

 if (Wow64PrepareForException)
      Wow64PrepareForException(
         ExceptionRecord,
         ContextRecord
         );

Wow64PrepareForException函数并不长,主要调用了两个函数,第一个是memcpy,将内核态复制上来的信息从当前使用32位栈复制到64位栈,另一个是wow64!CpuResetToConsistentState。后者主要是将异常信

息机构复制到wow64cpu!RecoverException64变量中,将异常对应的上下文结果复制到wow64cpu!RecoverContext64,然后切换栈,转向使用64位代码的专用栈。


3)接下来,KiUserExceptionDispatcher开始执行通用的代码,也就是调用RtlDispatchException来寻找和执行异常处理器。奥秘其实也就在这个过程中。
执行!exchain观察当前线程中有效的异常处理器,可以看到:

0:000> !exchain
8 stack frames, scanning for handlers...
Frame 0x02: wow64cpu!CpupReturnFromSimulatedCode (00000000`7456271e)
  ehandler wow64cpu!CpupSimulateHandler (00000000`74562560)
Frame 0x03: wow64!RunCpuSimulation+0xa (00000000`745dd07e)
  ehandler wow64!_C_specific_handler (00000000`745fe24e)
Frame 0x04: wow64!Wow64LdrpInitialize+0x429 (00000000`745dc549)
  ehandler wow64!_GSHandlerCheck (00000000`745fe190)
Frame 0x05: ntdll!LdrpInitializeProcess+0x17e4 (00000000`77214956)
  ehandler ntdll!_GSHandlerCheck (00000000`771e9818)
Frame 0x06: ntdll! ?? ::FNODOBFM::`string'+0x29220 (00000000`77211a17)
  ehandler ntdll!_C_specific_handler (00000000`771e850c)

RtlDispatchException会依次询问这些处理器是否处理异常,如果有人回答处理(1),那么就执行它的异常处理器。

4)RunCpuSimulation函数的名为_C_specific_handler的异常处理器起着关键作用,在它得到执行机会时,它会调用Pass64bitExceptionTo32Bit函数将异常信息传递到32位代码使用的栈,并将32位上下文中的

程序指针设置为NTDLL32中的KiUserExceptionDispatcher。执行过程如下:

0:000> kn
 # Child-SP          RetAddr           Call Site
00 00000000`0008db90 00000000`745dc9a5 wow64!Wow64SetupExceptionDispatch+0x1b7
01 00000000`0008dd00 00000000`745fea2c wow64!Pass64bitExceptionTo32Bit+0x105
02 00000000`0008e210 00000000`771e85a8 wow64!Wow64pLongJmp+0x66c
03 00000000`0008e240 00000000`771f9d0d ntdll!_C_specific_handler+0x8c
04 00000000`0008e2b0 00000000`771e91af ntdll!RtlpExecuteHandlerForException+0xd
05 00000000`0008e2e0 00000000`77221278 ntdll!RtlDispatchException+0x45a
06 00000000`0008e9c0 00000000`7456271e ntdll!KiUserExceptionDispatcher+0x2e
07 00000000`0008f100 00000000`745dd07e wow64cpu!CpupReturnFromSimulatedCode
08 00000000`0008f1c0 00000000`745dc549 wow64!RunCpuSimulation+0xa
09 00000000`0008f210 00000000`7724e707 wow64!Wow64LdrpInitialize+0x429
0a 00000000`0008f760 00000000`771fc32e ntdll! ?? ::FNODOBFM::`string'+0x29364
0b 00000000`0008f7d0 00000000`00000000 ntdll!LdrInitializeThunk+0xe

其中的Wow64SetupExceptionDispatch函数的目的便是将32位上下文中的程序指针飞到全局变量wow64!Ntdll32KiUserExceptionDispatcher飞到32位NTDLL中的KiUserExceptionDispatcher。

00000000`745dc707 8b0d73e80200    mov     ecx,dword ptr [wow64!Ntdll32KiUserExceptionDispatcher (00000000`7460af80)] ds:00000000`7460af80=773c0124
00000000`745dc70d ff151d58ffff    call    qword ptr [wow64!_imp_CpuSetInstructionPointer (00000000`745d1f30)]

5)接下来做栈展开,准备执行_C_specific_handler的异常处理代码,也就是要回到RunCpuSimulation函数中执行,细节上便是执行RtlUnwindEx,后者再调用RtlRestoreContext

ntdll!RtlRestoreContext

0:000> k
Child-SP          RetAddr           Call Site
00000000`0008d6a8 00000000`771e897b ntdll!RtlRestoreContext
00000000`0008d6b0 00000000`771e57c4 ntdll!RtlUnwindEx+0x42d
00000000`0008dd50 00000000`771f9d0d ntdll!_C_specific_handler+0xcc
00000000`0008ddc0 00000000`771e91af ntdll!RtlpExecuteHandlerForException+0xd
00000000`0008ddf0 00000000`77221278 ntdll!RtlDispatchException+0x45a
00000000`0008e4d0 00000000`7456271e ntdll!KiUserExceptionDispatcher+0x2e
00000000`0008ec10 00000000`745dd07e wow64cpu!CpupReturnFromSimulatedCode
00000000`0008ecd0 00000000`745dc549 wow64!RunCpuSimulation+0xa
00000000`0008ed20 00000000`77214956 wow64!Wow64LdrpInitialize+0x429
00000000`0008f270 00000000`77211a17 ntdll!LdrpInitializeProcess+0x17e4
00000000`0008f760 00000000`771fc32e ntdll! ?? ::FNODOBFM::`string'+0x29220
00000000`0008f7d0 00000000`00000000 ntdll!LdrInitializeThunk+0xe

在RtlRestoreContext,会把当前线程的执行上下文设置成适合执行RunCpuSimulation的状态,也就是“飞”回以上栈回溯的 wow64!RunCpuSimulation+0xa这一栈帧。

RtlRestoreContext的最后两条指令是:
00000000`77220bcb 488b8980000000  mov     rcx,qword ptr [rcx+80h]
00000000`77220bd2 48cf            iretq

其中的iretq一旦执行,CPU便从栈顶取出接下来要执行的位置,观察栈顶:
0:000> dd 000000000008d660
00000000`0008d660  745dd080 00000000 0
其值果然位于RunCpuSimulation函数中:
wow64!RunCpuSimulation+0xc

6)在RunCpuSimulation中,调用CpuSimulate,准备去执行32位代码:
wow64!RunCpuSimulation:
00000000`745dd074 4883ec48        sub     rsp,48h
00000000`745dd078 ff155a4effff    call    qword ptr [wow64!_imp_CpuSimulate (00000000`745d1ed8)] ds:00000000`745d1ed8={wow64cpu!CpuSimulate (00000000`745625b0)}
00000000`745dd07e eb00            jmp     wow64!RunCpuSimulation+0xc (00000000`745dd080)
00000000`745dd080 ebf6            jmp     wow64!RunCpuSimulation+0x4 (00000000`745dd078)
00000000`745dd082 4883c448        add     rsp,48h
00000000`745dd086 c3              ret

在CpuSimulate中,将32位的线程上下文加载到物理寄存器,然后一个长跳转,跳转到32位世界:
0:000> p
wow64cpu!CpuSimulate+0x16b:
00000000`7456271b 41ff2e          jmp     fword ptr [r14] ds:00000000`0008ec80=0023773c0124
因为前面已经做好准备工作,所以这一跳,便跳到了ntdll32!KiUserExceptionDispatcher函数:
0:000> p
ntdll32!KiUserExceptionDispatcher:
773c0124 fc              cld
至此,64位的异常处理代码将异常顺利移交到32位代码,完成了交接工作,接下来32位的异常代码仍然按老的逻辑继续按32位的规则分发异常。

 

posted on 2013年7月14日 12:53 由 Raymond

# re: WoW进程的异常分发过程 @ 2013年7月14日 19:31

这是我在论坛刚发的帖子引出的吗?
看时间这么巧,有点灵异,不过下面呢??

kkindof

# re: WoW进程的异常分发过程 @ 2013年7月14日 22:40

没错,是你的问题触发这篇博客,虽然早就想整理这个内容了

Raymond

# re: WoW进程的异常分发过程 @ 2013年7月14日 23:15

太爽了。另外x64这块的异常分发不知道张老师写了papper没,我之前单步了下,貌似里面lookup出filter后,比较opcode,例如在里面有比较0xE9什么的,觉得蛮奇怪。

这是网上boxcounter写的SEH分析笔记(X64篇)
http://boxcounter.com/showthread.php?tid=74

另外,我触发这个问题的时候也很神奇,就是我插APC是在wow64环境下,然后在这里触发发异常,这时候Wow64PrepareForException有效,但栈应该还是是wow64的栈,最后我们发现触发这个异常的线程就那么没了

kkindof

Powered by Community Server Powered by CnForums.Net