一.事由
二.问题
三.追踪溯源
四.解决问题
五.完
**********************************************************************************************
一. 事由
最近有个需求是需要在32bit进程的某个线程A在调用createthread创建线程B的时候,如果线程B的起始地址符合指定的值则需要把该进程dump出来,由于指定的系统环境是windows 64位,不能HOOK,所以采用了PsSetCreateThreadNotifyRoutine的方式,在CreateThreadNotify得到调用时,判断创建的线程起始地址,如果是,则通知我们的dump进程进行dump采集,不过发现在CreateThreadNotify函数做等待时,会和dump进程的采集函数造成deadlock,最后无奈采用插user APC(apc机制类似一个回调函数)的方式,因为user apc会在内核回到应用层时第一时间得到执行,所以如果在user apc中触发dump进程进行采集,也是符合需求
二. 问题
方案选定后,就开始写代码,在刚出demo的时候,为了方便快速测试,就直接在插user apc的时候,使用1为 user apc 的入口地址,运行结果也很好,系统在调用这个user apc的时候,由于入口地址是1,进程崩溃,自动触发了我们的dump进程,同时也成功采集了现场dump,栈如下:
0249eb40 75360816 ntdll!NtWaitForSingleObject+0x15
0249ebac 76b91184 KERNELBASE!WaitForSingleObjectEx+0x98
0249ebc4 76b91138 kernel32!WaitForSingleObjectExImplementation+0x75
0249ebd8 0039b951 kernel32!WaitForSingleObject+0x12
0249f2ec 76bb9d57 bavsvc!BugReportHelper::Handler+0xa61 [e:\xxxx\public\bugreporter\bugreporthelper\bugreporterhelper.cpp @ 717]
0249f374 772f0727 kernel32!UnhandledExceptionFilter+0x127
0249f37c 772f0604 ntdll!__RtlUserThreadStart+0x62
0249f390 772f04a9 ntdll!_EH4_CallFilterFunc+0x12
0249f3b8 772d87b9 ntdll!_except_handler4+0x8e
0249f3dc 772d878b ntdll!ExecuteHandler2+0x26
0249f48c 7729010f ntdll!ExecuteHandler+0x24
0249f48c 00000000 ntdll!KiUserExceptionDispatcher+0xf
WARNING: Frame IP not in any known module. Following frames may be wrong.
0249f7d8 7729004d 0x0
0249fc94 76b91ec8 ntdll!KiUserApcDispatcher+0x25
0249fcbc 00391b68 kernel32!CreateThreadStub+0x20
0249fd14 0039cd72 bavsvc!_crashRaiserThread+0xe8 [e:\xxxx\public\bugreporter\bugreporttest\bugreporttest.cpp @ 216]
0249fd4c 0039ce1a bavsvc!_callthreadstartex+0x1b [f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c @ 348]
0249fd58 76b93677 bavsvc!_threadstartex+0x82 [f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c @ 326]
0249fd64 772b9d72 kernel32!BaseThreadInitThunk+0xe
0249fda4 772b9d45 ntdll!__RtlUserThreadStart+0x70
0249fdbc 00000000 ntdll!_RtlUserThreadStart+0x1b
看结果,方案可行,但为了栈回溯的结果更直观和方便统计,我们决定在插apc时,不使用地址1作为入口地址,而是使用我们程序中的一个模块中的一个函数作为地址,这里不妨假设这个函数名为KissError,地址为0x54321,这样做的另一个好处是可以在这个KissError函数中执行额外的功能。但结果却事与愿违,在我们采用了这个方式后,一毛钱的crash都没抓到,Nothing!系统也没弹出相关的error窗口,这是为什么呢?下回分解
三. 追踪溯源
作为一名程序员,特别是作为一名有强迫症的程序员(和华健一起调的,不知道他是不是这样:),碰到问题,如果不把问题分析个一清二白,连觉都睡不安稳,所以接下来就是一步一步的调试分析问题了。
首先科普点基础知道:
我们知道在wow64进程中,每个线程都有2个用户栈,暂且称为stack32和stack64,并且对于wow64进程,系统会有一个wow64的转换层,实际起着“欺上瞒下”的作用,处在我们的32进程和系统内核之前,每次我们32进程在进入内核的时候都会穿过它,这个转换层,这个转换层进入内核前会把栈切成stack64栈。
另外我们知道在目前我们的情况下,apc是内核回返回应用层时第一时间得到执行的应用层代码,原理是内核在返回应用层时会判断当前线程是否有pending的apc,如果有,则把返回应用层后执行的第一条指令设置为ntdll64!KiUserApcDispatcher函数,这个函数就会调用我们插入的user apc。
有了前面的基础知道,现在就开始分析ntdll64!KiUserApcDispatcher这个在调用我们的user apc的时候发生了什么事,下面是其汇编代码:
kd> uf ntdll!KiUserApcDispatcher
ntdll!KiUserApcDispatch:
00000000`770efcd0 488b4c2418 mov rcx,qword ptr [rsp+18h]
00000000`770efcd5 488bc1 mov rax,rcx //此处rcx为apc的入口地址
00000000`770efcd8 4c8bcc mov r9,rsp
00000000`770efcdb 48c1f902 sar rcx,2
00000000`770efcdf 488b542408 mov rdx,qword ptr [rsp+8]
00000000`770efce4 48f7d9 neg rcx
00000000`770efce7 4c8b442410 mov r8,qword ptr [rsp+10h]
00000000`770efcec 480fa4c920 shld rcx,rcx,20h
00000000`770efcf1 85c9 test ecx,ecx
00000000`770efcf3 7422 je ntdll!KiUserApcDispatch+0x44 (00000000`770efd17)
ntdll!KiUserApcDispatch+0x25:
00000000`770efcf5 488b0c24 mov rcx,qword ptr [rsp]
00000000`770efcf9 ffd0 call rax
ntdll!KiUserApcDispatch+0x2b:
00000000`770efcfb 488bcc mov rcx,rsp
00000000`770efcfe b201 mov dl,1
00000000`770efd00 e8db050000 call ntdll!NtContinue (00000000`770f02e0)
00000000`770efd05 85c0 test eax,eax
00000000`770efd07 74c7 je ntdll!KiUserApcDispatch (00000000`770efcd0)
ntdll!KiUserApcDispatch+0x39:
00000000`770efd09 8bf0 mov esi,eax
ntdll!KiUserApcDispatch+0x3b:
00000000`770efd0b 8bce mov ecx,esi
00000000`770efd0d e83e060800 call ntdll!RtlRaiseStatus (00000000`77170350)
ntdll!KiUserApcDispatch+0x3e:
00000000`770efd0e 3e ???
00000000`770efd0f 06 ???
00000000`770efd10 0800 or byte ptr [rax],al
00000000`770efd12 90 nop
00000000`770efd13 eb00 jmp ntdll!KiUserApcDispatch+0x42 (00000000`770efd15)
ntdll!KiUserApcDispatch+0x42:
00000000`770efd15 ebf7 jmp ntdll!KiUserApcDispatch+0x3e (00000000`770efd0e)
ntdll!KiUserApcDispatch+0x44:
00000000`770efd17 8b0424 mov eax,dword ptr [rsp]
00000000`770efd1a 480bc8 or rcx,rax
00000000`770efd1d 488b0554bb0e00 mov rax,qword ptr [ntdll!Wow64ApcRoutine (00000000`771db878)]
00000000`770efd24 4885c0 test rax,rax
00000000`770efd27 74d2 je ntdll!KiUserApcDispatch+0x2b (00000000`770efcfb)
ntdll!KiUserApcDispatch+0x56:
00000000`770efd29 ffd0 call rax
00000000`770efd2b be0d0000c0 mov esi,0C000000Dh
00000000`770efd30 ebd9 jmp ntdll!KiUserApcDispatch+0x3b (00000000`770efd0b)
从上面的汇编代码可以看出,在获取到apc入口地址后,在第一块黄色的代码块中,会对ecx进行一系列的运算,最后判断运算出来的结果的低32位是否为0,若为0 ,则跳到地址00000000`770efd17处,而在实际运行中,刚好我们使用入口地址1和入口KissError函数也就是地址为0x54321却因为这个运算而跳转到不同的分支运行。
- 入口地址为1的时候,则会算出结果为0,最后跳转到00000000`770efd17执行,而0x54321的情况会执行到00000000`770efcf9的call rax指令,这2个分支的不同是,前者借助ntdll!Wow64ApcRoutine函数模拟成32位环境来执行32位的APC,就像以前的APC执行结果一样,而且入口地址因为运算的原因,会导致算出的入口地址为0,导致在32位环境下(代码段选择子是23)call 0,最后崩溃,于是出现了在(二)中描述的栈。
- 入口为0x54321的时候,会直接在64环境下执行00000000`770efcf9的call rax,指令,rax的值0x54321,此时的段选择子值保持着wow64环境下的33。
对于分支2中的情况,KissError这个函数中有一条指令如下:
0033:00000000`0046ba24 ff257c335600 jmp qword ptr [BAVSvc+0x16337c (00000000`0056337c)] ds:002b:00000000`009ceda6=????????????????
ds:002b:00000000`009ceda6=???????????????? 这块的显示是刚好EIP指向它时,windbg自动显示的,这条指令咋一看没什么奇怪,但细想是有问题的,在32 bit情况下应该是取00000000`0056337c的值来做跳转目标的,但windbg竟然显示的是00000000`009ceda6,凭经验,发现00000000`009ceda6这值刚好等于00000000`0046ba24+00000000`0056337c+6,这公式很眼熟,就是在做相对跳转时的取值,这样看来,就知道为什么windbg为什么会显示成00000000`009ceda6了,因为这时候这条opcode为ff25的jmp指令后面的操作数作为相对跳转目标而不是绝对跳转目标的绝对值了,因此导致了读取了不可读取的内存地址(00000000`009ceda6),最终触发了异常。
OK,分析到这里,总结了下问题,还有2个:
- cpu是怎么区分opcode为ff25的JMP是相对jmp还是绝对jump
- 触发异常后,为什么这条线程毫无征兆的没了,但进程没崩溃,同时也没被我们的dump进程抓到crash?
对于问题1,总的来说是cs段选择子不同的原因,在支持64位的cpu下,code segment 描述符中有一个标志为L,当此标志置位是,则为长模式,反之为兼容模式,对应这里的情况是cs=33时为前者,cs=23时为后者,所以cpu在运行过程取指令时能实时的按不同的模式执行指令。
对于问题2,这里还要先科普一点基础,有点像我们之前说过的apc,由于异常触发时,是由内核回调到应用层的,所以在内核回到应用层第一现场时是先触发ntdll64! KiUserExceptionDispatcher,然后ntdll64! KiUserExceptionDispatcher查看异常是32bit空间触发的不是wow64下触发的,若为前者,则会通过一些结构变换,然后调用wow64!Ntdll32KiUserExceptionDispatcher进而转到32bit空间的ntdll32! KiUserExceptionDispatcher来进行异常分发,或为后者则走wow64本身的she分发过程。
现在回头看下问题2中的情况,cpu是在执行0033:00000000`0046ba24 ff257c335600 jmp qword ptr [BAVSvc+0x16337c (00000000`0056337c)] ds:002b:00000000`009ceda6=????????????????
这条指令时发生异常,此时系统调用ntdll64! KiUserExceptionDispatcher来dispatch分发异常,但ntdll64! KiUserExceptionDispatcher在分发的时候,识别出发生的异常并不是32bit空间触发的,于是只会在wow64环境下查找异常处理,
最后调用了一个如下的SHE filter处理掉了
kd> uf 0033:00000000`7478ea02
wow64!Wow64pLongJmp+0x682:
00000000`7478ea02 4055 push rbp
00000000`7478ea04 4883ec20 sub rsp,20h
00000000`7478ea08 488bea mov rbp,rdx
00000000`7478ea0b 48894d60 mov qword ptr [rbp+60h],rcx
00000000`7478ea0f 48894d28 mov qword ptr [rbp+28h],rcx
00000000`7478ea13 488b4528 mov rax,qword ptr [rbp+28h]
00000000`7478ea17 488b08 mov rcx,qword ptr [rax]
00000000`7478ea1a 448b01 mov r8d,dword ptr [rcx]
00000000`7478ea1d 488d159c62fdff lea rdx,[wow64!`string' (00000000`74764cc0)]
00000000`7478ea24 b902000000 mov ecx,2
00000000`7478ea29 e8cea2fdff call wow64!Wow64LogPrint (00000000`74768cfc)
00000000`7478ea2e 4c8b5d28 mov r11,qword ptr [rbp+28h]
00000000`7478ea32 498b03 mov rax,qword ptr [r11]
00000000`7478ea35 813803000080 cmp dword ptr [rax],80000003h //STATUS_BREAKPOINT
00000000`7478ea3b 7410 je wow64!Wow64pLongJmp+0x6cd (00000000`7478ea4d)
wow64!Wow64pLongJmp+0x6bd:
00000000`7478ea3d 8138080000c0 cmp dword ptr [rax],0C0000008h//STATUS_INVALID_HANDLE
00000000`7478ea43 7408 je wow64!Wow64pLongJmp+0x6cd (00000000`7478ea4d)
wow64!Wow64pLongJmp+0x6c5:
00000000`7478ea45 8138350200c0 cmp dword ptr [rax],0C0000235h//STATUS_HANDLE_NOT_CLOSABLE
00000000`7478ea4b 7509 jne wow64!Wow64pLongJmp+0x6d6 (00000000`7478ea56)
wow64!Wow64pLongJmp+0x6cd:
00000000`7478ea4d 488b4d28 mov rcx,qword ptr [rbp+28h]
00000000`7478ea51 e84adefdff call wow64!Pass64bitExceptionTo32Bit (00000000`7476c8a0)//没进入这里,所以不会调用raymond说的Wow64SetupExceptionDispatch
wow64!Wow64pLongJmp+0x6d6:
00000000`7478ea56 b801000000 mov eax,1//永远返回1,1就是那个Exception Handler
00000000`7478ea5b 4883c420 add rsp,20h
00000000`7478ea5f 5d pop rbp
00000000`7478ea60 c3 ret
在调用上面filter时,栈如下:
kd> k
Child-SP RetAddr Call Site
00000000`050ed3e0 00000000`7478e1e9 ntdll!_C_specific_handler+0x8a
00000000`050ed450 00000000`770d554d wow64!_GSHandlerCheck_SEH+0x75
00000000`050ed480 00000000`770b5d1c ntdll!RtlpExecuteHandlerForException+0xd
00000000`050ed4b0 00000000`770efe48 ntdll!RtlDispatchException+0x3cb
00000000`050edb90 00000000`0046ba24 ntdll!KiUserExceptionDispatch+0x2e
00000000`050ee150 00000000`0053706d BAVSvc!Sleep
00000000`050ee158 00000000`000f4240 BAVSvc!ExceptionRaiserApc+0xd [e:\xxxxx\kernelservice.cpp @ 335]
00000000`050ee160 00000000`051efec8 0xf4240
00000000`050ee168 00000000`770efcfb 0x51efec8
00000000`050ee170 00000000`770f093a ntdll!KiUserApcDispatch+0x2b
00000000`050ee668 00000000`747803fd ntdll!ZwCreateThreadEx+0xa
00000000`050ee670 00000000`7476cf87 wow64!whNtCreateThreadEx+0x815
00000000`050ee840 00000000`746f276d wow64!Wow64SystemServiceEx+0xd7
00000000`050ef100 00000000`7476d07e wow64cpu!ServiceNoTurbo+0x24
00000000`050ef1c0 00000000`7476c549 wow64!RunCpuSimulation+0xa
00000000`050ef210 00000000`7711d177 wow64!Wow64LdrpInitialize+0x429
00000000`050ef760 00000000`770d308e ntdll! ?? ::FNODOBFM::`string'+0x2bfe4
00000000`050ef7d0 00000000`00000000 ntdll!LdrInitializeThunk+0xe
上面的_C_specific_handler函数接着会调用ntdll!RtlUnwindEx函数回到32bit空间(很优雅的回,竟然没挂掉)
而在回32bit空间时,32bit空间的栈如下:
而x86空间的栈如下:
32.kd:x86> k
ChildEBP RetAddr
051efcf8 75363054 ntdll_77280000!ZwCreateThreadEx+0x12
051efec8 76b91ec8 KERNELBASE!CreateRemoteThreadEx+0x161
051efef0 00537043 kernel32!CreateThreadStub+0x20
051eff44 0046fae4 BAVSvc!_crashRaiserThread+0xb3 [e:\xxxx\kernelservice.cpp @ 359]
051eff7c 0046fb8c BAVSvc!_callthreadstartex+0x1b [f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c @ 348]
051eff88 76b93677 BAVSvc!_threadstartex+0x82 [f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c @ 326]
051eff94 772b9d72 kernel32!BaseThreadInitThunk+0xe
051effd4 772b9d45 ntdll_77280000!__RtlUserThreadStart+0x70
051effec 00000000 ntdll_77280000!_RtlUserThreadStart+0x1b
回32bit空间时, 是回到栈顶处,相当完整,因此应用层这个调用可以按其完整的生命周期安全的退出。所以就出现在问题2中所描述的“毫无征兆的没了,进程也没崩溃”的现象
至此,问题产生原因和现象都得到了很好的解答。
四. 解决问题
经过上面的分析,我们知道,user apc派发入口处,会对user apc的入口地址进行一定的运算,如下:
ntdll!KiUserApcDispatch:
00000000`770efcd0 488b4c2418 mov rcx,qword ptr [rsp+18h]
00000000`770efcd5 488bc1 mov rax,rcx //此处rcx为apc的入口地址
00000000`770efcd8 4c8bcc mov r9,rsp
00000000`770efcdb 48c1f902 sar rcx,2
00000000`770efcdf 488b542408 mov rdx,qword ptr [rsp+8]
00000000`770efce4 48f7d9 neg rcx
00000000`770efce7 4c8b442410 mov r8,qword ptr [rsp+10h]
00000000`770efcec 480fa4c920 shld rcx,rcx,20h
00000000`770efcf1 85c9 test ecx,ecx运算到这里,如果是32bit的user apc的话,rcx的低32位也就是ecx为0,是高32位为user apc的入口地址
所以要想使我们在内核插入的user apc能得到正解的执行,只需要在内核插入user apc之前先对入口地址进行逆运算再插入即可(ntdll!KiUserApcDispatch的入口的运算会还原)
对于这个逆运算,可以在此处得到验证:
wow64!whNtQueueApcThread:
00000000`7477af68 4883ec38 sub rsp,38h
00000000`7477af6c 8b5104 mov edx,dword ptr [rcx+4]
00000000`7477af6f 8b4108 mov eax,dword ptr [rcx+8]
00000000`7477af72 448b490c mov r9d,dword ptr [rcx+0Ch]
00000000`7477af76 448b5110 mov r10d,dword ptr [rcx+10h]
00000000`7477af7a 486309 movsxd rcx,dword ptr [rcx]
00000000`7477af7d 4c8bc0 mov r8,rax
00000000`7477af80 48f7da neg rdx
00000000`7477af83 48c1e202 shl rdx,2
00000000`7477af87 4c89542420 mov qword ptr [rsp+20h],r10
00000000`7477af8c ff156e6cfeff call qword ptr [wow64!_imp_NtQueueApcThread (00000000`74761c00)]
00000000`7477af92 90 nop
00000000`7477af93 4883c438 add rsp,38h
00000000`7477af97 c3 ret
可以发现wow64无法偷偷帮我们做了这个转换
五. 完
附:
在wow64的异常分发这块,我自己研究了一天没找到关键点,最后请教了下业界大牛张银奎得到了解答,下面链接是他给出的答案
http://advdbg.org/blogs/advdbg_system/articles/5884.aspx