王大爷文明城市分爷 发表于 2023-6-27 14:04:33

记一次 .NET 某企业采购平台 崩溃分析

一:背景

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=0000000000000001r9=000000f7000006bd r10=0000000000f7d640
r11=0000000000f7cf50 r12=0000000000000001 r13=0000000000000001
r14=000000005c520126 r15=00000000013e3b10
iopl=0         nv up ei pl nz na po nc
cs=0033ss=002bds=002bes=002bfs=0053gs=002b             efl=00010204
clr!ComPreStubWorker+0xf2e54:
00007ffe`538e7044 f6403820      test    byte ptr ,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: 0000000000000000
   Parameter: 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=0000000000000001r9=000000f7000006bd r10=0000000000f7d640
r11=0000000000f7cf50 r12=0000000000000001 r13=0000000000000001
r14=000000005c520126 r15=00000000013e3b10
iopl=0         nv up ei pl nz na po nc
cs=0033ss=002bds=002bes=002bfs=0053gs=002b             efl=00010204
clr!ComPreStubWorker+0xf2e54:
00007ffe`538e7044 f6403820      test    byte ptr ,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
00007ffe`538e7019 488d81b8fffffflea   rax,
00007ffe`538e7020 48898424c0050000 mov   qword ptr ,rax
00007ffe`538e7028 48898424b8050000 mov   qword ptr ,rax
00007ffe`538e7030 488b89c0ffffffmov   rcx,qword ptr
00007ffe`538e7037 e8fcfcebff      call    clr!MethodTable::GetComCallWrapperTemplate (00007ffe`537a6d38)
00007ffe`538e703c 48898424b0050000 mov   qword ptr ,rax从汇编代码看,rax 是 clr!MethodTable::GetComCallWrapperTemplate 方法的返回值,从方法名字看是一个经典的 COM 和 .NET 互操作,接下来继续用 uf 反汇编看下这个方法,精简后的代码如下:
0:000> uf clr!MethodTable::GetComCallWrapperTemplate
00007ffe`537a6d38 48895c2408      mov   qword ptr ,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
...再结合 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
04 00000000`00f7d790 00007ffe`638a9c1e   ole32!PrivDragDrop+0x198
05 00000000`00f7d830 00007ffe`638ac239   ole32!CDragOperation::GetDropTarget+0xee
06 00000000`00f7d8b0 00007ffe`638ac91c   ole32!CDragOperation::UpdateTarget+0x4cd
07 00000000`00f7d9a0 00007ffe`2443f664   ole32!DoDragDrop+0x10c
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   wwkrn64C (export symbols)       wwkrn64.dll
    Loaded symbol image file: wwkrn64.dll
    Image path: D:\xxx\wwall\wwkrn64.dll
    Image name: wwkrn64.dll
    Browse all global symbolsfunctionsdata
    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. 安全软件为什么要介入

我这里只能简单推测一下,ComCallPreStub 和 ComPreStubWorker 方法是 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 ,r9 ss:0000009c`5efed218=0000000000000000
0:000> k
# Child-SP          RetAddr               Call Site
00 0000009c`5efed1f8 00007ffb`5917d683   clrjit!jitNativeCode
01 0000009c`5efed200 00007ffb`594d3091   clrjit!CILJit::compileMethod+0x83
02 (Inline Function) --------`--------   coreclr!invokeCompileMethodHelper+0x86
03 (Inline Function) --------`--------   coreclr!invokeCompileMethod+0xc5
04 0000009c`5efed270 00007ffb`594d274d   coreclr!UnsafeJitFunction+0x7f1
05 0000009c`5efed760 00007ffb`594d22ce   coreclr!MethodDesc::JitCompileCodeLocked+0x1f1
06 0000009c`5efed930 00007ffb`59472009   coreclr!MethodDesc::JitCompileCodeLockedEventWrapper+0x466
07 0000009c`5efeda90 00007ffb`59473f58   coreclr!MethodDesc::JitCompileCode+0x2a9
08 (Inline Function) --------`--------   coreclr!MethodDesc::PrepareILBasedCode+0x5ae
09 (Inline Function) --------`--------   coreclr!MethodDesc::PrepareCode+0x5ae
0a 0000009c`5efedb40 00007ffb`5947340c   coreclr!CodeVersionManager::PublishVersionableCodeIfNecessary+0x7f8
0b 0000009c`5efee070 00007ffb`5947316b   coreclr!MethodDesc::DoPrestub+0x16c
0c 0000009c`5efee190 00007ffb`595abec5   coreclr!PreStubWorker+0x21b
0d 0000009c`5efee320 00007ffa`f9a0296e   coreclr!ThePreStub+0x55
0e 0000009c`5efee3d0 00007ffb`595aae93   Example_19_1_1!Example_19_1_1.Program.Main+0x2e
...如果想在 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 的照护逻辑再优化下吧,减少误杀的发生。

来源:https://www.cnblogs.com/huangxincheng/archive/2023/06/27/17508154.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 记一次 .NET 某企业采购平台 崩溃分析