燕云十六州 發表於 2025-6-17 14:22:00

记一次 .NET 某发证机系统 崩溃分析

<h2 id="一背景">一:背景</h2>
<h3 id="1-讲故事">1. 讲故事</h3>
<p>前些天有位朋友在微信上找到我,说他的系统有偶发崩溃,自己也没找到原因,让我帮忙看下怎么回事,我分析dump一直都是免费的,毕竟对这些东西挺感兴趣,有问题可以直接call我,好了,接下来我们就来分析dump吧。</p>
<h2 id="二程序为什么会崩">二:程序为什么会崩</h2>
<h3 id="1-观察崩溃上下文">1. 观察崩溃上下文</h3>
<p>windbg有一个厉害之处在于双击dump之后会自动定位到崩溃的线程,然后通过 <code>.ecxr; k10</code> 命令就可以看到崩溃点了,输出如下:</p>
<pre><code class="language-C#">
0:083&gt; .ecxr; k10
rax=000000004bdefa50 rbx=00000c45ea960c80 rcx=000000ffffffffff
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=00000000773da365 rsp=0000000046d0fb50 rbp=000000004bde28d0
r8=000000004bde28c0r9=0000000000000001 r10=0000000000000000
r11=0000000000000206 r12=00000000006b0000 r13=000000004bde2950
r14=0000000000000000 r15=0000000000000001
iopl=0         nv up ei pl nz na pe nc
cs=0033ss=002bds=002bes=002bfs=0053gs=002b             efl=00010202
ntdll!RtlFreeHeap+0x1a5:
00000000`773da365 488b7b08      mov   rdi,qword ptr ds:00000c45`ea960c88=????????????????
# Child-SP          RetAddr               Call Site
00 00000000`46d0fb50 000007fe`feda10c8   ntdll!RtlFreeHeap+0x1a5
01 00000000`46d0fbd0 000007fe`ee66f126   msvcrt!free+0x1c
02 00000000`46d0fc00 000007fe`ee6556ae   ksproxy!CStandardInterfaceHandler::KsCompleteIo+0x4e6
03 00000000`46d0fce0 000007fe`ee66bf5c   ksproxy!CKsOutputPin::OutputPinBufferHandler+0x1e
04 00000000`46d0fd10 00000000`771a556d   ksproxy!CAsyncItemHandler::AsyncItemProc+0x20c
05 00000000`46d0fd70 00000000`7740372d   kernel32!BaseThreadInitThunk+0xd
06 00000000`46d0fda0 00000000`00000000   ntdll!RtlUserThreadStart+0x1d

</code></pre>
<p>从卦中可以看到,程序崩溃在 RtlFreeHeap 函数中,熟悉这玩意的朋友应该知道它是用来释放 <code>堆块</code> 的,签名如下:</p>
<pre><code class="language-C#">
NTSYSAPI LOGICAL RtlFreeHeap(
         PVOID               HeapHandle,
ULONG               Flags,
               _Frees_ptr_opt_ PVOID BaseAddress
);

