当前位置: 首页 > 科技观察

记得一个医疗器械程序的.NET崩溃分析

时间:2023-03-20 11:45:16 科技观察

1.背景1.讲故事前段时间有个朋友在微信上找到我,说他的程序时不时崩溃,让我帮你搞清楚是怎么回事.压力比较大。对于这种偶发的崩溃,比较好的办法是用AEDebug在程序崩溃的时候自动抽一管血,看看崩溃点在什么地方。其实在我的系列文章中,crashclass的dump比较少,正好凑个文章,话不多说,上windbg吧。二、WinDbg分析1、崩溃点在哪里?windbg中有一个!analyze-v命令可以自动分析,输出信息如下:0:120>!analyze-v******************************************************************************************异常分析*****************************************************************************************CONTEXT:(.ecxr)rax=00000000032fed38rbx=00000000c0000374rcx=0000000000000000rdx=0000000000000020rsi=0000000000000001rdi=00007ffbada727f0rip=00007ffbada0a8f9rsp=000000003103c8b0rbp=0000000000c40000r8=00007ffb779bdab7r9=00007ffb782e94c0r10=0000000000002000r11=000000002c4aa498r12=0000000000000000r13=000000003103eb60r14=0000000000000000r15=000000002c873720iopl=0nvupeiplnznapenccs=0033ss=002bds=002bes=002bfs=0020gefl=005ntdll!RtlReportFatalFailure+0x9:00007ffb`ada0a8f9eb00jmpntdll!RtlReportFatalFailure+0xb(00007ffb`ada0a8fb)ResettingdefaultscopeEXCEPTION_RECORD:(.exr-1)ExceptionAddress:00007ffbada0a8f9(ntdll!RtlReportFatalFailure+0x0000000000000009)ExceptionCode:c0000374ExceptionFlags:00000001NumberParameters:1Parameter[0]:00007ffbada727f0...从卦中的ExceptionCode:c0000374来看,说明当前nt堆损坏,尴尬。C#程序如何破坏WindowsNT堆?第三方C++代码分为异常前异常和异常后异常,所以需要用.ecxr把当前线程切到异常前的崩溃点,然后用k观察当前线程栈。0:120>.ecxr;krax=00000000032fed38rbx=00000000c0000374rcx=0000000000000000rdx=0000000000000020rsi=0000000000000001rdi=00007ffbada727f0rip=00007ffbada0a8f9rsp=000000003103c8b0rbp=0000000000c40000r8=00007ffb779bdab7r9=00007ffb782e94c0r10=0000000000002000r11=000000002c4aa498r12=0000000000000000r13=000000003103eb60r14=0000000000000000r15=000000002c873720iopl=0nvupeiplnznapenccs=0033ss=002bds=002bes=002bfs=0053gs=002befl=00000202ntdll!RtlReportFatalFailure+0x9:00007ffb`ada0a8f9eb00jmpntdll!RtlReportFatalFailure`0badafailure+0xb0***affStacktraceforlastsetcontext-.thread/.cxrresetsit#Child-SPRetAddrCallSite0000000000`3103c8b000007ffb`ada0a8c3ntdll!RtlReportFatalFailure+0x90100000000`3103c90000007ffb`ada1314entdll!RtlReportCriticalFailure+0x970200000000`3103c9f000007ffb`ada1345antdll!RtlpHeapHandleError+0x120300000000`3103ca2000007ffb`ad9aef41ntdll!RtlpHpHeapHandleError+0x7a0400000000`3103ca5000007ffb`ad9be520ntdll!RtlpLogHeapFailure+0x450500000000`3103ca8000007ffb`aa3882bfntdll!RtlFreeHeap+0x966e00600000000`3103cb2000007ffb`66fac78fKERNELBASE!LocalFree+0x2f0700000000`3103cb6000007ffb`66f273a4mscorlib_ni+0x63c78f0800000000`3103cc1000007ffb`185c4fdemscorlib_ni!System.Runtime.InteropServices.Marshal.FreeHGlobal+0x24[f:\dd\ndp\clr\src\BCL\system\runtime\interopservices\marshal.cs@12002]02]`3103cc5000007ffb`185c4fa10x00007ffb`185c4fde0a00000000`3103cca000007ffb`185edc820x00007ffb`185c4fa1...从卦中的KERNELBASE!LocalFree方法可以看出程序在释放堆块的过程中。发布会失败吗?原因有很多种,比如:原因一:释放一个已经被释放的堆块原因二:释放一个别人的堆块,那是什么情况呢?有经验的朋友应该知道,ntheap默认开启了损坏退出机制,使用!heap-s命令可以显示损坏原因0:120>!heap-s****************************************************************************************************************************NT堆统计信息如下*****************************************************************************************************************************************************************************************************检测到堆错误******************************************************************详情:堆地址:0000000000c40000错误地址:000000002c873710错误类型:HEAP_FAILURE_BLOCK_NOT_BUSY详情:调用者在空闲块上执行了非法的操作(例如空闲或大小检查)。后续:检查错误的堆栈跟踪以找到罪魁祸首。堆栈跟踪:0x00007处的堆栈跟踪ffbada7284800007ffbad9aef41:ntdll!RtlpLogHeapFailure+0x4500007ffbad9be520:ntdll!RtlFreeHeap+0x966e000007ffbaa3882bf:KERNELBASE!LocalFree+0x2f00007ffb66fac78f:??mscorlib_ni+0x63c78f00007ffb66f273a4:mscorlib_ni!System.Runtime.InteropServices.Marshal.FreeHGlobal+0x2400007ffb185c4fde:+0x185c4fdeLFHKey:0x1d4fd2a71d8b8280腐败终止:启用堆标志保留提交VirtFreeListUCRVirtLockFast(k)(k)(k)(k)lengthblockscont.堆--------------------------------------------------------------------------------------0000000000c4000000000002167561368816364220140520LFH...从卦象中可以清楚的看到错误类型:Errortype:HEAP_FAILURE_BLOCK_NOT_BUSY,这是一个经典的DoubleFree,也就是上面1的原因,接下来我们要找到代码来源了。.2、是谁的代码造成的从线程栈看,底层方法区都是16进制的,说明目前是托管方法,好办。让我们使用!clrstack看看托管代码是什么?0:120>!clrstackOSThreadId:0x4d54(120)ChildSPIPCallSite000000003103cb8800007ffbad9b0544[InlinedCallFrame:000000003103cb88]Microsoft.Win32.Win32Native.LocalFree(IntPtr)000000003103cb8800007ffb66fac78f[InlinedCallFrame:000000003103cb88]Microsoft.Win32.Win32Native.LocalFree(IntPtr)000000003103cb6000007ffb66fac78fDomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr)000000003103cc1000007ffb66f273a4System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr)[f:\dd\ndp\clr\src\BCL\system\runtime\interopservices\marshal.cs@1212]000000003103cc5000007ffb185c4fdexxxx.StructToBytes(System.Object)000000003103ced000007ffb185ec6b1xxx.SendDoseProject(System.String)...从卦象可以清楚的看出是托管方法StructToBytes()引起的。接下来导出这个方法的源码,截图:从方法逻辑上看,这位朋友使用了Marshal来做互操作。为了能够进一步分析,需要找到localResource堆句柄,使用!clrstack-l显示方法栈参数。0:120>!clrstack-lOSThreadId:0x4d54(120)...000000003103cca000007ffb185c4fa1xxx.StructToBytes(System.Object)LOCALS:0x000000003103cd0c=0x000000000000018f0x000000003103ccf8=0x00000000030844200x000000003103ccf0=0x00000000030844200x000000003103cce8=0x00000000000000000x000000003103cce0=0x0000000000000000...经过对比,尴尬的发现localResource的值没有显示出来。..一般在dump中不能显示IntPtr类型,遇到过好几次了,挺烦人的。..由于无法显示堆块句柄值。..那我们该怎么办呢?天要杀人?3、绝境求生既然托管层找不到堆块句柄,那就去非托管层找,比如这里的KERNELBASE!LocalFree+0x2f函数,msdn上的定义如下:HLOCALLocalFree([输入]_Frees_ptr_opt_HLOCALhMem);那么如何找到这个hMem值呢?在x86程序中可以直接用kb提取,但在x64下无效,因为它使用寄存器传递方法参数。此时寄存器值已经刷新为ntdll!NtWaitForMultipleObjects+0x14,比如下面的rcx肯定不是hMem值。0:120>rrax=000000000000005brbx=0000000000005b08rcx=0000000000000002rdx=000000003103b690rsi=0000000000000002rdi=0000000000000000rip=00007ffbad9b0544rsp=000000003103b658rbp=0000000000001da4r8=0000000000001000r9=0101010101010101r10=0000000000000000r11=0000000000000246r12=0000000000000000r13=000000003103c930r14=0000000000001f98r15=c3b05呢?其实还有一个办法,就是观察KERNELBASE!LocalFree+0x2f方法的汇编代码,看是否将rcx临时保存到线程栈中。0:120>ukernelbase!localfreekernelbase!localfree:00007FFB`AA38829048895C2410MOVQWORDPTRPTR[RSP+10H],RBX00007FFB`AAA3888295rcx00007ffb`aa38829f57pushrdi00007ffb`aa3882a04883ec30subrsp,30h00007ffb`aa3882a4488bd9movrbx,rcx00007ffb`aa3882a7f6c308testbl,800007ffb`aa3882aa753fjneKERNELBASE!LocalFree+0x5b(00007ffb`aa3882eb)很开心的看到,当前的rcx保存到rsp+8,如何获取rsp?可以用k提取父函数mscorlib_ni+0x63c78f中的Child-SP值。0:120>k#Child-SPRetAddrCallSite...0e00000000`3103ca8000007ffb`aa3882bfntdll!RtlFreeHeap+0x966e00f00000000`3103cb2000007ffb`66fac78fKERNELBASE!LocalFree+0x2f1000000000`3103cb6000007ffb`66f273a4mscorlib_ni+0x63c78f...因为这个Child-SP是调用前的sp,而汇编中的sp是调用后的,所以相差一个retaddr指针单元,所以计算方法是:ChildSp-0x8+0x8是堆块句柄。0:120>dp00000000`3103cb60-0x8+0x8L100000000`3103cb6000000000`2c873720上面的000000002c873720就是堆块句柄,然后用命令!heap-x000000002c873720观察堆块情况。0:120>!heap-x000000002c873720Entry用户堆段大小PrevSize未使用的标志--------------------------------------------------------------------------------------------------------000000002c873710000000002c8737200000000000c40000000000002c8703c030-0LFH;free不出所料,这个堆块已经处于Free状态,Free必然会报错,经典的DoubleFree哈。4.回头看源码仔细阅读源码,发现两个问题。localResource没有加锁处理,并发时容易出问题。localResource是在多个方法中使用的类级变量。将信息反馈给好友后,建议好友锁定并缩小localResource的范围。3.总结偶尔的生产崩溃。主要原因是我朋友的代码逻辑有问题。localResourcehandle资源没有得到妥善保护,重复释放导致ntheap损坏。这个dump的问题虽然比较小,但是通过逆向分析找出原因,还是比较考验基本功的。