某医院影像客户端卡死分析

1 故障现象

Windows里的his客户端,登录后,打开患者的影像照片,就很容易his卡死。只得将his强杀再开。卡死频率大约每3~6次就会出现1次。

2 初步分析

2.1 检查his安装正确否

通过beyondcompare做文件夹比较,可以认为his安装正确。

2.2 死循环还是卡死

卡死时用procexp查看该进程的cpu消耗以及各个线程的cpu消耗。发现都接近0。可以认为当时,这个进程里的每个线程都是陷入内核不返回,而不是某些线程处于死循环中。
遂对该进程创建dmp。

3 深入分析

3.1 进程dmp分析

3.1.1 看看0号线程在等什么

用windbg打开进程dmp,~*kv查看每个线程的调用栈。发现wow64字样,这个dmp应该是procexp64抓取的32位进程的dmp。windbg输入!wow64exts.sw进行切换。然后再~*kv
先看0号线程,它的调用栈是:

 # ChildEBP RetAddr      Args to Child              
00 00189424 75d52129     000303cc 0000000d 000000ff user32!NtUserMessageCall+0x15 (FPO: [7,0,0])
01 00189464 75d500d1     008e7e20 00000000 76592744 user32!SendMessageWorker+0x5f2 (FPO: [Non-Fpo])
02 001894ac 06960220     000303cc 06a34518 000000ff user32!GetWindowTextA+0x49 (FPO: [Non-Fpo])
WARNING: Stack unwind information not available. Following frames may be wrong.
03 001894d8 75d4947a     000303cc 069d04d0 000000b6 Report2DL!DllUnregisterServer+0x1027dc
04 001894f8 75d4d205     00000000 00000000 069601e8 user32!InternalEnumWindows+0x5a (FPO: [Non-Fpo])
05 00189518 069603dd     069601e8 069d04d0 00189534 user32!EnumWindows+0x16 (FPO: [Non-Fpo])
06 00189560 0684477b     001895cc 06844789 00189588 Report2DL!DllUnregisterServer+0x102999
07 00189588 06844868     0684739d 00000000 069c4f74 Report2DL+0x477b
08 001895d8 77179280     06840000 00000001 00000000 Report2DL+0x4868
09 001895f8 7717feb7     069c4f64 06840000 00000001 ntdll_77140000!LdrpCallInitRoutine+0x14
0a 001896ec 7717ea8e     00000000 773cc92d 00189890 ntdll_77140000!LdrpRunInitializeRoutines+0x26f (FPO: [Non-Fpo])
0b 00189860 771bd3ff     001898d0 00189890 002db376 ntdll_77140000!LdrpLoadDll+0x472 (FPO: [Non-Fpo])
0c 0018989c 75a82e6a     00000000 001898f0 001898d0 ntdll_77140000!LdrLoadDll+0xc7 (FPO: [Non-Fpo])
0d 001898e4 76429c17     00000000 00000000 00002008 KERNELBASE!LoadLibraryExW+0x233 (FPO: [Non-Fpo])
0e 00189900 76429b9a     00000000 0018997c 00002008 ole32!LoadLibraryWithLogging+0x16 (FPO: [Non-Fpo]) (CONV: stdcall) [d:\w7rtm\com\ole32\common\loadfree.cxx @ 157]
0f 00189924 76429a86     0018997c 00189948 0018994c ole32!CClassCache::CDllPathEntry::LoadDll+0xaf (FPO: [Non-Fpo]) (CONV: stdcall) [d:\w7rtm\com\ole32\com\objact\dllcache.cxx @ 1925]

0号线程正在GetWindowTextAGetWindowTextA说明文档是https://docs.microsoft.com/en-us/previous-versions/ms929480(v=msdn.10)。GetWindowTextA是向其它窗口询问窗口标题的文字,参数1既是窗口hwnd,此例中是303cc
欲获知hwnd=303cc的窗口归属哪个线程进程,想到的是sdbgext http://www.nynaeve.net/?p=7扩展的hwnd命令来查询。可惜此例dmp中无效,听说实时调试的时候它才有用。不得不用spy++找出hwnd=303cc其实就是改进程的另一个线程17c8。

