記一次 .NET 某企業採購平臺 崩潰分析

2023-06-27 12:00:45

一:背景

1. 講故事

前段時間有個朋友找到我,說他們的程式有偶發崩潰的情況,讓我幫忙看下怎麼回事,針對這種 crash 的程式,用 AEDebug 的方式抓取一個便知,有了 dump 之後接下來就可以分析了。

二:Windbg 分析

1. 為什麼會崩潰

既然是程式的崩潰,我們可以像看藍屏一下看dump檔案,使用 !analyze -v 命令即可。


0:000> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************
CONTEXT:  (.ecxr)
rax=0000000000000000 rbx=0000000000f7ccb0 rcx=00007ffe23af7ab0
rdx=00000000013e3b10 rsi=0000000000f7ccb0 rdi=0000000000f7c7c0
rip=00007ffe538e7044 rsp=0000000000f7cf60 rbp=0000000000f7d770
 r8=0000000000000001  r9=000000f7000006bd r10=0000000000f7d640
r11=0000000000f7cf50 r12=0000000000000001 r13=0000000000000001
r14=000000005c520126 r15=00000000013e3b10
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010204
clr!ComPreStubWorker+0xf2e54:
00007ffe`538e7044 f6403820        test    byte ptr [rax+38h],20h ds:00000000`00000038=??
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ffe538e7044 (clr!ComPreStubWorker+0x00000000000f2e54)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000001
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 0000000000000038
Attempt to read from address 0000000000000038

PROCESS_NAME:  xxx.exe

READ_ADDRESS:  0000000000000038 

ERROR_CODE: (NTSTATUS) 0xc0000005 - 0x%p            0x%p                    %s

EXCEPTION_CODE_STR:  c0000005

EXCEPTION_PARAMETER1:  0000000000000000

EXCEPTION_PARAMETER2:  0000000000000038

STACK_TEXT:  
00000000`00f7cf60 00007ffe`538e7044 clr!ComPreStubWorker+0xf2e54
00000000`00f7d590 00007ffe`53712d62 clr!ComCallPreStub+0x62
00000000`00f7d660 00007ffe`1de3ba83 wwkrn64+0xba83
00000000`00f7d740 00007ffe`638ebc70 ole32!CPrivDragDrop::PrivDragDrop+0x2b0
00000000`00f7d790 00007ffe`638eb98c ole32!PrivDragDrop+0x198
00000000`00f7d830 00007ffe`638a9c1e ole32!CDragOperation::GetDropTarget+0xee
00000000`00f7d8b0 00007ffe`638ac239 ole32!CDragOperation::UpdateTarget+0x4cd
....

從上面的資訊看,這個程式是一個經典的 存取違例 異常,違例是因為 rax=0 導致讀取了不該讀取的地方,接下來我們切到異常上下文看下為什麼會是 0 ?

2. eax 為什麼會是 0

要想切到異常上下文,先使用 .ecxr 命令,再使用 ub 反組合。


0:000> .ecxr
rax=0000000000000000 rbx=0000000000f7ccb0 rcx=00007ffe23af7ab0
rdx=00000000013e3b10 rsi=0000000000f7ccb0 rdi=0000000000f7c7c0
rip=00007ffe538e7044 rsp=0000000000f7cf60 rbp=0000000000f7d770
 r8=0000000000000001  r9=000000f7000006bd r10=0000000000f7d640
r11=0000000000f7cf50 r12=0000000000000001 r13=0000000000000001
r14=000000005c520126 r15=00000000013e3b10
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010204
clr!ComPreStubWorker+0xf2e54:
00007ffe`538e7044 f6403820        test    byte ptr [rax+38h],20h ds:00000000`00000038=??

