记一次 .NET 某企业采购平台 崩溃分析 比眉伴天荒 2024-03-17 00:28 21阅读 0赞 ### 一:背景 ### #### 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: 从输出信息看,果然是一个外来物种,经过网上一顿搜索,发现是一款 `信息安全软件`,哪家公司就模糊了哈,截图如下: ![71c89d48c9c815a40b569a9fcf854971.png][] 到这里就真相大白了,让朋友把这款软件卸载掉再试试看,问题就解决了。 #### 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` 方法的编译过程,看过程之前先上一张架构图: ![cc8dfbb82033e8ab36e20fcd30f1f74c.png][] 从架构图看 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 [71c89d48c9c815a40b569a9fcf854971.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/4786bd0c468b4491960bea41a643f807.png [cc8dfbb82033e8ab36e20fcd30f1f74c.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/266eff2bb5164de2a3f2c80c6874fd10.png
还没有评论,来说两句吧...