一、背景
1.讲故事前段时间有位朋友在微信上找到我,说他的程序偶发性崩溃,让我帮忙看下怎么回事,上面给的压力比较大,对于这种偶发性崩溃,比较好的办法就是利用 AEDebug 在程序崩溃的时候自动抽一管血出来,看看崩溃点是什么,其实我的系列文章中,关于崩溃类的dump比较少,刚好补一篇上来,话不多说,上 windbg 。
二、WinDbg 分析
1. 崩溃点在哪里在 windbg 中有一个 !analyze -v 命令可以自动化分析,输出信息如下:
0 : 120 > ! analyze - v ******************************************************************************* * * * Exception Analysis * * * ******************************************************************************* CONTEXT : ( .ecxr ) rax = 00000000032 fed38 rbx = 00000000 c0000374 rcx = 0000000000000000 rdx = 0000000000000020 rsi = 0000000000000001 rdi = 00007 ffbada727f0 rip = 00007 ffbada0a8f9 rsp = 000000003103 c8b0 rbp = 0000000000 c40000 r8 = 00007 ffb779bdab7 r9 = 00007 ffb782e94c0 r10 = 0000000000002000 r11 = 000000002 c4aa498 r12 = 0000000000000000 r13 = 000000003103 eb60 r14 = 0000000000000000 r15 = 000000002 c873720 iopl = 0 nv up ei pl nz na pe nc cs = 0033 ss = 002 b ds = 002 b es = 002 b fs = 0053 gs = 002 b efl = 00000202 ntdll ! RtlReportFatalFailure + 0x9 : 00007 ffb`ada0a8f9 eb00 jmp ntdll ! RtlReportFatalFailure + 0xb ( 00007 ffb`ada0a8fb ) Resetting default scope EXCEPTION_RECORD : ( .exr - 1 ) ExceptionAddress : 00007 ffbada0a8f9 ( ntdll ! RtlReportFatalFailure + 0x0000000000000009 ) ExceptionCode : c0000374 ExceptionFlags : 00000001 NumberParameters : 1 Parameter [ 0 ] : 00007 ffbada727f0 ...
从卦中的 ExceptionCode: c0000374 异常码来看,表示当前 nt堆损坏,这就尴尬了,一个C#程序咋会把 windows nt 堆给弄坏了,可能是引入了第三方的 C++ 代码。
由于异常分异常前和异常后,所以需要用 .ecxr 将当前线程切到异常前的崩溃点,然后使用 k 观察当前的线程栈。
0 : 120 > .ecxr ; k rax = 00000000032 fed38 rbx = 00000000 c0000374 rcx = 0000000000000000 rdx = 0000000000000020 rsi = 0000000000000001 rdi = 00007 ffbada727f0 rip = 00007 ffbada0a8f9 rsp = 000000003103 c8b0 rbp = 0000000000 c40000 r8 = 00007 ffb779bdab7 r9 = 00007 ffb782e94c0 r10 = 0000000000002000 r11 = 000000002 c4aa498 r12 = 0000000000000000 r13 = 000000003103 eb60 r14 = 0000000000000000 r15 = 000000002 c873720 iopl = 0 nv up ei pl nz na pe nc cs = 0033 ss = 002 b ds = 002 b es = 002 b fs = 0053 gs = 002 b efl = 00000202 ntdll ! RtlReportFatalFailure + 0x9 : 00007 ffb`ada0a8f9 eb00 jmp ntdll ! RtlReportFatalFailure + 0xb ( 00007 ffb`ada0a8fb ) *** Stack trace for last set context - .thread / .cxr resets it # Child - SP RetAddr Call Site 00 00000000 `3103c8b0 00007 ffb`ada0a8c3 ntdll ! RtlReportFatalFailure + 0x9 01 00000000 `3103c900 00007 ffb`ada1314e ntdll ! RtlReportCriticalFailure + 0x97 02 00000000 `3103c9f0 00007 ffb`ada1345a ntdll ! RtlpHeapHandleError + 0x12 03 00000000 `3103ca20 00007 ffb`ad9aef41 ntdll ! RtlpHpHeapHandleError + 0x7a 04 00000000 `3103ca50 00007 ffb`ad9be520 ntdll ! RtlpLogHeapFailure + 0x45 05 00000000 `3103ca80 00007 ffb`aa3882bf ntdll ! RtlFreeHeap + 0x966e0 06 00000000 `3103cb20 00007 ffb`66fac78f KERNELBASE ! LocalFree + 0x2f 07 00000000 `3103cb60 00007 ffb`66f273a4 mscorlib_ni + 0x63c78f 08 00000000 `3103cc10 00007 ffb`185c4fde mscorlib_ni ! System .Runtime .InteropServices .Marshal .FreeHGlobal + 0x24 [ f : \dd\ndp\clr\src\BCL\system\runtime\interopservices\marshal .cs @ 1212 ] 09 00000000 `3103cc50 00007 ffb`185c4fa1 0x00007ffb `185c4fde 0 a 00000000 `3103cca0 00007 ffb`185edc82 0x00007ffb `185c4fa1 ...
从卦中的 KERNELBASE!LocalFree 方法可知,程序正在释放一个 堆块,在释放的过程中抛出了异常,那为什么会释放失败呢?原因就比较多了,比如:
原因1:Free 一个已 Free 的堆块
原因2:Free 了一个别人的堆块
那到底是哪一种情况呢?有经验的朋友应该知道,ntheap 默认开启了 损坏退出 机制,用 !heap -s 命令就能显示出这种损坏原因。
0 : 120 > ! heap - s ************************************************************************************************************************ NT HEAP STATS BELOW ************************************************************************************************************************ ************************************************************** * * * HEAP ERROR DETECTED * * * ************************************************************** Details : Heap address : 0000000000 c40000 Error address : 000000002 c873710 Error type : HEAP_FAILURE_BLOCK_NOT_BUSY Details : The caller performed an operation ( such as a free or a size check ) that is illegal on a free block. Follow - up : Check the error 's stack trace to find the culprit. Stack trace: Stack trace at 0x00007ffbada72848 00007ffbad9aef41: ntdll!RtlpLogHeapFailure+0x45 00007ffbad9be520: ntdll!RtlFreeHeap+0x966e0 00007ffbaa3882bf: KERNELBASE!LocalFree+0x2f 00007ffb66fac78f: mscorlib_ni+0x63c78f 00007ffb66f273a4: mscorlib_ni!System.Runtime.InteropServices.Marshal.FreeHGlobal+0x24 00007ffb185c4fde: +0x185c4fde LFH Key : 0x1d4fd2a71d8b8280 Termination on corruption : ENABLED Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast (k) (k) (k) (k) length blocks cont. heap ------------------------------------------------------------------------------------- 0000000000c40000 00000002 16756 13688 16364 220 140 5 2 0 LFH ...
从卦中可以清晰的看到错误类型:Error type: HEAP_FAILURE_BLOCK_NOT_BUSY ,这是经典的 Double Free,也就是上面的 原因1 ,接下来我们就要寻找代码源头了。。。
2. 是谁的代码引发的从线程栈上看,底层的方法区都是十六进制,这表示当前是托管方法,这就好办了,我们用 !clrstack 看看托管代码是什么?
0 : 120 > ! clrstack OS Thread Id : 0x4d54 ( 120 ) Child SP IP Call Site 000000003103 cb88 00007 ffbad9b0544 [ InlinedCallFrame : 000000003103 cb88 ] Microsoft .Win32 .Win32Native .LocalFree ( IntPtr ) 000000003103 cb88 00007 ffb66fac78f [ InlinedCallFrame : 000000003103 cb88 ] Microsoft .Win32 .Win32Native .LocalFree ( IntPtr ) 000000003103 cb60 00007 ffb66fac78f DomainNeutralILStubClass .IL_STUB_PInvoke ( IntPtr ) 000000003103 cc10 00007 ffb66f273a4 System .Runtime .InteropServices .Marshal .FreeHGlobal ( IntPtr ) [ f : \dd\ndp\clr\src\BCL\system\runtime\interopservices\marshal .cs @ 1212 ] 000000003103 cc50 00007 ffb185c4fde xxxx .StructToBytes ( System .Object ) 000000003103 ced0 00007 ffb185ec6b1 xxx .SendDoseProject ( System .String ) ...
从卦中可以清晰的看到是托管方法 StructToBytes() 引发的,接下来导出这个方法的源码,截图如下:
从方法逻辑看,这位朋友用了 Marshal 做了互操作,为了能够进一步分析,需要找到 localResource 堆块句柄,使用 !clrstack -l 显示方法栈参数。
0 : 120 > ! clrstack - l OS Thread Id : 0x4d54 ( 120 ) ... 000000003103 cca0 00007 ffb185c4fa1 xxx .StructToBytes ( System .Object ) LOCALS : 0x000000003103cd0c = 0x000000000000018f 0x000000003103ccf8 = 0x0000000003084420 0x000000003103ccf0 = 0x0000000003084420 0x000000003103cce8 = 0x0000000000000000 0x000000003103cce0 = 0x0000000000000000 ...
经过对比,发现并没有显示 localResource 值,这就很尴尬了。。。一般在 dump 中 IntPtr 类型是显示不出来的,遇到好几次了,比较闹心。。。既然显示不出来堆块句柄值。。。那怎么办呢?天要绝人之路吗?
3. 绝处逢生既然托管层找不到堆块句柄,那就到非托管层去找,比如这里的 KERNELBASE!LocalFree+0x2f 函数,msdn 上的定义如下:
HLOCAL LocalFree ( [ in ] _Frees_ptr_opt_ HLOCAL hMem ) ;
那如何找到这个 hMem 值呢?在 x86 程序中可以直接用 kb 就能提取出来,但在 x64 下是无效的,因为它是用寄存器来传递方法参数,此时的寄存器值已经刷新到了 ntdll!NtWaitForMultipleObjects+0x14 上,比如下面的 rcx 肯定不是 hMem 值。
0 : 120 > r rax = 000000000000005 b rbx = 0000000000005 b08 rcx = 0000000000000002 rdx = 000000003103 b690 rsi = 0000000000000002 rdi = 0000000000000000 rip = 00007 ffbad9b0544 rsp = 000000003103 b658 rbp = 0000000000001 da4 r8 = 0000000000001000 r9 = 0101010101010101 r10 = 0000000000000000 r11 = 0000000000000246 r12 = 0000000000000000 r13 = 000000003103 c930 r14 = 0000000000001 f98 r15 = 0000000000000000 iopl = 0 nv up ei pl zr na po nc cs = 0033 ss = 002 b ds = 002 b es = 002 b fs = 0053 gs = 002 b efl = 00000246 ntdll ! NtWaitForMultipleObjects + 0x14 : 00007 ffb`ad9b0544 c3 ret
怎么办呢?其实还有一条路,就是观察 KERNELBASE!LocalFree+0x2f 方法的汇编代码,看看它有没有将 rcx 临时性的存到 线程栈 上。
0 : 120 > u KERNELBASE ! LocalFree KERNELBASE ! LocalFree : 00007 ffb`aa388290 48895 c2410 mov qword ptr [ rsp + 10 h ] , rbx 00007 ffb`aa388295 4889742418 mov qword ptr [ rsp + 18 h ] , rsi 00007 ffb`aa38829a 48894 c2408 mov qword ptr [ rsp + 8 ] , rcx 00007 ffb`aa38829f 57 push rdi 00007 ffb`aa3882a0 4883 ec30 sub rsp , 30 h 00007 ffb`aa3882a4 488 bd9 mov rbx , rcx 00007 ffb`aa3882a7 f6c308 test bl , 8 00007 ffb`aa3882aa 753 f jne KERNELBASE ! LocalFree + 0x5b ( 00007 ffb`aa3882eb )
很开心的看到,当前的 rcx 存到了 rsp+8 位置上,那如何拿到 rsp 呢?可以用 k 提取父函数 mscorlib_ni+0x63c78f 中的 Child-SP 值。
0 : 120 > k # Child - SP RetAddr Call Site ... 0 e 00000000 `3103ca80 00007 ffb`aa3882bf ntdll ! RtlFreeHeap + 0x966e0 0 f 00000000 `3103cb20 00007 ffb`66fac78f KERNELBASE ! LocalFree + 0x2f 10 00000000 `3103cb60 00007 ffb`66f273a4 mscorlib_ni + 0x63c78f ...
因为这个 Child-SP 是 call 之前的 sp, 汇编中的 sp 是 call 之后的,所以相差一个 retaddr 指针单元,所以计算方法是:ChildSp- 0x8 + 0x8 就是 堆块句柄。
0 : 120 > dp 00000000 `3103cb60 - 0x8 + 0x8 L1 00000000 `3103cb60 00000000 `2c873720
上面的 000000002c873720 就是堆块句柄,接下来用命令 !heap -x 000000002c873720 观察堆块情况。
0 : 120 > ! heap - x 000000002 c873720 Entry User Heap Segment Size PrevSize Unused Flags ------------------------------------------------------------------------------------------------------------- 000000002 c873710 000000002 c873720 0000000000 c40000 000000002 c8703c0 30 - 0 LFH ; free
果不其然,这个堆块已经是 Free 状态了,再 Free 必然会报错,经典的 Double Free 哈。
4. 回首再看源码仔细阅读源码,发现有两个问题。
没有对 localResource 加锁处理,在并发的时候容易出现问题。
localResource 是一个类级别变量,在多个方法中被使用。
将信息反馈给朋友之后,建议朋友加锁并降低 localResource 作用域。
三、总结
这次偶发的生产崩溃事故,主要原因是朋友的代码在逻辑上出了点问题,没有合理的保护好 localResource 句柄资源,反复释放导致的 ntheap 破坏。
这个 dump 虽然问题比较小白,但逆向分析找出原因,还是挺考验基本功的。
原文地址:https://mp.weixin.qq.com/s/Xd7yjZjF8tMHkypHfnVjFg
查看更多关于记一次 .NET 某医疗器械 程序崩溃分析的详细内容...