3.1.2 关于GetWindowText

上网搜搜GetWindowText 卡死,很容易发现https://blog.csdn.net/shang_cm/article/details/88819472,GetWindowText不适合于进程内各线程间互相询问,因为若对方线程不答,发问线程就卡死。所以此例,这个程序写的不规范,理应改用InternalGetWindowText

3.1.3 看看应答线程为什么不应答

查看线程17c8的栈,为

21  Id: 1720.17c8 Suspend: 0 Teb: 7ef67000 Unfrozen
ChildEBP RetAddr  Args to Child              
057efc34 75a81629 000006f8 00000000 00000000 ntdll_77140000!ZwWaitForSingleObject+0x15 (FPO: [3,0,0])
057efca0 765e1194 000006f8 ffffffff 00000000 KERNELBASE!WaitForSingleObjectEx+0x98 (FPO: [Non-Fpo])
057efcb8 765e1148 000006f8 ffffffff 00000000 kernel32!WaitForSingleObjectExImplementation+0x75 (FPO: [Non-Fpo])
057efccc 5e2204f9 000006f8 ffffffff 051b0f60 kernel32!WaitForSingleObject+0x12 (FPO: [Non-Fpo])
057efcdc 5e220535 5e239f38 057efd04 5e2206b8 wdmaud!CWorker::~CWorker+0x43 (FPO: [0,0,4])
057efce8 5e2206b8 00000001 00000000 0517d200 wdmaud!CWorker::`scalar deleting destructor'+0xd (FPO: [Non-Fpo])
057efd04 5e21f607 04024978 0494565b 00000000 wdmaud!CWorkerGuard::DereferenceWorker+0x9e (FPO: [Non-Fpo])
057efd30 5e21db2d 04024938 057efd50 5e21dd77 wdmaud!CWaveHandle::~CWaveHandle+0x29 (FPO: [Non-Fpo])
057efd3c 5e21dd77 00000001 03fa3318 0517d218 wdmaud!CWaveHandle::`scalar deleting destructor'+0xd (FPO: [Non-Fpo])
057efd50 5e214b2f 04024938 057efd84 76594bbf wdmaud!CWxd::Close+0x2b (FPO: [Non-Fpo])
057efd5c 76594bbf 00000000 00000006 04024938 wdmaud!wodMessage+0x79 (FPO: [Non-Fpo])
*** WARNING: symbols timestamp is wrong 0x4a5bdf90 0x4a5bda4f for msacm32.drv
057efd84 5bf019e5 0517d218 00000000 0517d1c8 winmm!waveOutClose+0x68 (FPO: [Non-Fpo])
057efda0 5bf0146e 00362e98 0517d1c8 00370790 msacm32!mapWaveClose+0x28 (FPO: [Non-Fpo])
057efdb4 76594bbf 00000000 00000006 00362e98 msacm32!wodMessage+0x86 (FPO: [Non-Fpo])
057efddc 76594b57 0517d1c8 06199118 00000000 winmm!waveOutClose+0x68 (FPO: [Non-Fpo])
057efe10 7659a9d1 76592744 7659aa0b 00000000 winmm!soundClose+0x8a (FPO: [Non-Fpo])
057efe18 7659aa0b 00000000 00000000 76592744 winmm!WaveOutNotify+0x65 (FPO: [2,0,4])
057efe48 75d4630a 000303cc 000003bd 0517d1c8 winmm!mmWndProc+0x6f (FPO: [Non-Fpo])
057efe74 75d46d4a 76592744 000303cc 000003bd user32!InternalCallWinProc+0x23
057efeec 75d477d7 00000000 76592744 000303cc user32!UserCallWinProcCheckWow+0x109 (FPO: [Non-Fpo])
057eff4c 75d47bda 76592744 00000001 057eff88 user32!DispatchMessageWorker+0x3b5 (FPO: [Non-Fpo])
057eff5c 76592876 057eff6c 00000000 000303cc user32!DispatchMessageA+0xf (FPO: [Non-Fpo])
057eff88 765e343d 000004fc 057effd4 77179812 winmm!mciwindow+0xf9 (FPO: [Non-Fpo])
057eff94 77179812 000004fc 725aae99 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
057effd4 771797e5 765927c1 000004fc 00000000 ntdll_77140000!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
057effec 00000000 765927c1 000004fc 00000000 ntdll_77140000!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])

