概述

金蝶 K3Cloud 用户导出对账单 Excel 时 w3wp 崩溃(c0000005)。完整还原:FarPoint 控件许可证加载用 BinaryFormatter 反序列化 → 进程内 Hook 引擎的安全 Binder 拦截 → P/Invoke 配置检查 → 触发 CLR CAS 声明式权限栈走查 → clr!Assembly::GetSecurityDescriptor 解引用空指针。崩溃点在 CLR 内部(CLR bug),用 IDA 把 NULL 路径在汇编层钉死。

关键字:#调试技术/WinDbg 调试技术/dump 编程语言/dotnet/clr 逆向工程/ida iis/w3wp cas

一、现场

项值
进程w3wp.exe(IIS 工作进程),194 托管线程
崩溃线程#57(OSID 0x49d4)
异常码c0000005 读取 AV,读地址 0x70
崩溃指令mov rax, qword ptr [rax+70h],rax=0
CLR4.8.4795.0(clr.dll 时间戳 6837CE52,.NET 4.8 早期 RTM 批次,未打最新 CU)
业务金蝶 K3Cloud,应收应付(ARAP)对账单导出
Dumpw3wp.dmp(4.3 GB)

一句话结论:金蝶 K3Cloud 用户导出对账单 Excel 时,FarPoint 控件许可证加载使用 BinaryFormatter 反序列化,进程内的反序列化防护 Hook(安全 Binder)拦截检查 → P/Invoke 配置检查 → 触发 CLR CAS 声明式权限栈走查 → CLR 内部 clr!Assembly::GetSecurityDescriptor 解引用空指针崩溃(c0000005)。崩溃点在 CLR 内部(CLR bug 性质),但触发链由 Hook 引入。不是攻击行为,是正常业务操作触发。

二、业务场景完整还原(自顶向下)

这是本次分析最有价值的部分——完整的用户操作 → 崩溃链路:

PlantUML diagram

关键认知:

  1. 触发源是 GrapeCity/FarPoint 控件的许可证机制(license 数据用 BinaryFormatter 序列化,内含 Hashtable),不是攻击流量
  2. 进程内的反序列化防护 Hook 是挂给 BinaryFormatter 的安全 Binder,任何 BindToType 都会走配置检查——license 反序列化这种”无害绑定”也会进检查流程
  3. 白名单检查本身正是为了避免误报,但检查动作(P/Invoke)引发了崩溃

崩溃点证据

异常记录与寄存器现场

text
ExceptionAddress: 00007ffb57aaa8eb(clr 模块内,后经 PDB 修为 GetSecurityDescriptor+0x2b)
ExceptionCode: c0000005 (Access violation)
Attempt to read from address 0000000000000070
 
rax=0000000000000000  rbx=000002a6feaa4c40
rip=00007ffb57aaa8eb
clr!Assembly::GetSecurityDescriptor+0x2b:
00007ffb`57aaa8eb 488b4070  mov rax,qword ptr [rax+70h]  ds:00000000`00000070=???
  • rbx = Assembly 对象(位于第二 AppDomain 地址段)
  • 崩溃指令 mov rax,[rax+70h],rax=0 → 读空指针 +0x70

IDA 反编译定案

Assembly::GetSecurityDescriptor @ 0x18005a8bc(与 dump 现场 CLR 同版本 4.8.4795):

asm
18005a8d1  mov   rcx, [rbx+58h]        ; rcx = this->Module
18005a8d8  mov   r8,  [rcx+30h]
18005a8dc  cmp   [r8+58h], rcx         ; Module back-pointer 一致性检查
18005a8e0  jnz   → FindDomainAssembly  ; 非默认模块走域程序集查找
18005a8e6  call  Module::FindDomainFile
18005a8eb  mov   rax, [rax+70h]        ; 💥 崩溃指令,rax = FindDomainFile 返回值,无判空

反编译伪代码:

Cc
return *((IAssemblySecurityDescriptor **)DomainFile + 14);   // +14×8 = +0x70 ✅

+14(指针数组下标)× 8 = +0x70,与 dump 现场(rax=0、读地址 0x70)逐项吻合——确认 dump 现场崩溃指令与 4.8.4795 版本该函数完全一致。

Module::FindDomainFile 的两条 NULL 路径(汇编钉死)

Module::FindDomainFile @ 0x18000e968:

asm
18000e968  mov   r9, [rcx+568h]
18000e96f  test  r9b, 1
18000e973  jz    → 单域快速路径
; ── 多域模式(Module+0x568 bit0=1)──
18000e979  mov   rax, [rcx+570h]        ; 域索引
18000e983  cmp   rax, [rdx+20h]         ; 与 AppDomain 的 assembly 列表长度比较
18000e987  jnb   → return 0             ; 路径①:索引越界 → NULL
18000e98e  mov   rcx, [rdx+18h]         ; assembly 列表
18000e992  mov   rdx, [rcx+rax*8]
18000e996  test  rdx, rdx
18000e999  jz    → return 0             ; 路径①:槽位为空 → NULL
; ── 单域快速路径 ──
18018a7de  mov   rax, [rcx+30h]         ; Module 所属域对象
18018a7e2  cmp   rdx, [rax+40h]
18018a7e6  jz    → 返回绑定的 DomainFile
18018a7ec  xor   eax, eax               ; 路径②:传入域 ≠ 模块所属域 → 直接 NULL