</code></pre>
<p>接下来就是寻找该堆块的首地址 <code>BaseAddress</code>,即 r8 寄存器值,使用 <code>uf ntdll!RtlFreeHeap</code> 即可,输出如下:</p>
<pre><code class="language-C#">
0:083&gt; uf ntdll!RtlFreeHeap
ntdll!RtlFreeHeap:
00000000`773da1c0 4053            push    rbx
00000000`773da1c2 55            push    rbp
00000000`773da1c3 56            push    rsi
00000000`773da1c4 57            push    rdi
00000000`773da1c5 4154            push    r12
00000000`773da1c7 4883ec50      sub   rsp,50h
00000000`773da1cb 33f6            xor   esi,esi
00000000`773da1cd 498be8          mov   rbp,r8
00000000`773da1d0 8bfa            mov   edi,edx
00000000`773da1d2 4c8be1          mov   r12,rcx
00000000`773da1d5 488bde          mov   rbx,rsi
00000000`773da1d8 4d85c0          test    r8,r8
...
ntdll!RtlFreeHeap+0x179:
00000000`773da339 498b4008      mov   rax,qword ptr
00000000`773da33d 498bd8          mov   rbx,r8
00000000`773da340 48b9ffffffffff000000 mov rcx,0FFFFFFFFFFh
00000000`773da34a 4933dc          xor   rbx,r12
00000000`773da34d 4823c1          and   rax,rcx
00000000`773da350 48c1eb04      shr   rbx,4
00000000`773da354 4833d8          xor   rbx,rax
00000000`773da357 48331d3a321000xor   rbx,qword ptr
00000000`773da35e 48c1e304      shl   rbx,4
00000000`773da362 0f0d0b          prefetchw
00000000`773da365 488b7b08      mov   rdi,qword ptr

</code></pre>
<p>根据卦中的汇编代码 <code>mov rbp,r8</code> ,看样子是 rbp 保存了 <code>BaseAddress</code> 地址,接下来使用 <code>!heap -x 000000004bde28d0</code> 看看到底啥情况,输出如下:</p>
<pre><code class="language-C#">
0:083&gt; !heap -x 000000004bde28d0
SEGMENT HEAP ERROR: failed to initialize the extention
List corrupted: (Blink-&gt;Flink = 0000000000000000) != (Block = 000000004bde28c0)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bde28b0 Error: block list entry corrupted

List corrupted: (Flink-&gt;Blink = 900000004bdefa50) != (Block = 000000004bdefa50)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bdefa40 Error: block list entry corrupted

Entry             User            Heap            Segment               SizePrevSizeUnused    Flags
-------------------------------------------------------------------------------------------------------------
000000004bde28b0000000004bde28c000000000006b0000000000004bde0000      3740       800         0free

</code></pre>
<p>从卦中可以看到,当前的 <code>000000004bde28c0</code> 是一个 free 堆块,同时也抛了一个 <code>堆块列表</code> 的损坏错误,这就有点意思了。。。</p>
<h3 id="2-doublefree-导致的吗">2. doublefree 导致的吗</h3>
<p>熟悉 win32 的朋友应该知道,在已经 free 的块上再次调用 RtlFreeHeap 释放会是一个经典的 <code>doublefree</code>,貌似这个问题已经定位了。。。但如果你<strong>修车经验</strong>丰富的话,你应该知道 <code>堆管理器</code> 检测到 doublefree 的时候会是这样的调用栈记一次 .NET 某医疗住院系统 崩溃分析<br>
, 参考如下:</p>
<pre><code class="language-C#">
0:090&gt; !heap -s
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
    ...

</code></pre>
<p>而我们这个dump是访问违例,虽然这个dump必崩无疑,但这段逻辑此时还没执行到,也就是在这块逻辑之前就崩掉了,那为什么会崩掉呢?到底经历了何样的惊魂时刻。。。</p>
<h3 id="3-突破口在哪里">3. 突破口在哪里</h3>
<p>要想寻找突破口,就得理解下面的这两句了,再次输出一下吧。</p>
<pre><code class="language-C#">
List corrupted: (Blink-&gt;Flink = 0000000000000000) != (Block = 000000004bde28c0)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bde28b0 Error: block list entry corrupted

List corrupted: (Flink-&gt;Blink = 900000004bdefa50) != (Block = 000000004bdefa50)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bdefa40 Error: block list entry corrupted

</code></pre>
<p>要理解这个,需要你对 <code>_HEAP</code> 结构有一个深度的理解,这个在我的 <code>.NET高级调试训练营</code> 里有一个系统的解读。</p>
<p>大家要知道 <code>堆管理器</code> 对 Free 的管理采用的是 <code>双向链表</code> 的方式,其中 Flink 表示下一个(Forward)节点,BLink 表示前一个(Backward)结点,有朋友搞不清的话,我画个简图。</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202506/214741-20250617142206906-1728852590.png" alt="" loading="lazy"></p>
<p>有了简图之后,接下来逐个解读下:</p>
<ul>
<li>(Blink-&gt;Flink = 0000000000000000) != (Block = 000000004bde28c0)</li>
</ul>
<p>这是经典的 <code>一去一回</code> ,结果发现不再是自己了。。。即 <code>0000000000000000 != 000000004bde28c0</code>,我们用 ntdll!_LIST_ENTRY 来具象化一下,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202506/214741-20250617142206870-1702896978.png" alt="" loading="lazy"></p>
<p>从卦中可以看到 <code>Blink = 0x900000004bdefa50</code> 时就报错了,由于报错windbg 就将结果显示为 <code>0000000000000000</code>,所以这一来一回居然不等于自己,所以堆管理器就觉得很奇葩。。。</p>
<ul>
<li>(Flink-&gt;Blink = 900000004bdefa50) != (Block = 000000004bdefa50)</li>
</ul>
<p>这是经典的 <code>一回一去</code>,结果发现 Blink 不对。。。本来应该是 <code>0x000000004bdefa50</code> 结果是 <code>0x900000004bdefa50</code> 截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202506/214741-20250617142206881-1968119809.png" alt="" loading="lazy"></p>
<p>到这里我相信很多人会有一个疑问,我也没看到 <code>0x000000004bdefa50</code> 地址呀,凭什么说<code>0x900000004bdefa50</code> 的前身是 <code>0x000000004bdefa50</code> ?</p>
<p>要想找到这个答案,又是考你的 <code>堆管理器</code> 知识,所有的 free 都挂在 <code>FreeLists</code> 字段里,同样也是采用 <code>双向链表</code> 的方式,输出如下:</p>
<pre><code class="language-C#">
0:083&gt; dt nt!_HEAP 00000000006b0000 -yFreeLists
ntdll!_HEAP
   +0x158 FreeLists : _LIST_ENTRY [ 0x00000000`1f1d7f90 - 0x00000000`1f23dbf0 ]

</code></pre>
<p>接下来写一段简单的脚本把这个list给遍历下,看看 <code>0x000000004bde28c0</code> 的BLink结点是不是 <code>0x000000004bdefa50</code> 即可,脚本如下:</p>
<pre><code class="language-C#">
$$ $$&gt;a&lt; D:\debugging\18.20250506\src\Example\scripts\while2.txt
r @$t0 = 0x00000000006b0000 ;

r @$t1 = poi(@$t0+0x158) ; $$ Flink
r @$t2 = poi(@$t0+0x158+8); $$ Blink

.if (@$t1 == @$t2)
{
    .echo "@$t0+0x158 list is empty"
}
.else
{
    .echo "Walking @$t0+0x128 list..."
   
    r @$t3 = @$t0+0x158
    r @$t4 = @$t1
   
    .while (@$t4 != @$t3)
    {
      .printf "Entry at %p\n", @$t4      
      r @$t4 = poi(@$t4)
    }

    .echo "End of list reached"
}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/214741/202506/214741-20250617142206888-1174713677.png" alt="" loading="lazy"></p>
<p>哈哈,睁大眼睛看下卦哦,真的是 <code>000000004bdefa50</code>,而不是 <code>0x900000004bdefa50</code>,到这里基本就搞清楚了,由于地址的变化 <code>000000004bdefa50 -&gt; 0x900000004bdefa50</code> 导致 <code>双向链表</code> 断裂,当 RtlFreeHeap 在解码这个错误的内存地址时,导致程序的崩溃,同样你了解底层的话,你会发现用户态怎么可能会有 <code>0x900000004bdefa50</code> 这样的内存地址呢???</p>
<h3 id="4-为什么地址变化了">4. 为什么地址变化了</h3>
<p>这个问题真的问到我了,因为我也不知道为什么地址 <code>突变了</code>, 原因是我在 <code>000000004bde28c0</code> 周围也没发现有越界写入的情况,为啥高<code>31</code>和<code>28</code>位就硬生生的由0变1了,输出如下:</p>
<pre><code class="language-C#">
0:083&gt; dp 000000004bde28b0
00000000`4bde28b000000000`00000000 0001a08c`44153f2e
00000000`4bde28c000000000`4bdf79e0 90000000`4bdefa50
00000000`4bde28d000000000`00000080 00000000`00000000

0:083&gt; .formats 90000000`4bdefa50
Binary:10010000 00000000 00000000 00000000 01001011 11011110 11111010 01010000

</code></pre>
<p>最后就是这种问题该如何解决呢?只能开启 <code>页堆</code>,可以用 <code>Application Verifier</code> 工具,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202506/214741-20250617142206858-2121781584.png" alt="" loading="lazy"></p>
<p>能不能找到就看<code>个人造化</code>了,如何真的找不到,就当是神奇的 <code>bit位翻转</code> 吧,建议换机器尝试,或上 ECC 纠错的内存条。。。</p>
<h2 id="三总结">三:总结</h2>
<p>这次生产事故非常考察你对 Windows <code>堆管理器</code> 的深度理解,这块在我的训练营里有系统而深入的讲解,dump分析就是这样的有趣,各种迷惑和幻境,全靠扎实的底层功力和丰富的经验来冲出迷雾!<br>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"></p><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/18932886
頁: [1]
查看完整版本: 记一次 .NET 某发证机系统 崩溃分析