大致认为该线程正在析构某某音频对象wdmaud!CWorker::~CWorker时候,需要等某个东西。不知为何,要一直等。
后尝试在设备管理器里禁用音频驱动器,果然故障不再。

3.2 内核层完全dmp分析

3.2.1 看看应答线程为什么不应答

用windbg打开内核dmp,输入!process 1720,看看各个线程的栈和线程等待的对象。
查看线程17c8的栈,

THREAD fffffa80cac248b0  Cid 1720.17c8  Teb: 000000007ef67000 Win32Thread: fffff900c1d55c10 WAIT: (UserRequest) UserMode Non-Alertable
            fffffa80caca9760  Thread
        Not impersonating
        DeviceMap                 fffff8a002050cb0
        Owning Process            fffffa80cabf7b00       Image:         Ó°ÏñÖÐÐÄä¯ÀÀÆ÷.exe
        Attached Process          N/A            Image:         N/A
        Wait Start TickCount      2877           Ticks: 94880 (0:00:24:42.500)
        Context Switch Count      163            IdealProcessor: 2                 LargeStack
        UserTime                  00:00:00.000
        KernelTime                00:00:00.015
        Win32 Start Address 0x00000000765927c1
        Stack Init fffff88014284c70 Current fffff880142847c0
        Base fffff88014285000 Limit fffff8801427c000 Call 0000000000000000
        Priority 14 BasePriority 10 PriorityDecrement 2 IoPriority 2 PagePriority 5
        Kernel stack not resident.
        Child-SP          RetAddr               Call Site
        fffff880`14284800 fffff800`01c84452     nt!KiSwapContext+0x7a
        fffff880`14284940 fffff800`01c87a73     nt!KiCommitThreadWait+0x1d2
        fffff880`142849d0 fffff800`01f52aee     nt!KeWaitForSingleObject+0x1a3
        fffff880`14284a70 fffff800`01cf3f53     nt!NtWaitForSingleObject+0xde
        fffff880`14284ae0 00000000`746e2e09     nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ fffff880`14284ae0)
        00000000`056ef0f8 00000000`00000000     0x746e2e09

发现17c8线程正在等待线程fffffa80caca9760,即等待其终止。
继续查看线程fffffa80caca9760,这个线程也属于同进程。

THREAD fffffa80caca9760  Cid 1720.1570  Teb: 000000007ef61000 Win32Thread: fffff900c1d6a010 WAIT: (UserRequest) UserMode Non-Alertable
            fffffa80cabf7570  SynchronizationEvent
        Not impersonating
        DeviceMap                 fffff8a002050cb0
        Owning Process            fffffa80cabf7b00       Image:         Ó°ÏñÖÐÐÄä¯ÀÀÆ÷.exe
        Attached Process          N/A            Image:         N/A
        Wait Start TickCount      2877           Ticks: 94880 (0:00:24:42.500)
        Context Switch Count      35             IdealProcessor: 3                 LargeStack
        UserTime                  00:00:00.000
        KernelTime                00:00:00.000
        Win32 Start Address 0x000000005e2180eb
        Stack Init fffff88014452c70 Current fffff880144527c0
        Base fffff88014453000 Limit fffff8801444c000 Call 0000000000000000
        Priority 15 BasePriority 15 PriorityDecrement 0 IoPriority 2 PagePriority 5
        Kernel stack not resident.
        Child-SP          RetAddr               Call Site
        fffff880`14452800 fffff800`01c84452     nt!KiSwapContext+0x7a
        fffff880`14452940 fffff800`01c87a73     nt!KiCommitThreadWait+0x1d2
        fffff880`144529d0 fffff800`01f52aee     nt!KeWaitForSingleObject+0x1a3
        fffff880`14452a70 fffff800`01cf3f53     nt!NtWaitForSingleObject+0xde
        fffff880`14452ae0 00000000`746e2e09     nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ fffff880`14452ae0)
        00000000`065ef0f8 00000000`00000000     0x746e2e09