两条 NULL 路径:

  1. 多域模式:该 Module 的域索引超出目标 AppDomain 的 assembly 列表长度,或列表槽位为空(该域中此程序集尚未注册 DomainFile)
  2. 单域模式:传入的 AppDomain ≠ Module 所属域 → 不查列表,直接返回 NULL(模块根本不在这个域里)

托管栈头部:GC 保护帧与安全标记帧

崩溃点正上方的 !clrstack 原始输出(OS Thread Id: 0x49d4 (57)):

text
000000c4786fa968 00007ffb57aaa8eb [GCFrame: 000000c4786fa968]
000000c4786faab8 00007ffb57aaa8eb [GCFrame: 000000c4786faab8]
000000c4786fab38 00007ffb57aaa8eb [DebuggerSecurityCodeMarkFrame: 000000c4786fab38]
000000c4786fabf8 00007ffb57aaa8eb [HelperMethodFrame: 000000c4786fabf8] System.StubHelpers.StubHelpers.DemandPermission(IntPtr)
000000c4786fae10 00007ffaf85a7873 [InlinedCallFrame: 000000c4786fae10] DnrspEngine.HookLib.Api64.CheckHookConfig(...)

DebuggerSecurityCodeMarkFrame 的意义:CLR 在进入安全敏感路径(本例为 P/Invoke 声明式 CAS demand 的全栈走查)时压入的安全代码标记帧,是 SecurityStackWalk::SpecialDemand 全栈走查在托管栈上的直接痕迹,与下方原生栈 SpecialDemand → Check_PLS_SW → DoStackWalk → WalkFrame 链一一对应。其上两个 GCFrame 是 StubHelpers.DemandPermission 走查前的 GC 保护帧。三个帧的 IP 全部等于崩溃地址 00007ffb57aaa8eb(崩溃发生在走查回调内部)。

关键托管栈(!clrstack -p 带参数)

崩溃线程自崩溃点向外的托管栈完整链(节选关键帧,Hook 引擎名做了泛化):

text
[GCFrame]
[GCFrame]
[DebuggerSecurityCodeMarkFrame]                ← CAS 走查安全标记帧
[HelperMethodFrame] System.StubHelpers.StubHelpers.DemandPermission(IntPtr)
[InlinedCallFrame] HookLib.Api64.CheckHookConfig(...)          ← P/Invoke 配置检查
[IL_STUB_PInvoke] DomainBoundILStubClass.IL_STUB_PInvoke(...)
ConfigManager.CheckConfig(...)                                 ← Binder 回调内的白名单检查
DnrspBinaryFormatter+SecureSerializationBinder.BindToType(String, String)
    assemblyName = "mscorlib, Version=4.0.0.0, ..."
    typeName = "System.Collections.Hashtable"
System.Runtime.Serialization.Formatters.Binary.ObjectReader.Bind(String, String)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream)
GrapeCity.Licenses.PowerToolsLicenseProvider.DeserializeLicenseStream(...)
System.ComponentModel.LicenseManager.Validate(Type, Object)
FarPoint.Win.Spread.FpSpread..ctor()
Kingdee.BOS.ExcelToolHelper.ExcelHelper..ctor(IDynamicFormView)
Kingdee.K3.FIN.Business.PlugIn.ARAP.Statement.ExportData()
Kingdee.BOS.Web.DynamicForm.DynamicWebFormView.MainBarItemClick(String)
[HelperMethodFrame_PROTECTOBJ] System.RuntimeMethodHandle.InvokeMethod(...)   ← 反射②
Kingdee.BOS.ServiceFacade.ServicesStub.DynamicForm.DynamicFormService.Call(String, ...)
...(System.Web / ASP.NET 请求处理帧)

原生栈关键段

text
clr!Assembly::GetSecurityDescriptor+0x2b          ← 崩溃点
clr!DemandStackWalk::WalkFrame+0xfb
clr!Thread::StackWalkFramesEx+0x10d
clr!Thread::StackWalkFrames+0xab
clr!DemandStackWalk::DoStackWalk+0x8d
clr!SecurityStackWalk::Check_StackWalk+0x10d
clr!SecurityStackWalk::SpecialDemand+0x142b0
clr!DoDeclarativeActionsForPInvoke+0x72
clr!StubHelpers::DemandPermission+0x262b15
...(托管帧:P/Invoke stub + Binder 回调)
...(业务帧 + System.Web 请求处理)

关于”两段重复 mscorlib 反射帧”的修正:托管栈参数出来后确认,这不是 Hook 重入——两段 RuntimeMethodHandle.InvokeMethod 分别是 ServiceFacade 反射调插件方法和 K3Cloud 服务框架外层反射调用,属于框架的正常反射模式。Hook 的 Binder 只进了一层(BindToType 一帧)。

