一.事由
二.問題
三.追蹤溯源
四.解決問題
五.完
**********************************************************************************************
一. 事由
最近有個需求是需要在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