概述
金蝶 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 |
| CLR | 4.8.4795.0(clr.dll 时间戳 6837CE52,.NET 4.8 早期 RTM 批次,未打最新 CU) |
| 业务 | 金蝶 K3Cloud,应收应付(ARAP)对账单导出 |
| Dump | w3wp.dmp(4.3 GB) |
一句话结论:金蝶 K3Cloud 用户导出对账单 Excel 时,FarPoint 控件许可证加载使用 BinaryFormatter 反序列化,进程内的反序列化防护 Hook(安全 Binder)拦截检查 → P/Invoke 配置检查 → 触发 CLR CAS 声明式权限栈走查 → CLR 内部 clr!Assembly::GetSecurityDescriptor 解引用空指针崩溃(c0000005)。崩溃点在 CLR 内部(CLR bug 性质),但触发链由 Hook 引入。不是攻击行为,是正常业务操作触发。
二、业务场景完整还原(自顶向下)
这是本次分析最有价值的部分——完整的用户操作 → 崩溃链路:
关键认知:
- 触发源是 GrapeCity/FarPoint 控件的许可证机制(license 数据用 BinaryFormatter 序列化,内含 Hashtable),不是攻击流量
- 进程内的反序列化防护 Hook 是挂给 BinaryFormatter 的安全 Binder,任何
BindToType都会走配置检查——license 反序列化这种”无害绑定”也会进检查流程 - 白名单检查本身正是为了避免误报,但检查动作(P/Invoke)引发了崩溃
崩溃点证据
异常记录与寄存器现场
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):
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 返回值,无判空反编译伪代码:
return *((IAssemblySecurityDescriptor **)DomainFile + 14); // +14×8 = +0x70 ✅+14(指针数组下标)× 8 = +0x70,与 dump 现场(rax=0、读地址 0x70)逐项吻合——确认 dump 现场崩溃指令与 4.8.4795 版本该函数完全一致。
Module::FindDomainFile 的两条 NULL 路径(汇编钉死)
Module::FindDomainFile @ 0x18000e968:
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 路径:
- 多域模式:该 Module 的域索引超出目标 AppDomain 的 assembly 列表长度,或列表槽位为空(该域中此程序集尚未注册 DomainFile)
- 单域模式:传入的 AppDomain ≠ Module 所属域 → 不查列表,直接返回 NULL(模块根本不在这个域里)
托管栈头部:GC 保护帧与安全标记帧
崩溃点正上方的 !clrstack 原始输出(OS Thread Id: 0x49d4 (57)):
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 引擎名做了泛化):
[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 请求处理帧)原生栈关键段
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/更新历史核实 | ⚠️ 待核实 |
修复建议(按优先级)
- Hook 引擎代码(根治):配置检查的 P/Invoke 声明加
[SuppressUnmanagedCodeSecurity],hook 引擎程序集标记 SecurityCritical / 授予完全信任 → 消除DoDeclarativeActionsForPInvoke声明式 demand 路径,走查不再发生 - 配置预载(更彻底):启动时把 hook 配置预载到托管侧缓存(静态字典 / MMF),
BindToType回调内不再每次 P/Invoke - 白名单短路(低成本止血):mscorlib 等系统程序集 +
System.Collections.Hashtable这类 BCL 基础类型在托管侧直接放行,不进 native 检查(检测目标是业务类型反序列化,BCL 类型本就该放) - 宿主侧:建议客户升级 .NET Framework 4.8 到最新 CU,修复 CLR 自身缺陷;升级前可用 IDA 对照新版 clr.dll 的
Assembly::GetSecurityDescriptor是否已加判空 - 防重入加固(次要):BindToType 处理期间避免再触发反射加载路径
AppDomain 观察
!threads 显示进程存在两个 AppDomain:
- 默认域(多数线程)
- 第二域(崩溃线程的多个对象、Hook Binder this 都在此地址段)
结合崩溃点 FindDomainFile 返回 NULL:CAS 走查回溯经过 P/Invoke ILStub 帧时,stub 所属程序集在其查找的 Domain 中无 DomainFile 绑定(域边界/动态 stub 交互问题)。进程内多 AppDomain(ASP.NET 域隔离)放大了此 CLR 缺陷的触发面。
给同类 Hook 引擎开发者的启示
- Binder 回调内避免 P/Invoke:
BinaryFormatter的BindToType回调发生在序列化管线深处,此处任何 P/Invoke 都会触发StubHelpers.DemandPermission→ 声明式 CAS 全栈走查(哪怕目标 native 函数无权限要求)——既可能踩 CLR 缺陷,也有性能损耗 - 配置预载:把检查所需数据在启动时预载到托管侧,回调内纯托管判定
- BCL 类型白名单前置:
mscorlib基础类型绑定在托管侧直接放行,不进 native 检查 [SuppressUnmanagedCodeSecurity]:完全信任的 hook 引擎程序集可对 P/Invoke 声明此特性,整条绕开声明式 demand(需引擎自身代码可信)
符号环境修复记录(复现分析必读)
本机 SOS 4.8.9337 与 dump CLR 4.8.4795 不匹配 → 托管命令全挂;msdl 在线下载失败。最终用本地符号库解决:
# 目录准备(一次性)
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]/ 配置预载),按需求流程立项
相关参考
- clr.dll逆向-GetFunctionPointer与Precode(同一 clr.dll 库的 IDA 分析)
- Dump分析方法论与工具链
- 【调试技术】.NET调试