机制定性

层级结论置信度
直接死因FindDomainFile 返回 NULL → CLR GetSecurityDescriptor 未判空解引用✅ 已证实(反汇编+寄存器)
崩溃责任CLR 内部缺陷(4.8.4795 早期版本),不是 Hook 引擎 native 代码自身 AV✅ 已证实
触发链Binder Hook → 配置检查 P/Invoke → P/Invoke 声明式 CAS demand → 全栈走查踩中 CLR 缺陷✅ 已证实(完整托管栈+参数)
业务触发源K3Cloud 对账单导出 → FarPoint license 的 BinaryFormatter 反序列化(正常业务)✅ 已证实
DomainFile 为 NULL 的底层场景两条路径已在汇编层钉死(见上):①多域模式域索引越界/槽位空;②传入域≠模块所属域。具体踩中哪条属推断⚠️ 路径已证实,现场归因推断
P/Invoke 隐式 CAS demand 为触发入口DoDeclarativeActionsForPInvoke 反编译证实 MSD 存在才走 SpecialDemand,[SuppressUnmanagedCodeSecurity] 可整条绕开✅ 已证实(IDA 反编译)
clr.dll 4.8.4795 后续 CU 有修复需对照微软 KB/更新历史核实⚠️ 待核实

修复建议(按优先级)

  1. Hook 引擎代码(根治):配置检查的 P/Invoke 声明加 [SuppressUnmanagedCodeSecurity],hook 引擎程序集标记 SecurityCritical / 授予完全信任 → 消除 DoDeclarativeActionsForPInvoke 声明式 demand 路径,走查不再发生
  2. 配置预载(更彻底):启动时把 hook 配置预载到托管侧缓存(静态字典 / MMF),BindToType 回调内不再每次 P/Invoke
  3. 白名单短路(低成本止血):mscorlib 等系统程序集 + System.Collections.Hashtable 这类 BCL 基础类型在托管侧直接放行,不进 native 检查(检测目标是业务类型反序列化,BCL 类型本就该放)
  4. 宿主侧:建议客户升级 .NET Framework 4.8 到最新 CU,修复 CLR 自身缺陷;升级前可用 IDA 对照新版 clr.dll 的 Assembly::GetSecurityDescriptor 是否已加判空
  5. 防重入加固(次要):BindToType 处理期间避免再触发反射加载路径

AppDomain 观察

!threads 显示进程存在两个 AppDomain:

  • 默认域(多数线程)
  • 第二域(崩溃线程的多个对象、Hook Binder this 都在此地址段)

结合崩溃点 FindDomainFile 返回 NULL:CAS 走查回溯经过 P/Invoke ILStub 帧时,stub 所属程序集在其查找的 Domain 中无 DomainFile 绑定(域边界/动态 stub 交互问题)。进程内多 AppDomain(ASP.NET 域隔离)放大了此 CLR 缺陷的触发面。

给同类 Hook 引擎开发者的启示

  1. Binder 回调内避免 P/Invoke:BinaryFormatter 的 BindToType 回调发生在序列化管线深处,此处任何 P/Invoke 都会触发 StubHelpers.DemandPermission → 声明式 CAS 全栈走查(哪怕目标 native 函数无权限要求)——既可能踩 CLR 缺陷,也有性能损耗
  2. 配置预载:把检查所需数据在启动时预载到托管侧,回调内纯托管判定
  3. BCL 类型白名单前置:mscorlib 基础类型绑定在托管侧直接放行,不进 native 检查
  4. [SuppressUnmanagedCodeSecurity]:完全信任的 hook 引擎程序集可对 P/Invoke 声明此特性,整条绕开声明式 demand(需引擎自身代码可信)

符号环境修复记录(复现分析必读)

本机 SOS 4.8.9337 与 dump CLR 4.8.4795 不匹配 → 托管命令全挂;msdl 在线下载失败。最终用本地符号库解决:

text
# 目录准备(一次性)
D:\symbols\dacflat   ← 拍平 mscordacwks_AMD64_AMD64_4.8.4795.00.dll + SOS_AMD64_AMD64_4.8.4795.00.dll
D:\symbols\flat      ← 拍平 clr.pdb(匹配 GUID {9C151594-ADB5-449D-922C-8A661D9B47A3} age 2)
D:\symbols\pingme.txt ← 使 srv 格式 store 校验通过
 
# 每次新 cdb 会话必跑的四条(批量脚本每轮自动执行)
.sympath D:\symbols\flat
.reload /f clr.dll
.cordll -lp D:\symbols\dacflat
.load D:\symbols\dacflat\SOS_AMD64_AMD64_4.8.4795.00.dll

后续动作

  • 核实 clr.dll 4.8.4795 相关 CLR 修复 KB(待办)
  • 若形成代码改动([SuppressUnmanagedCodeSecurity] / 配置预载),按需求流程立项

相关参考