这个线程正在等待异步事件fffffa80cabf7570。也就是说如果这个异步事件被置信号,该进程就不会卡在这。下一步需要找出这个异步事件的创建者以及本该是由谁来置信号这个异步事件。
另外,遍历该进程的所有线程,发现还有不少线程(10dc 1668 1608 1764 179c 1794 17fc 1664 1570 1b0c 19a4 1b7c)都在等这个异步事件。这个是异步事件不是通知事件,估计得置信号很多遍才能把每个线程都解除阻塞。

3.2.2 试图找出异步事件的创建者(失败)

方法参阅此文 https://blog.csdn.net/wzsy/article/details/50771450

2: kd> !object fffffa80cabf7570
Object: fffffa80cabf7570  Type: (fffffa80c7655350) Event
    ObjectHeader: fffffa80cabf7540 (new version)
    HandleCount: 1  PointerCount: 13
2: kd> dt nt!_OBJECT_HEADER fffffa80cabf7540
   +0x000 PointerCount     : 0n13
   +0x008 HandleCount      : 0n1
   +0x008 NextToFree       : 0x00000000`00000001 Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0xc ''
   +0x019 TraceFlags       : 0 ''
   +0x01a InfoMask         : 0x8 ''
   +0x01b Flags            : 0 ''
   +0x020 ObjectCreateInfo : 0xfffffa80`c9f35300 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : 0xfffffa80`c9f35300 Void
   +0x028 SecurityDescriptor : (null)
   +0x030 Body             : _QUAD

此异步对象的InfoMask=8,即代表OB_INFOMASK_QUOTA此结构体占0x10字节。对象头往前0x10字节,即可查看该对象。

InfoMask=8里没有OB_INFOMASK_PROCESS_INFO,所以也不知道这个异步对象是谁创建的。

2: kd> dt _OBJECT_HEADER_QUOTA_INFO fffffa80cabf7530
nt!_OBJECT_HEADER_QUOTA_INFO
   +0x000 PagedPoolCharge  : 0
   +0x004 NonPagedPoolCharge : 0
   +0x008 SecurityDescriptorCharge : 0x3063014d
   +0x010 SecurityDescriptorQuotaBlock : 0x00000000`0000000d Void
   +0x018 Reserved         : 1

4 修复

4.1 修改Report2DL.dll,直接换为InternalGetWindowText(失败)

查看Report2DL.dll的汇编,能找到call XXXX这句,call的地址就是GetWindowTextA的地址。如果GetWindowTextA和InternalGetWindowText的形式参数和返回值相同,那么就可以直接修改call的地址。可惜此例中两个函数的形参并不同。

4.2 用CreateRemoteThread和ReplaceIAT法,在进程加载动态库时去替换InternalGetWindowsText(失败)

参阅《Windows核心编程》的22.9节。此法对于动态LoadLibrary并且此后再调用dll的函数的情况才适用。可惜此例是在LoadLibraryEx的栈内立刻调用dll的函数,因此也不适用。

4.3


版权声明:本文为u010607621原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。