一:背景
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=0000000000c40000r8=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?scopeEXCEPTION_RECORD:??(.exr?-1)
ExceptionAddress:?00007ffbada0a8f9?(ntdll!RtlReportFatalFailure+0x0000000000000009)ExceptionCode:?c0000374ExceptionFlags:?00000001
NumberParameters:?1Parameter[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=0000000000c40000r8=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?freeor?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?0x00007ffbada7284800007ffbad9aef41:?ntdll!RtlpLogHeapFailure+0x4500007ffbad9be520:?ntdll!RtlFreeHeap+0x966e000007ffbaa3882bf:?KERNELBASE!LocalFree+0x2f00007ffb66fac78f:?mscorlib_ni+0x63c78f00007ffb66f273a4:?mscorlib_ni!System.Runtime.InteropServices.Marshal.FreeHGlobal+0x2400007ffb185c4fde:?+0x185c4fdeLFH?Key???????????????????:?0x1d4fd2a71d8b8280
Termination?on?corruption?:?ENABLEDHeap?????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?=?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>?r
rax=000000000000005b?rbx=0000000000005b08?rcx=0000000000000002
rdx=000000003103b690?rsi=0000000000000002?rdi=0000000000000000
rip=00007ffbad9b0544?rsp=000000003103b658?rbp=0000000000001da4r8=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. 回首再看源碼
仔細閱讀源碼,發現有兩個問題。
沒有對 localResource 加鎖處理,在并發的時候容易出現問題。
localResource 是一個類級別變量,在多個方法中被使用。
將信息反饋給朋友之后,建議朋友加鎖并降低 localResource 作用域。
三:總結
這次偶發的生產崩潰事故,主要原因是朋友的代碼在邏輯上出了點問題,沒有合理的保護好 localResource
句柄資源,反復釋放導致的 ntheap 破壞。
這個 dump 雖然問題比較小白,但逆向分析找出原因,還是挺考驗基本功的。