结论先行
Microsoft Passport Container 服务(系统服务名NgcCtnrSvc,与Windows Hello认证有关)存在一个DoS漏洞:当Container中包含算法为ECDSA_P256的密钥时,若试图向NgcCtnrSvc请求以RSA格式导出该密钥的公钥数据(RSAPUBLICBLOB),则NgcCtnrSvc服务会崩溃。
崩溃的原因为ECDSA_P256(基于圆锥曲线)和RSA(基于大整数因子分解)格式不匹配。NgcCtnrSvc未检查密钥算法一致性,试图以RSA格式解析ECDSA_P256密钥,导致数据长度头(字节数)被误读为一个大数(例如0xce95989d,对应3.23GB),进而调用memcpy以错误长度复制数据,引发内存越界。
NgcCtnrSvc崩溃退出后,系统仍可正常使用,但与Windows Hello认证相关的流程无法进行。例如:尝试在Microsoft Edge中查看已存储的密码时,Windows安全中心会询问“你是哪位用户”而非直接询问PIN码;电脑锁屏后,解锁密码/PIN码输入框不显示,导致无法登录。


该漏洞需要特定触发路径(当系统中包含ECDSA_P256算法的密钥时才能触发),因此似乎并不影响所有Windows 11用户。
该漏洞的临时解决方案是重启NgcCtnrSvc服务,更简单的操作是重启电脑。
如何发现的?
「连环电脑睡死案」
在两个月前,我发现Office在启用MathType加载项后会导致启动变慢——Word通常需要加载数分钟后才能正常使用,而我不得不用MathType修改论文里的数学公式。
值得注意的是,若在成功加载后重启一次Word,“启动变慢”的症状便奇迹般地消失了;后续关闭再打开Word,亦无此症状。但在电脑重启后,这种症状又会恢复。
换言之,MathType加载项引起的“变慢”似乎只影响Word的“初次启动”,这给人造成一种“MathType+Office启动需要预热”的错觉。
起初我认为这是MathType版本过旧,或者与它的后端服务器启动耗时长等等原因。但在网上搜索很多教程后,我尝试过的解决方案终究治标不治本——好在,凑合一下还能用。
而在为论文忙得焦头烂额的那个月,我还注意到Windows Hello稳定性有所下降,症状如前所述——电脑锁屏后,PIN码输入框不显示,尝试各种快捷键也无济于事——电脑不得不重启一次。这种恼人的现象常常发生在电脑睡眠之后:电脑睡死了,但是我写到一半没保存的论文也死了😭(这倒逼我养成了及时保存的好习惯…)
某个周三的中午,“电脑睡死”症状的再次发作让我实在忍受不了。我开始向DeepSeek求助,并锁定了初步调查方向:这和Microsoft Passport Container 服务崩溃有关。每当该服务崩溃时,Windows日志会留下一个标志性的、ID为7031的事件:Microsoft Passport Container意外终止。
至此,初步思路已经很清晰:每当7031事件发生时,只需让系统弹出告警(设定一个系统计划任务,将Powershell脚本和该事件关联,作出弹窗提示),然后由我锁定该事件发生前的可疑行为,便能找出“罪魁祸首”。此刻,只能静待症状复现。
而这一等,就是两个月。
“MathType,启动”
之所以两个月与电脑相安无事,恐怕与我的论文完成相关。两个月前论文定稿,于是,我同时把MathType尘封进禁用列表中——不然,每次启动Office都得卡顿好久。
转机发生在前天晚上。我碰巧受托阅览一篇带MathType公式的Word文档,便启用了“那个”加载项——打开Word,加载MathType,久违的卡顿。不到两分钟,告警弹窗显现。
难道是MathType加载项有缺陷?结合两个月前的回忆,我很快把MathType和Windows Hello的Bug联系起来。
值得一提的是,这两个月来,我的AI Agent使用能力也有了一些进步——虽然我不具备专业的调试分析能力,但是我的Codex有。
我决定当晚用Codex开启这项研究。
GPT-5.5的洞察力
我的Codex使用GPT-5.5模型。当我把初步想法告诉GPT-5.5时,它否定了由MathType到Windows Hello的直接关联,但给出了一个令人兴奋的方向:是不是Microsoft Passport Container 服务自身有缺陷?
▼ GPT-5.5的输出
如果 MathTypeServerLaunch.bat* 只是启动 MathType 或做 OLE 注册,那么它和 NgcCtnrSvc 没有直接组件关系。Wiris 文档里 MathType 命令行主要涉及打开文件、重置偏好、OLE 注册/反注册等:参考 MathType command line options。
但存在一种“间接触发”链:
运行 MathTypeServerLaunch.bat
-> 启动 MathType / Office 插件 / OLE 自动化 / 可能的 WebView 或账号授权流程
-> Office、浏览器 WebAuthn、Microsoft 账号、工作或学校账户、Windows Hello 被唤起
-> Windows 请求访问本地 Hello/Passport 身份密钥
-> NgcCtnrSvc 处理本地身份密钥/TPM 容器
-> NgcCtnrSvc 因自身状态异常崩溃
-> Service Control Manager 记录 7034/7031
(*笔者注:MathTypeServerLaunch.bat是我写的用于手动启动MathType后端服务器的脚本。)
随后,我的职责变成了提示词工程,真正的行为溯源和反汇编分析全部交给了GPT-5.5。分析结论正如文章开头所述。
一个自然的问题:在不具备专业知识的前提下,如何判断AI是否在胡说八道?亦即,我是如何确信结论中的大部分内容是真实的?
实践是检验真理的唯一标准。“检验”部分的工作可以分三部分阐述:
其一,我要求Agent提供一条“证据链”,即各种程序行为的调用栈,确保每一个论断都有迹可循。
其二,我要求AI输出一条“逻辑链”,由人来审阅其中的推理是否存在谬误。
其三,我要求系统崩溃的Bug需要能够复现,由Codex生成一段能稳定触发崩溃症状的脚本,并比对脚本作用路径和真实崩溃路径的一致性。
当然,美中不足的是,我仍旧无法保证这里的每一个字都是准确无误的,因为我的专业储备并不充分;但是,经过以上三方面的检验,GPT-5.5的确给出了令我满意的阶段性方案:逻辑上可以自恰、实践上可以复现。
具体的分析过程请参阅下方由AI生成的「研究手记」。如果觉得太长而不愿意看,也可以跳至后文的“事故现场还原”。
▼ GPT-5.5的「研究手记」
研究过程
我一开始接到的问题是:用户怀疑桌面上的
MathTypeServerLaunch.bat会导致 Windows 11 的Microsoft Passport Container服务异常停止,也就是事件日志里的 7031/7034。这个怀疑是合理的,因为从现象上看,MathType 启动项、Word 启动、以及服务崩溃之间确实经常出现在同一个时间窗口里。我的第一反应不是直接认定 MathType 有问题,而是先判断这是不是一个“时间相关”误判。也就是说,MathType 到底是根因,还是只是碰巧触发了某条 Windows 系统路径。于是我先围绕 Word、MathType、Office 启动项做对照:有 MathType、无 MathType,有 Word、无 Word,启动项启用和禁用分别观察 7031/7034 是否出现。
很快我发现,只从 Office 层面看不够。事件日志只能说明
NgcCtnrSvc崩了,不能说明是谁把它打崩的。于是我转向 dump 和调用栈。这个阶段的关键是把注意力从“哪个应用启动了”转到“服务端崩在什么函数里”。dump 显示崩溃点在ucrtbase!memcpy,上层是NgcIsoContainer::ExportRsaPublicKey一类的 Passport 容器导出逻辑。这个结果让我基本排除了“MathType 直接写坏 Windows 服务内存”的可能性,因为崩溃发生在 Windows Passport 服务自己的 RSA 公钥导出处理路径里。接下来我开始追问:什么行为会让
NgcCtnrSvc进入ExportRsaPublicKey?这个问题比“什么程序导致崩溃”更重要。通过符号、栈和反汇编对照,我把服务端路径还原成:客户端通过 Passport KSP 请求导出 RSA 公钥,ngcksp.dll通过 RPC 调到ngcctnrsvc,服务端进入RsaExportPublicKey,再进入隔离容器里的导出实现,最后在复制返回 blob 时崩溃。这时还不能下结论,因为“RSA 导出路径崩溃”本身不说明为什么崩。于是我继续看内存内容。真正的突破点是发现:服务端 RSA 导出包装层拿到的并不是一个 RSA 结果结构,而是一个 ECC public blob。blob 开头是
ECS1,这对应 ECC P-256 公钥;而 RSA 包装层却把 blob 偏移+8的字节当成长度字段。对 ECC blob 来说,+8位置其实是 X 坐标的一部分。于是随机的椭圆曲线坐标字节被误读成一个很大的长度,随后memcpy按这个错误长度复制,最终读越界崩溃。到这里,服务端根因已经比较清楚:这是 RSA/ECDSA key 类型错配后没有被正确拒绝,导致错误解析。下一步我需要证明 MathType/Word 不是必要条件。为此我构造了非 Office 的最小复现:直接打开 Microsoft Passport Key Storage Provider,找到实际算法是
ECDSA_P256的FIDO_AUTHENTICATORkey,然后故意请求RSAPUBLICBLOB。这个最小复现不需要 Word、不需要 MathType、不需要.dotm,但能触发同样的 7031/7034 和同样的服务端崩溃栈。这个结果把 MathType 从“根因”降级成了“自然触发入口”。之后我再回到 Word/MathType 链路,证明它为什么会撞上这个 bug。通过调试 Word 启动过程,我看到 Word 加载启动项时会做 VBA/add-in 签名验证,路径大致是
Word -> WinTrust -> Crypt32 -> CNG -> Passport KSP。Crypt32 在为证书寻找候选 key provider/container 时,会枚举一些候选容器,其中包括 Passport KSP 里的 key。MathType 的加载项签名证书本身是 RSA,但候选池里混进了用户的 Passport/FIDO ECDSA key。随后系统尝试导出候选 key 的公钥来和证书公钥比较,于是对 ECDSA/FIDO key 发出了 RSA 公钥导出请求,最终触发服务端 bug。这个阶段我刻意避免把责任简单甩给 Office。Office 并不是“知道自己在拿 ECDSA 冒充 RSA”。更准确地说,Office 调用了 Windows 提供的通用信任验证机制;Crypt32/CNG 在候选 key 匹配过程中触达了 Passport KSP;Passport/NgcCtnrSvc 服务端没有正确拒绝错配请求,才导致崩溃。所以归因上我会分层:MathType/Word 是入口,Crypt32/CNG 是把候选 key 带入匹配流程的一环,真正的内存安全缺陷在 Windows Passport Container 服务端。
后面很大一部分工作其实不是找根因,而是把复现工具做得可靠。这个 bug 有个麻烦点:服务端崩溃后,客户端
NCryptExportKey可能不会正常返回,脚本会卡在触发步骤。如果脚本在同一个进程里既负责触发又负责恢复,那么它自己卡住后就没法恢复服务。所以我把工具改成控制进程加触发进程的结构:触发进程只负责发出危险调用;控制进程监控 7031/7034,一旦看到服务崩溃,就结束卡住的触发进程并执行恢复。恢复也不是一次就一定彻底。因为服务崩溃后,可能还有残留的 CNG/RPC 请求或重试行为继续触发服务异常。所以我把恢复做成“只触发一次崩溃测试,然后连续做多轮恢复检查”。这不是重复测试三次,而是一次测试后观察服务是否稳定,必要时连续恢复几轮。如果最后仍然恢复不稳,就提示测试者发送日志并重启系统。
最后还处理了很多工程细节:cmd 中文乱码、PowerShell 5.1 对 UTF-8 的 BOM 要求、批处理里不能用反斜杠转义引号、日志文件过多、stdout/stderr 空文件残留等。这些不是漏洞本身的一部分,但如果工具要给别人测试,就必须处理好。最终工具被改成:cmd 只做最小启动器,中文提示交给 PowerShell;一次测试尽量产出一个主日志;复现前强提醒保存工作;测试后建议发送日志并重启。
我最后形成的判断是:
最初看起来像 MathType 启动项导致系统不稳定,但真正的问题不在 MathType。MathType/Word 只是把系统带进了 Office 签名验证和 Windows Crypt32/CNG 的 key 匹配流程。真正导致服务崩溃的是 Windows Passport Container 服务端在处理
ECDSA_P256/FIDO key + RSAPUBLICBLOB这种错配请求时,没有正确拒绝,而是把 ECC blob 当 RSA 结构解析,最终造成越界memcpy和服务崩溃。
事故现场还原(省流版)
(*以下内容为笔者的个人理解,不对知识专业性作担保;其中的细节可能存在部分错误)
- Office需要检验MathType的证书(包含一个RSA公钥)是否有效,以此实现安全加载。
- Office调用了crypt32.dll,试图在系统中查找能匹配MathType证书的私钥。
- crypt32.dll开启了枚举查找,枚举到了Microsoft Passport Key Storage Provider。
- crypt32.dll向Microsoft Passport Container发起请求,希望导出其包含的RSA公钥数据。
- Microsoft Passport Container服务(NgcCtnrSvc)来者不拒,接受了请求,但它所包含的压根不是RSA密钥,而是ECDSA_P256密钥。
- NgcCtnrSvc试图以RSA格式解析ECDSA_P256密钥,读出了一个错误的长度头。
- NgcCtnrSvc以错误的长度头进行了memcpy。
- NgcCtnrSvc崩溃了。
在我的系统里当然找不到匹配MathType证书的私钥。而这里,crypt32.dll之所以开启枚举查找流程,更像是一次“例行公事”,用来判定“要检验的证书来自谁”:如果找到了,证书就可以按自签名方式获得信任;如果没找到,则说明证书来自第三者。
看似规范化的流程也会犯错,恰恰说明流程仍不够规范:一项服务,或一个接口,不能无差别地信任和接受发来的任何请求。
启示就是永远把用户当黑客。
后记
在Codex打包好测试脚本后,我发给了2位同样使用Windows 11的朋友,并在他们完全知情的前提下进行了漏洞复现测试。
遗憾的是,测试日志显示,由于系统中并不包含ECDSA_P256算法的密钥,因此该漏洞均无法在他们的电脑中复现。
总而言之,这次漏洞分析源于偶然注意,也始于一份对「答案」的执着。整个过程像是一场解谜游戏,艰辛而有趣。
感谢两位朋友,也感谢用耐心看完文章的你!复现代码会适时公布,请在确保安全的前提下以正确用途使用(❁´◡`❁)。