0:000> ub 00007ffe`538e7044
clr!ComPreStubWorker+0xf2e20:
00007ffe`538e7010 0f8591d3f0ff    jne     clr!ComPreStubWorker+0x1b4 (00007ffe`537f43a7)
00007ffe`538e7016 488b0e          mov     rcx,qword ptr [rsi]
00007ffe`538e7019 488d81b8ffffff  lea     rax,[rcx-48h]
00007ffe`538e7020 48898424c0050000 mov     qword ptr [rsp+5C0h],rax
00007ffe`538e7028 48898424b8050000 mov     qword ptr [rsp+5B8h],rax
00007ffe`538e7030 488b89c0ffffff  mov     rcx,qword ptr [rcx-40h]
00007ffe`538e7037 e8fcfcebff      call    clr!MethodTable::GetComCallWrapperTemplate (00007ffe`537a6d38)
00007ffe`538e703c 48898424b0050000 mov     qword ptr [rsp+5B0h],rax

從組合程式碼看,rax 是 clr!MethodTable::GetComCallWrapperTemplate 方法的返回值,從方法名字看是一個經典的 COM 和 .NET 互操作,接下來繼續用 uf 反組合看下這個方法,精簡後的程式碼如下:


0:000> uf clr!MethodTable::GetComCallWrapperTemplate
00007ffe`537a6d38 48895c2408      mov     qword ptr [rsp+8],rbx
00007ffe`537a6d3d 57              push    rdi
...
00007ffe`537a6d57 488bcb          mov     rcx,rbx
00007ffe`537a6d5a e8c943f7ff      call    clr!MethodTable::GetClass (00007ffe`5371b128)
00007ffe`537a6d5f 488b4030        mov     rax,qword ptr [rax+30h]
...

再結合 coreclr 原始碼:


inline ComCallWrapperTemplate *MethodTable::GetComCallWrapperTemplate()
{
    LIMITED_METHOD_CONTRACT;
    return GetClass()->GetComCallWrapperTemplate();
}

class EEClass // DO NOT CREATE A NEW EEClass USING NEW!
{
    ComCallWrapperTemplate* m_pccwTemplate;   // points to interop data structures used when this type is exposed to COM

    inline ComCallWrapperTemplate *GetComCallWrapperTemplate()
    {
        LIMITED_METHOD_CONTRACT;
        return m_pccwTemplate;
    }
}

到這裡大概能推測到是因為 EEClass.m_pccwTemplate 欄位為 null 所致,從註釋看,他是 CLR 用來暴露給 COM 使用的資料結構,那為什麼暴露給 COM 使用的資料結構為 NULL 呢? 這個分析起來就複雜了。

但有一點可以確定,像這種邏輯必然是 堅如磐石,受過日月精華,經歷過500年的風吹雨打,不可能無緣無故的出簍子。

3. 出路在哪裡

要尋找突破口還得從呼叫棧入手,我們用 k 命令洞察一下。


0:000> k
  *** Stack trace for last set context - .thread/.cxr resets it
 # Child-SP          RetAddr               Call Site
00 00000000`00f7cf60 00007ffe`53712d62     clr!ComPreStubWorker+0xf2e54
01 00000000`00f7d590 00007ffe`1de3ba83     clr!ComCallPreStub+0x62
02 00000000`00f7d660 00007ffe`638ebc70     wwkrn64+0xba83
03 00000000`00f7d740 00007ffe`638eb98c     ole32!CPrivDragDrop::PrivDragDrop+0x2b0 [com\ole32\com\rot\getif.cxx @ 659] 
04 00000000`00f7d790 00007ffe`638a9c1e     ole32!PrivDragDrop+0x198 [com\ole32\com\rot\getif.cxx @ 920] 
05 00000000`00f7d830 00007ffe`638ac239     ole32!CDragOperation::GetDropTarget+0xee [com\ole32\ole232\drag\drag.cpp @ 1128] 
06 00000000`00f7d8b0 00007ffe`638ac91c     ole32!CDragOperation::UpdateTarget+0x4cd [com\ole32\ole232\drag\drag.cpp @ 2026] 
07 00000000`00f7d9a0 00007ffe`2443f664     ole32!DoDragDrop+0x10c [com\ole32\ole232\drag\drag.cpp @ 3007] 
08 00000000`00f7dc80 00007ffe`244ccd8d     System_Windows_Forms_ni+0x9cf664
...

仔細觀察執行緒棧資訊,不難發現使用者是在用 DoDragDrop 方法實現控制元件的拖拽,不過在執行流中有一個陌生的動態連結庫 wwkrn64,它到底是何方神聖呢?我們用 lmvm 觀察下。


0:000> lmvm wwkrn64
Browse full module list
start             end                 module name
00007ffe`1de30000 00007ffe`1df1e000   wwkrn64  C (export symbols)       wwkrn64.dll
    Loaded symbol image file: wwkrn64.dll
    Image path: D:\xxx\wwall\wwkrn64.dll
    Image name: wwkrn64.dll
    Browse all global symbols  functions  data
    Timestamp:        Wed Apr 26 10:18:26 2023 (644889F2)
    CheckSum:         00000000
    ImageSize:        000EE000
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4
    Information from resource tables:

從輸出資訊看,果然是一個外來物種,經過網上一頓搜尋,發現是一款 資訊保安軟體,哪家公司就模糊了哈,截圖如下:

到這裡就真相大白了,讓朋友把這款軟體解除安裝掉再試試看,問題就解決了。

4. 安全軟體為什麼要介入

我這裡只能簡單推測一下,ComCallPreStubComPreStubWorker 方法是 JIT 在編譯某一個方法時的字首路徑,也是很多 加殼軟體 以及 永恆之藍 這樣的蠕蟲病毒重點關注的方法,所以這些高危方法自然也是 安全軟體 重點監視的,如果 安全軟體 沒處理好,自然就會誤殺。。。

當然真正的原因只能問 繫鈴人

可能有些朋友要說了,怎麼驗證這兩個方法就是 JIT 編譯的字首,這裡我們用 普通方法+windbg 的方法簡單驗證下吧,參考程式碼如下:


    internal class Program
    {
        static void Main(string[] args)
        {
            Debugger.Break();
            Test();
            Console.ReadLine();
        }

        static void Test()
        {
            Console.WriteLine("Test1");
        }
    }

接下來我們重點觀察下 Test 方法的編譯過程,看過程之前先上一張架構圖:

從架構圖看 Test() 方法的編譯最終是由 clrjit!jitNativeCode 來處理的,要想驗證很簡單用 bp clrjit!jitNativeCode 下一個斷點即可。


0:000> bp clrjit!jitNativeCode
0:000> g
Breakpoint 0 hit
clrjit!jitNativeCode:
00007ffb`590cc040 4c894c2420      mov     qword ptr [rsp+20h],r9 ss:0000009c`5efed218=0000000000000000
0:000> k
 # Child-SP          RetAddr               Call Site
00 0000009c`5efed1f8 00007ffb`5917d683     clrjit!jitNativeCode [D:\a\_work\1\s\src\coreclr\jit\compiler.cpp @ 6941] 
01 0000009c`5efed200 00007ffb`594d3091     clrjit!CILJit::compileMethod+0x83 [D:\a\_work\1\s\src\coreclr\jit\ee_il_dll.cpp @ 279] 
02 (Inline Function) --------`--------     coreclr!invokeCompileMethodHelper+0x86 [D:\a\_work\1\s\src\coreclr\vm\jitinterface.cpp @ 12774] 
03 (Inline Function) --------`--------     coreclr!invokeCompileMethod+0xc5 [D:\a\_work\1\s\src\coreclr\vm\jitinterface.cpp @ 12839] 
04 0000009c`5efed270 00007ffb`594d274d     coreclr!UnsafeJitFunction+0x7f1 [D:\a\_work\1\s\src\coreclr\vm\jitinterface.cpp @ 13355] 
05 0000009c`5efed760 00007ffb`594d22ce     coreclr!MethodDesc::JitCompileCodeLocked+0x1f1 [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 1051] 
06 0000009c`5efed930 00007ffb`59472009     coreclr!MethodDesc::JitCompileCodeLockedEventWrapper+0x466 [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 920] 
07 0000009c`5efeda90 00007ffb`59473f58     coreclr!MethodDesc::JitCompileCode+0x2a9 [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 860] 
08 (Inline Function) --------`--------     coreclr!MethodDesc::PrepareILBasedCode+0x5ae [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 439] 
09 (Inline Function) --------`--------     coreclr!MethodDesc::PrepareCode+0x5ae [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 332] 
0a 0000009c`5efedb40 00007ffb`5947340c     coreclr!CodeVersionManager::PublishVersionableCodeIfNecessary+0x7f8 [D:\a\_work\1\s\src\coreclr\vm\codeversion.cpp @ 1701] 
0b 0000009c`5efee070 00007ffb`5947316b     coreclr!MethodDesc::DoPrestub+0x16c [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 2215] 
0c 0000009c`5efee190 00007ffb`595abec5     coreclr!PreStubWorker+0x21b [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 2039] 
0d 0000009c`5efee320 00007ffa`f9a0296e     coreclr!ThePreStub+0x55
0e 0000009c`5efee3d0 00007ffb`595aae93     Example_19_1_1!Example_19_1_1.Program.Main+0x2e [D:\skyfly\19.20230624\src\Example\Example_19_1_1\Program.cs @ 10] 
...

如果想在 jitNativeCode 方法中把 md 提取出來的話,可以取 r9 引數。


0:000> !dumpmd poi(r9)
Method Name:          Example_19_1_1.Program.Test()
Class:                00007ffaf9abd520
MethodTable:          00007ffaf9ac8880
mdToken:              0000000006000006
Module:               00007ffaf9ac6908
IsJitted:             no
Current CodeAddr:     ffffffffffffffff
Version History:
  ILCodeVersion:      0000000000000000
  ReJIT ID:           0
  IL Addr:            000001f865be20a7
     CodeAddr:           0000000000000000  (MinOptJitted)
     NativeCodeVersion:  0000000000000000

三:總結

這次崩潰事故的直接原因是由於第三方安全軟體的介入導致的,因 ComPreStubWorker 是加殼程式和蠕蟲病毒注入的突破口,不管怎樣還是希望安全軟體對高危函數 ComPreStubWorker 的照護邏輯再優化下吧,減少誤殺的發生。

圖片名稱