记一次 .NET 某发证机系统 崩溃分析
一:背景
1. 講故事
前些天有位朋友在微信上找到我,說他的系統有偶發崩潰,自己也沒找到原因,讓我幫忙看下怎么回事,我分析dump一直都是免費的,畢竟對這些東西挺感興趣,有問題可以直接call我,好了,接下來我們就來分析dump吧。
二:程序為什么會崩
1. 觀察崩潰上下文
windbg有一個厲害之處在于雙擊dump之后會自動定位到崩潰的線程,然后通過 .ecxr; k10 命令就可以看到崩潰點了,輸出如下:
0:083> .ecxr; k10
rax=000000004bdefa50 rbx=00000c45ea960c80 rcx=000000ffffffffff
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=00000000773da365 rsp=0000000046d0fb50 rbp=000000004bde28d0
r8=000000004bde28c0 r9=0000000000000001 r10=0000000000000000
r11=0000000000000206 r12=00000000006b0000 r13=000000004bde2950
r14=0000000000000000 r15=0000000000000001
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
ntdll!RtlFreeHeap+0x1a5:
00000000`773da365 488b7b08 mov rdi,qword ptr [rbx+8] 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
從卦中可以看到,程序崩潰在 RtlFreeHeap 函數中,熟悉這玩意的朋友應該知道它是用來釋放 堆塊 的,簽名如下:
NTSYSAPI LOGICAL RtlFreeHeap(
[in] PVOID HeapHandle,
[in, optional] ULONG Flags,
_Frees_ptr_opt_ PVOID BaseAddress
);
接下來就是尋找該堆塊的首地址 BaseAddress,即 r8 寄存器值,使用 uf ntdll!RtlFreeHeap 即可,輸出如下:
0:083> 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 [r8+8]
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 48331d3a321000 xor rbx,qword ptr [ntdll!RtlpLFHKey (00000000`774dd598)]
00000000`773da35e 48c1e304 shl rbx,4
00000000`773da362 0f0d0b prefetchw [rbx]
00000000`773da365 488b7b08 mov rdi,qword ptr [rbx+8]
根據卦中的匯編代碼 mov rbp,r8 ,看樣子是 rbp 保存了 BaseAddress 地址,接下來使用 !heap -x 000000004bde28d0 看看到底啥情況,輸出如下:
0:083> !heap -x 000000004bde28d0
SEGMENT HEAP ERROR: failed to initialize the extention
List corrupted: (Blink->Flink = 0000000000000000) != (Block = 000000004bde28c0)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bde28b0 Error: block list entry corrupted
List corrupted: (Flink->Blink = 900000004bdefa50) != (Block = 000000004bdefa50)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bdefa40 Error: block list entry corrupted
Entry User Heap Segment Size PrevSize Unused Flags
-------------------------------------------------------------------------------------------------------------
000000004bde28b0 000000004bde28c0 00000000006b0000 000000004bde0000 3740 800 0 free
從卦中可以看到,當前的 000000004bde28c0 是一個 free 堆塊,同時也拋了一個 堆塊列表 的損壞錯誤,這就有點意思了。。。
2. doublefree 導致的嗎
熟悉 win32 的朋友應該知道,在已經 free 的塊上再次調用 RtlFreeHeap 釋放會是一個經典的 doublefree,貌似這個問題已經定位了。。。但如果你修車經驗豐富的話,你應該知道 堆管理器 檢測到 doublefree 的時候會是這樣的調用棧記一次 .NET 某醫療住院系統 崩潰分析, 參考如下:
0:090> !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
...
而我們這個dump是訪問違例,雖然這個dump必崩無疑,但這段邏輯此時還沒執行到,也就是在這塊邏輯之前就崩掉了,那為什么會崩掉呢?到底經歷了何樣的驚魂時刻。。。
3. 突破口在哪里
要想尋找突破口,就得理解下面的這兩句了,再次輸出一下吧。
List corrupted: (Blink->Flink = 0000000000000000) != (Block = 000000004bde28c0)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bde28b0 Error: block list entry corrupted
List corrupted: (Flink->Blink = 900000004bdefa50) != (Block = 000000004bdefa50)
HEAP 00000000006b0000 (Seg 000000004bde0000) At 000000004bdefa40 Error: block list entry corrupted
要理解這個,需要你對 _HEAP 結構有一個深度的理解,這個在我的 .NET高級調試訓練營 里有一個系統的解讀。
大家要知道 堆管理器 對 Free 的管理采用的是 雙向鏈表 的方式,其中 Flink 表示下一個(Forward)節點,BLink 表示前一個(Backward)結點,有朋友搞不清的話,我畫個簡圖。
有了簡圖之后,接下來逐個解讀下:
- (Blink->Flink = 0000000000000000) != (Block = 000000004bde28c0)
這是經典的 一去一回 ,結果發現不再是自己了。。。即 0000000000000000 != 000000004bde28c0,我們用 ntdll!_LIST_ENTRY 來具象化一下,截圖如下:
從卦中可以看到 Blink = 0x900000004bdefa50 時就報錯了,由于報錯windbg 就將結果顯示為 0000000000000000,所以這一來一回居然不等于自己,所以堆管理器就覺得很奇葩。。。
- (Flink->Blink = 900000004bdefa50) != (Block = 000000004bdefa50)
這是經典的 一回一去,結果發現 Blink 不對。。。本來應該是 0x000000004bdefa50 結果是 0x900000004bdefa50 截圖如下:
到這里我相信很多人會有一個疑問,我也沒看到 0x000000004bdefa50 地址呀,憑什么說0x900000004bdefa50 的前身是 0x000000004bdefa50 ?
要想找到這個答案,又是考你的 堆管理器 知識,所有的 free 都掛在 FreeLists 字段里,同樣也是采用 雙向鏈表 的方式,輸出如下:
0:083> dt nt!_HEAP 00000000006b0000 -y FreeLists
ntdll!_HEAP
+0x158 FreeLists : _LIST_ENTRY [ 0x00000000`1f1d7f90 - 0x00000000`1f23dbf0 ]
接下來寫一段簡單的腳本把這個list給遍歷下,看看 0x000000004bde28c0 的BLink結點是不是 0x000000004bdefa50 即可,腳本如下:
$$ $$>a< 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"
}
哈哈,睜大眼睛看下卦哦,真的是 000000004bdefa50,而不是 0x900000004bdefa50,到這里基本就搞清楚了,由于地址的變化 000000004bdefa50 -> 0x900000004bdefa50 導致 雙向鏈表 斷裂,當 RtlFreeHeap 在解碼這個錯誤的內存地址時,導致程序的崩潰,同樣你了解底層的話,你會發現用戶態怎么可能會有 0x900000004bdefa50 這樣的內存地址呢???
4. 為什么地址變化了
這個問題真的問到我了,因為我也不知道為什么地址 突變了, 原因是我在 000000004bde28c0 周圍也沒發現有越界寫入的情況,為啥高31和28位就硬生生的由0變1了,輸出如下:
0:083> dp 000000004bde28b0
00000000`4bde28b0 00000000`00000000 0001a08c`44153f2e
00000000`4bde28c0 00000000`4bdf79e0 90000000`4bdefa50
00000000`4bde28d0 00000000`00000080 00000000`00000000
0:083> .formats 90000000`4bdefa50
Binary: 10010000 00000000 00000000 00000000 01001011 11011110 11111010 01010000
最后就是這種問題該如何解決呢?只能開啟 頁堆,可以用 Application Verifier 工具,截圖如下:
能不能找到就看個人造化了,如何真的找不到,就當是神奇的 bit位翻轉 吧,建議換機器嘗試,或上 ECC 糾錯的內存條。。。
三:總結
這次生產事故非常考察你對 Windows 堆管理器 的深度理解,這塊在我的訓練營里有系統而深入的講解,dump分析就是這樣的有趣,各種迷惑和幻境,全靠扎實的底層功力和豐富的經驗來沖出迷霧!
總結
以上是生活随笔為你收集整理的记一次 .NET 某发证机系统 崩溃分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 宝宝歪头与斜颈的区别
- 下一篇: 为啥半夜会醒!