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

从卦中可以清晰的看到错误类型: Error type: HEAP_FAILURE_BLOCK_NOT_BUSY ,这是经典的 Double Free,也就是上面的 原因1,接下来我们就要寻找代码源头了 。。。
2. 是谁的代码引发的从线程栈上看,底层的方法区都是十六进制,这表示当前是托管方法,这就好办了,我们用 !clrstack 看看托管代码是什么?
0:120> !clrstackOS Thread Id: 0x4d54 (120)Child SPIP Call Site000000003103cb88 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() 引发的,接下来导出这个方法的源码,截图如下:

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

文章插图
从方法逻辑看,这位朋友用了 Marshal 做了互操作,为了能够进一步分析 , 需要找到 localResource 堆块句柄,使用 !clrstack -l 显示方法栈参数 。
0:120> !clrstack -lOS Thread Id: 0x4d54 (120)...000000003103cca0 00007ffb185c4fa1 xxx.StructToBytes(System.Object)LOCALS:0x000000003103cd0c = 0x000000000000018f0x000000003103ccf8 = 0x00000000030844200x000000003103ccf0 = 0x00000000030844200x000000003103cce8 = 0x00000000000000000x000000003103cce0 = 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> rrax=000000000000005b rbx=0000000000005b08 rcx=0000000000000002rdx=000000003103b690 rsi=0000000000000002 rdi=0000000000000000rip=00007ffbad9b0544 rsp=000000003103b658 rbp=0000000000001da4 r8=0000000000001000r9=0101010101010101 r10=0000000000000000r11=0000000000000246 r12=0000000000000000 r13=000000003103c930r14=0000000000001f98 r15=0000000000000000iopl=0nv up ei pl zr na po nccs=0033ss=002bds=002bes=002bfs=0053gs=002befl=00000246ntdll!NtWaitForMultipleObjects+0x14:00007ffb`ad9b0544 c3ret怎么办呢?其实还有一条路,就是观察 KERNELBASE!LocalFree+0x2f 方法的汇编代码 , 看看它有没有将 rcx 临时性的存到 线程栈 上 。
0:120> u KERNELBASE!LocalFreeKERNELBASE!LocalFree:00007ffb`aa388290 48895c2410movqword ptr [rsp+10h],rbx00007ffb`aa388295 4889742418movqword ptr [rsp+18h],rsi00007ffb`aa38829a 48894c2408movqword ptr [rsp+8],rcx00007ffb`aa38829f 57pushrdi00007ffb`aa3882a0 4883ec30subrsp,30h00007ffb`aa3882a4 488bd9movrbx,rcx00007ffb`aa3882a7 f6c308testbl,800007ffb`aa3882aa 753fjneKERNELBASE!LocalFree+0x5b (00007ffb`aa3882eb)很开心的看到,当前的 rcx 存到了 rsp+8 位置上,那如何拿到 rsp 呢? 可以用 k 提取父函数 mscorlib_ni+0x63c78f 中的 Child-SP 值 。
0:120> k # Child-SPRetAddrCall Site ...0e 00000000`3103ca80 00007ffb`aa3882bfntdll!RtlFreeHeap+0x966e00f 00000000`3103cb20 00007ffb`66fac78fKERNELBASE!LocalFree+0x2f10 00000000`3103cb60 00007ffb`66f273a4mscorlib_ni+0x63c78f...因为这个 Child-SP 是 call 之前的 sp, 汇编中的 sp 是 call 之后的 , 所以相差一个 retaddr 指针单元,所以计算方法是: ChildSp- 0x8 + 0x8 就是 堆块句柄 。
0:120> dp 00000000`3103cb60-0x8+0x8 L100000000`3103cb6000000000`2c873720上面的 000000002c873720 就是堆块句柄 , 接下来用命令 !heap -x 000000002c873720 观察堆块情况 。
0:120> !heap -x 000000002c873720EntryUserHeapSegmentSizePrevSizeUnusedFlags-------------------------------------------------------------------------------------------------------------000000002c873710000000002c8737200000000000c40000000000002c8703c030-0LFH;free

推荐阅读