记一次 .NET 某医疗住院系统 崩溃分析

Posted 一线码农

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次 .NET 某医疗住院系统 崩溃分析相关的知识,希望对你有一定的参考价值。

一:背景

1. 讲故事

最近收到了两起程序崩溃的dump,查了下都是经典的 double free 造成的,蛮有意思,这里就抽一篇出来分享一下经验供后面的学习者避坑吧。

二:WinDbg 分析

1. 崩溃点在哪里

windbg 带了一个自动化分析命令 !analyze -v 可以帮助我们找到崩溃时的程序指令地址以及崩溃的代码,这对我们分析问题非常有帮助。


0:090> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************
CONTEXT:  (.ecxr)
rax=00007ffec265d6e0 rbx=00000000c0000374 rcx=00000053653fe4f0
rdx=00007ffec1d3e9a0 rsi=0000000000000001 rdi=00007ffed7b827f0
rip=00007ffed7b1b349 rsp=00000053653fed10 rbp=000001c14fd20000
 r8=000001c11957d9a0  r9=0000000000000033 r10=000001c453dbc7f0
r11=00007ffeb94db004 r12=0000000000000001 r13=000001c12e8526d0
r14=0000000000000000 r15=000001ce25531c60
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
ntdll!RtlReportFatalFailure+0x9:
00007ffe`d7b1b349 eb00            jmp     ntdll!RtlReportFatalFailure+0xb (00007ffe`d7b1b34b)
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ffed7b1b349 (ntdll!RtlReportFatalFailure+0x0000000000000009)
   ExceptionCode: c0000374
  ExceptionFlags: 00000001
NumberParameters: 1
   Parameter[0]: 00007ffed7b827f0

PROCESS_NAME:  w3wp.exe
...

熟悉 windows ntheap 的朋友应该知道,ExceptionCode: c0000374 是经典的 堆破坏 状态码,那到底是谁破坏了呢?

2. 到底是谁破坏了NT堆

windows 给了 ntheap 强大的调试支持,默认开启了 Termination on corruption 破坏检测,也就是说当你使用 !heap -s 的时候会显示具体破坏的详情记录,输出如下:


0:090> !heap -s


************************************************************************************************************************
                                              NT HEAP STATS BELOW
************************************************************************************************************************
**************************************************************
*                                                            *
*                  HEAP ERROR DETECTED                       *
*                                                            *
**************************************************************

Details:

Heap address:  000001c14fd20000
Error address: 000001ce25531c50
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 0x00007ffed7b82848
    00007ffed7abe109: ntdll!RtlpLogHeapFailure+0x45
    00007ffed7acbb0e: ntdll!RtlFreeHeap+0x9d3ce
    00007ffeb093276f: OraOps12!ssmem_free+0xf
    00007ffeb0943077: OraOps12!OpsMetFreeValCtx+0xd7
    00007ffeb093cdd8: OraOps12!OpsDacDispose+0x2b8
    00007ffe655e4374: +0x655e4374

LFH Key                   : 0x5baf44f8068da60f
Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                            (k)     (k)    (k)     (k) length      blocks cont. heap 
-------------------------------------------------------------------------------------
000001c14fd20000 00000002 1021576 964388 1020020  19222  6063   166    2    82f   LFH
000001c14fc70000 00008000      64      4     64      2     1     1    0      0      
...

上面的 Error type: HEAP_FAILURE_BLOCK_NOT_BUSY 表示是一个 double free,从 Stack trace 看是 OpsDacDispose 方法造成的,应该和 Oracle 相关,这就比较迷了。。。

3. 是托管层触发的吗

是不是托管层触发的呢?这就需要理解 Windows 独有的 SEH 异常处理机制,也就是说 Windows 的异常都会在 内核态 走一圈,画个图如下:

只要找到 t1 时刻的崩溃点,然后观察线程栈即可,代码如下:


0:090> .ecxr
rax=00007ffec265d6e0 rbx=00000000c0000374 rcx=00000053653fe4f0
rdx=00007ffec1d3e9a0 rsi=0000000000000001 rdi=00007ffed7b827f0
rip=00007ffed7b1b349 rsp=00000053653fed10 rbp=000001c14fd20000
 r8=000001c11957d9a0  r9=0000000000000033 r10=000001c453dbc7f0
r11=00007ffeb94db004 r12=0000000000000001 r13=000001c12e8526d0
r14=0000000000000000 r15=000001ce25531c60
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
ntdll!RtlReportFatalFailure+0x9:
00007ffe`d7b1b349 eb00            jmp     ntdll!RtlReportFatalFailure+0xb (00007ffe`d7b1b34b)
0:090> k
  *** Stack trace for last set context - .thread/.cxr resets it
 # Child-SP          RetAddr               Call Site
00 00000053`653fed10 00007ffe`d7b1b313     ntdll!RtlReportFatalFailure+0x9
01 00000053`653fed60 00007ffe`d7b23b9e     ntdll!RtlReportCriticalFailure+0x97
02 00000053`653fee50 00007ffe`d7b23eaa     ntdll!RtlpHeapHandleError+0x12
03 00000053`653fee80 00007ffe`d7abe109     ntdll!RtlphpHeapHandleError+0x7a
04 00000053`653feeb0 00007ffe`d7acbb0e     ntdll!RtlpLogHeapFailure+0x45
05 00000053`653feee0 00007ffe`b093276f     ntdll!RtlFreeHeap+0x9d3ce
06 00000053`653fef80 00007ffe`b0943077     OraOps12!ssmem_free+0xf
07 00000053`653fefb0 00007ffe`b093cdd8     OraOps12!OpsMetFreeValCtx+0xd7
08 00000053`653fefe0 00007ffe`655e4374     OraOps12!OpsDacDispose+0x2b8
09 00000053`653ff060 00007ffe`655e31cf     0x00007ffe`655e4374
0a 00000053`653ff150 00007ffe`6a80969d     0x00007ffe`655e31cf
0b 00000053`653ff1f0 00007ffe`c4b96d06     0x00007ffe`6a80969d
0c 00000053`653ff220 00007ffe`c4c30e81     clr!FastCallFinalizeWorker+0x6
0d 00000053`653ff250 00007ffe`c4c30e09     clr!FastCallFinalize+0x55
0e 00000053`653ff2a0 00007ffe`c4c30d3a     clr!MethodTable::CallFinalizer+0xb5
0f 00000053`653ff2f0 00007ffe`c4c30bf5     clr!CallFinalizer+0x5e
10 00000053`653ff330 00007ffe`c4c304dc     clr!FinalizerThread::DoOneFinalization+0x95
11 00000053`653ff410 00007ffe`c4c31777     clr!FinalizerThread::FinalizeAllObjects+0xbf
12 00000053`653ff450 00007ffe`c4b97d01     clr!FinalizerThread::FinalizeAllObjects_Wrapper+0x18
13 00000053`653ff480 00007ffe`c4b97c70     clr!ManagedThreadBase_DispatchInner+0x39
14 00000053`653ff4c0 00007ffe`c4b97bad     clr!ManagedThreadBase_DispatchMiddle+0x6c
15 00000053`653ff5c0 00007ffe`c4b9ac34     clr!ManagedThreadBase_DispatchOuter+0x75
16 00000053`653ff650 00007ffe`c4bf5271     clr!ManagedThreadBase_DispatchInCorrectAD+0x15
17 00000053`653ff680 00007ffe`c4b9ac72     clr!Thread::DoADCallBack+0x109
18 00000053`653ff830 00007ffe`c4c3172a     clr!ManagedThreadBase_DispatchInner+0x82
19 00000053`653ff870 00007ffe`c4c304dc     clr!FinalizerThread::DoOneFinalization+0x1f1
1a 00000053`653ff950 00007ffe`c4c3062b     clr!FinalizerThread::FinalizeAllObjects+0xbf
1b 00000053`653ff990 00007ffe`c4b97d01     clr!FinalizerThread::FinalizerThreadWorker+0xbb
1c 00000053`653ff9d0 00007ffe`c4b97c70     clr!ManagedThreadBase_DispatchInner+0x39
1d 00000053`653ffa10 00007ffe`c4b97bad     clr!ManagedThreadBase_DispatchMiddle+0x6c
1e 00000053`653ffb10 00007ffe`c4cf4d4a     clr!ManagedThreadBase_DispatchOuter+0x75
1f 00000053`653ffba0 00007ffe`c4d5044f     clr!FinalizerThread::FinalizerThreadStart+0x126
20 00000053`653ffc40 00007ffe`d6157e94     clr!Thread::intermediateThreadProc+0x86
21 00000053`653ffd00 00007ffe`d7a87ad1     kernel32!BaseThreadInitThunk+0x14
22 00000053`653ffd30 00000000`00000000     ntdll!RtlUserThreadStart+0x21

0:090> !clrstack 
OS Thread Id: 0x5634 (90)
        Child SP               IP Call Site
00000053653ff0b8 00007ffed7abf0e4 [InlinedCallFrame: 00000053653ff0b8] Oracle.DataAccess.Client.OpsDac.Dispose(IntPtr, IntPtr, IntPtr, IntPtr ByRef, Oracle.DataAccess.Client.OpoMetValCtx*, Oracle.DataAccess.Client.OpoDacValCtx* ByRef, Oracle.DataAccess.Client.OpoSqlValCtx*, Int32, Int32)
00000053653ff0b8 00007ffe655e4374 [InlinedCallFrame: 00000053653ff0b8] Oracle.DataAccess.Client.OpsDac.Dispose(IntPtr, IntPtr, IntPtr, IntPtr ByRef, Oracle.DataAccess.Client.OpoMetValCtx*, Oracle.DataAccess.Client.OpoDacValCtx* ByRef, Oracle.DataAccess.Client.OpoSqlValCtx*, Int32, Int32)
00000053653ff060 00007ffe655e4374 DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr, IntPtr, IntPtr, IntPtr ByRef, Oracle.DataAccess.Client.OpoMetValCtx*, Oracle.DataAccess.Client.OpoDacValCtx* ByRef, Oracle.DataAccess.Client.OpoSqlValCtx*, Int32, Int32)
00000053653ff150 00007ffe655e31cf Oracle.DataAccess.Client.OracleDataReader.Dispose(Boolean)
00000053653ff1f0 00007ffe6a80969d Oracle.DataAccess.Client.OracleDataReader.Finalize()
00000053653ff608 00007ffec4b96d06 [DebuggerU2MCatchHandlerFrame: 00000053653ff608] 
00000053653ff788 00007ffec4b96d06 [ContextTransitionFrame: 00000053653ff788] 
00000053653ff8d0 00007ffec4b96d06 [GCFrame: 00000053653ff8d0] 
00000053653ffb58 00007ffec4b96d06 [DebuggerU2MCatchHandlerFrame: 00000053653ffb58] 

从调用栈来看,原来是 终结器线程 在调用 OracleDataReader.Dispose() 方法的时候抛的异常,这个结果还是挺意外的,也就是说这个问题不是用户代码造成的,真的是 Oracle 这个 OraOps12.dll 造成的。。。

接下来用 lm 观察下这个 dll 的详情信息,输出如下:


0:090> lmDvmOraOps12
Browse full module list
start             end                 module name
00007ffe`b0920000 00007ffe`b098c000   OraOps12 C (export symbols)       OraOps12.dll
    Loaded symbol image file: OraOps12.dll
    Image path: C:\\ODAC\\xxxx\\OraOps12.dll
    Image name: OraOps12.dll
    Browse all global symbols  functions  data
    Timestamp:        Sat Sep 26 23:16:56 2015 (5606B6E8)
    CheckSum:         00000000
    ImageSize:        0006C000
    File version:     2.121.2.0
    Product version:  2.121.2.0
    File flags:       0 (Mask 3F)
    File OS:          4 Unknown Win32
    File type:        2.0 Dll
    File date:        00000000.00000000
    Translations:     0409.04b0
    Information from resource tables:
        CompanyName:      Oracle Corporation
        ProductName:      Oracle Data Provider for .NET
        InternalName:     OraOps
        OriginalFilename: OraOps12.dll
        ProductVersion:   2.121.2.0 ODAC RELEASE 4
        FileVersion:      2.121.2.0
        FileDescription:  Oracle Provider Services
        LegalCopyright:   Copyright © 2014

虽然对 Oracle 不熟,但从 Timestamp: Sat Sep 26 23:16:56 2015 来看应该是一个比较老的 DLL 了,所以给到朋友的建议就是升级 OraOps12.dll

4. 是否有同行者

有时候直接让朋友升级dll有点缺少底气,最好就是找到一些同行者,经过一顿搜索,还真有同行者,又多了一份说服力,网址: https://techcommunity.microsoft.com/t5/iis-support-blog/w3wp-exe-crash-exception-code-0xc0000005/ba-p/334351

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cvkAKFpk-1679565236422)(https://huangxincheng.oss-cn-hangzhou.aliyuncs.com/img/20230323172126.png)]

三:总结

在百加dump的分析旅程中,碰到和 Oracle SDK 相关的也有 3+ 起了,可能也许这些 SDK 在对接 .NET 上还不是特别稳健,大家在使用上尽量更新到最新版本吧,且用且珍惜!

记一次 .NET 某医疗器械 程序崩溃分析

一:背景

1.讲故事

前段时间有位朋友在微信上找到我,说他的程序偶发性崩溃,让我帮忙看下怎么回事,上面给的压力比较大,对于这种偶发性崩溃,比较好的办法就是利用 AEDebug 在程序崩溃的时候自动抽一管血出来,看看崩溃点是什么,其实我的系列文章中,关于崩溃类的dump比较少,刚好补一篇上来,话不多说,上 windbg 。

二:WinDbg 分析

1. 崩溃点在哪里

在 windbg 中有一个 !analyze -v 命令可以自动化分析,输出信息如下:

0:120> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************
CONTEXT:  (.ecxr)
rax=00000000032fed38 rbx=00000000c0000374 rcx=0000000000000000
rdx=0000000000000020 rsi=0000000000000001 rdi=00007ffbada727f0
rip=00007ffbada0a8f9 rsp=000000003103c8b0 rbp=0000000000c40000
 r8=00007ffb779bdab7  r9=00007ffb782e94c0 r10=0000000000002000
r11=000000002c4aa498 r12=0000000000000000 r13=000000003103eb60
r14=0000000000000000 r15=000000002c873720
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!RtlReportFatalFailure+0x9:
00007ffb`ada0a8f9 eb00            jmp     ntdll!RtlReportFatalFailure+0xb (00007ffb`ada0a8fb)
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ffbada0a8f9 (ntdll!RtlReportFatalFailure+0x0000000000000009)
   ExceptionCode: c0000374
  ExceptionFlags: 00000001
NumberParameters: 1
   Parameter[0]: 00007ffbada727f0
...

从卦中的 ExceptionCode: c0000374 异常码来看,表示当前 nt堆损坏,这就尴尬了,一个C#程序咋会把 windows nt 堆给弄坏了,可能是引入了第三方的 C++ 代码。

由于异常分异常前和异常后,所以需要用 .ecxr 将当前线程切到异常前的崩溃点,然后使用 k 观察当前的线程栈。

0:120> .ecxr ; k
rax=00000000032fed38 rbx=00000000c0000374 rcx=0000000000000000
rdx=0000000000000020 rsi=0000000000000001 rdi=00007ffbada727f0
rip=00007ffbada0a8f9 rsp=000000003103c8b0 rbp=0000000000c40000
 r8=00007ffb779bdab7  r9=00007ffb782e94c0 r10=0000000000002000
r11=000000002c4aa498 r12=0000000000000000 r13=000000003103eb60
r14=0000000000000000 r15=000000002c873720
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!RtlReportFatalFailure+0x9:
00007ffb`ada0a8f9 eb00            jmp     ntdll!RtlReportFatalFailure+0xb (00007ffb`ada0a8fb)
  *** Stack trace for last set context - .thread/.cxr resets it
 # Child-SP          RetAddr               Call Site
00 00000000`3103c8b0 00007ffb`ada0a8c3     ntdll!RtlReportFatalFailure+0x9
01 00000000`3103c900 00007ffb`ada1314e     ntdll!RtlReportCriticalFailure+0x97
02 00000000`3103c9f0 00007ffb`ada1345a     ntdll!RtlpHeapHandleError+0x12
03 00000000`3103ca20 00007ffb`ad9aef41     ntdll!RtlpHpHeapHandleError+0x7a
04 00000000`3103ca50 00007ffb`ad9be520     ntdll!RtlpLogHeapFailure+0x45
05 00000000`3103ca80 00007ffb`aa3882bf     ntdll!RtlFreeHeap+0x966e0
06 00000000`3103cb20 00007ffb`66fac78f     KERNELBASE!LocalFree+0x2f
07 00000000`3103cb60 00007ffb`66f273a4     mscorlib_ni+0x63c78f
08 00000000`3103cc10 00007ffb`185c4fde     mscorlib_ni!System.Runtime.InteropServices.Marshal.FreeHGlobal+0x24 [f:\\dd\\ndp\\clr\\src\\BCL\\system\\runtime\\interopservices\\marshal.cs @ 1212] 
09 00000000`3103cc50 00007ffb`185c4fa1     0x00007ffb`185c4fde
0a 00000000`3103cca0 00007ffb`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:  0000000000c40000
Error address: 000000002c873710
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
000000003103cb88 00007ffbad9b0544 [InlinedCallFrame: 000000003103cb88] Microsoft.Win32.Win32Native.LocalFree(IntPtr)
000000003103cb88 00007ffb66fac78f [InlinedCallFrame: 000000003103cb88] Microsoft.Win32.Win32Native.LocalFree(IntPtr)
000000003103cb60 00007ffb66fac78f DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr)
000000003103cc10 00007ffb66f273a4 System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr) [f:\\dd\\ndp\\clr\\src\\BCL\\system\\runtime\\interopservices\\marshal.cs @ 1212]
000000003103cc50 00007ffb185c4fde xxxx.StructToBytes(System.Object)
000000003103ced0 00007ffb185ec6b1 xxx.SendDoseProject(System.String)
...

从卦中可以清晰的看到是托管方法 StructToBytes() 引发的,接下来导出这个方法的源码,截图如下:

从方法逻辑看,这位朋友用了 Marshal 做了互操作,为了能够进一步分析,需要找到 localResource 堆块句柄,使用 !clrstack -l 显示方法栈参数。

0:120> !clrstack -l
OS Thread Id: 0x4d54 (120)
...
000000003103cca0 00007ffb185c4fa1 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=000000000000005b rbx=0000000000005b08 rcx=0000000000000002
rdx=000000003103b690 rsi=0000000000000002 rdi=0000000000000000
rip=00007ffbad9b0544 rsp=000000003103b658 rbp=0000000000001da4
 r8=0000000000001000  r9=0101010101010101 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=000000003103c930
r14=0000000000001f98 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!NtWaitForMultipleObjects+0x14:
00007ffb`ad9b0544 c3              ret

怎么办呢?其实还有一条路,就是观察 KERNELBASE!LocalFree+0x2f 方法的汇编代码,看看它有没有将 rcx 临时性的存到 线程栈 上。

0:120> u KERNELBASE!LocalFree
KERNELBASE!LocalFree:
00007ffb`aa388290 48895c2410      mov     qword ptr [rsp+10h],rbx
00007ffb`aa388295 4889742418      mov     qword ptr [rsp+18h],rsi
00007ffb`aa38829a 48894c2408      mov     qword ptr [rsp+8],rcx
00007ffb`aa38829f 57              push    rdi
00007ffb`aa3882a0 4883ec30        sub     rsp,30h
00007ffb`aa3882a4 488bd9          mov     rbx,rcx
00007ffb`aa3882a7 f6c308          test    bl,8
00007ffb`aa3882aa 753f            jne     KERNELBASE!LocalFree+0x5b (00007ffb`aa3882eb)

很开心的看到,当前的 rcx 存到了 rsp+8 位置上,那如何拿到 rsp 呢?可以用 k 提取父函数 mscorlib_ni+0x63c78f 中的 Child-SP 值。

0:120> k
 # Child-SP          RetAddr               Call Site
 ...
0e 00000000`3103ca80 00007ffb`aa3882bf     ntdll!RtlFreeHeap+0x966e0
0f 00000000`3103cb20 00007ffb`66fac78f     KERNELBASE!LocalFree+0x2f
10 00000000`3103cb60 00007ffb`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 000000002c873720
Entry             User              Heap              Segment               Size  PrevSize  Unused    Flags
-------------------------------------------------------------------------------------------------------------
000000002c873710  000000002c873720  0000000000c40000  000000002c8703c0        30      -            0  LFH;free

果不其然,这个堆块已经是 Free 状态了,再 Free 必然会报错,经典的 Double Free 哈。

4. 回首再看源码

仔细阅读源码,发现有两个问题。

  1. 没有对 localResource 加锁处理,在并发的时候容易出现问题。

  2. localResource 是一个类级别变量,在多个方法中被使用。

将信息反馈给朋友之后,建议朋友加锁并降低 localResource 作用域。

三:总结

这次偶发的生产崩溃事故,主要原因是朋友的代码在逻辑上出了点问题,没有合理的保护好 localResource 句柄资源,反复释放导致的 ntheap 破坏。

这个 dump 虽然问题比较小白,但逆向分析找出原因,还是挺考验基本功的。

以上是关于记一次 .NET 某医疗住院系统 崩溃分析的主要内容,如果未能解决你的问题,请参考以下文章

记一次 .NET某医疗器械清洗系统 卡死分析

记一次 .NET 某企业 ERP网站系统 崩溃分析

记一次 .NET 某制造业 MES 系统崩溃分析

记一次 .NET 某工控MES程序 崩溃分析

记一次 .NET 某新能源汽车锂电池检测程序 UI挂死分析

记一次 .NET 某新能源汽车锂电池检测程序 UI挂死分析