一個APC引起的折騰


一.事由

二.問題

三.追蹤溯源

四.解決問題

五.完

 

**********************************************************************************************

一.           事由

最近有個需求是需要在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. 入口地址為1的時候,則會算出結果為0,最后跳轉到00000000`770efd17執行,而0x54321的情況會執行到00000000`770efcf9的call rax指令,這2個分支的不同是,前者借助ntdll!Wow64ApcRoutine函數模擬成32位環境來執行32位的APC,就像以前的APC執行結果一樣,而且入口地址因為運算的原因,會導致算出的入口地址為0,導致在32位環境下(代碼段選擇子是23)call 0,最后崩潰,於是出現了在(二)中描述的棧。
  2. 入口為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個:

  1. cpu是怎么區分opcode為ff25的JMP是相對jmp還是絕對jump
  2. 觸發異常后,為什么這條線程毫無征兆的沒了,但進程沒崩潰,同時也沒被我們的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM