@CrackmesOne: 关于分析 Windows UCPD 的 DR(动态规则)的新博客,以及一个有趣的故事,发现整个过程可能已经……
摘要
一位逆向工程师对存储在注册表键中的隐藏 Windows 可执行文件的分析,该注册表键被 UCPD 的动态规则使用,揭示了该文件不包含任何代码且可能已损坏,强调了在人工智能时代人类逆向工程仍然不可或缺。
查看缓存全文
缓存时间: 2026/08/05 04:19
新博客:分析 Windows UCPD 的 DR(动态规则),外加一个有趣的故事——发现整个机制可能已经坏掉,以及为什么即使在 AI 时代,人类逆向工程师仍然很重要:https://t.co/vxhwt64492
Binary Ninja - 隐藏在你的注册表中的二进制文件:破解 Windows UCPD 的动态规则
来源:https://binary.ninja/2026/08/04/ucpd-dynamic-rules.html 微软是否正在把一个隐藏的二进制文件送到你的电脑上——而且还藏在注册表里?
几周前,我在看一个 YouTube 视频(https://www.youtube.com/watch?v=xQUYh4iKsB0),里面介绍了我们之前对 UCPD 驱动(https://binary.ninja/2025/03/25/default-browser-upcd.html)的研究。就在那一瞬间,我看到了一个我找了好久示例的注册表键。我联系了视频作者,从他的机器上拿到了这个键。它是 Base64 编码的,而令我惊讶的是,当我解码后,它以 MZ 开头。
没错,这是一个有效的 Windows 可执行文件,就藏在注册表键里。这篇文章讲述了我如何拆解它的故事,以及我发现的那个 Bug——这意味着整个机制实际上已经是死路一条。
UCPD 快速回顾
如果你还没读过我之前的 UCPD 文章(https://binary.ninja/2025/03/25/default-browser-upcd.html),这里有个简短版本。
UCPD 全称是 User Choice Protection Driver(用户选择保护驱动),它的全部工作就是保护你的默认浏览器选择。在 Windows 上,设置默认浏览器曾经只是写入一个带有正确哈希的注册表键的问题。UCPD 终结了这种做法:现在只有 Windows 设置应用有权这样做,并且驱动会专门阻止微软自家的一系列工具,比如 reg.exe、powershell.exe、rundll32.exe,这些工具原本可能被诱骗去修改该设置。
浏览器厂商(还有间谍软件/广告软件作者!)对此并不高兴。他们找到了绕过的办法,微软加固了驱动,他们又找到新的绕过办法,如此循环。我在上面的博客文章和 RE//verse 2025 的一场闪电演讲(https://youtu.be/TheUdURzFjI)中已经介绍过这场猫鼠游戏,所以这里就不再赘述了。
不过,那项研究有一个未解决的问题。驱动里埋藏着一条代码路径,它会从这个注册表键加载一些配置:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\UCPD\DR
遗憾的是,我无法分析它,因为在我所有的机器和虚拟机上,这个键都是空的。
一个没有代码的 PE
从 FlyTech 视频“Microsoft Added This Driver to Windows and Said Nothing”(微软给 Windows 加了这么个驱动,却什么都没说)(https://youtu.be/xQUYh4iKsB0?t=381)中可以看到,在他的机器上 DR 有一个值。他把 DR 理解为“Disaster Recovery”(灾难恢复),但正如我们下面会看到的,这很可能不是它的本意。
FlyTech 身在欧洲,这可能解释为什么他的机器上有这个键。整个 UCPD 事件源于欧盟的浏览器选择规则,因此如果微软只向欧洲用户推送这些策略数据块,那也不足为奇。但需要明确的是,这只是一个猜测。
如前所述,我们 Base64 解码后,它就是一个 PE 文件。
那么:微软是否在你不知情的情况下,在你的机器上运行二进制文件?
不。而这与这个文件的第一件有趣的事情有关。我在 Binary Ninja 中打开它,发现里面根本没有代码。这不是解析 Bug——它只有一个很小的 .rdata 节,里面看起来像是加密数据,文件末尾还有一个 Authenticode 证书。
``
list(bv.functions) [] ``
但既然永远不会执行它,为什么要把数据包在 PE 里呢?
大概是因为,通过将载荷打包为已签名的 PE,微软可以免费复用整个 Authenticode 代码签名基础设施,而驱动可以在对该数据块的内容采取行动之前,验证只有微软才能生成这个数据块。这实际上是一个非常明智的设计决策。你真的不希望一个内核驱动去消费一个任何管理员都能覆盖的注册表键中的策略。
现在,我已经有了拼图的两半——加密的数据块和解密它的代码——我们终于可以弄清楚它到底做什么了。
逆向加载器
这个驱动非常容易逆向,因为每个阶段都会记录一个 ETW 事件,带有描述性的操作标签。从上到下阅读,process_DR 做的正是你所期望的事:
| 日志标签 | 作用
— | — | —
1 | Base64Decode | 将 REG_SZ 字符串解码为 PE 字节
2 | ParsePEFormat | 定位 .rdata 数据块
3 | CalculatePEHashInMem | 对内存中的 PE 进行哈希
4 | CertificateVerify | 签名门禁——只接受微软签名的策略
5 | DecryptData | 解密数据块
6 | DispatcherConfig | 解析并分发解密后的记录
驱动在叙述自己的流水线。
第 5 阶段是我关心的。我请 Sidekick(https://sidekick.binary.ninja/),我们的 AI 助手,去逆向这个解密函数。它的回答是:这是一个自定义的 XOR 流密码。一个哈希函数从密钥派生出一组种子,这些种子生成密钥流,密钥流再与密文进行 XOR。没什么特别的。
它还顺手把所有东西重新命名了:expand_key_state、derive_keystream,以及两个混合函数。这把一堆 sub_140004xxx 调用变成了一段真正能读的代码。我快速扫了一眼代码,看起来是对的。
Sidekick 标记后的代码
重新实现密码
然后我让 Sidekick 用 Python 重新实现了整个算法。
在我运行代码之前,我注意到这个密码需要一个 32 字节的密钥,但函数里没有内置的密钥。因此,密钥一定来自数据本身。
看看 .rdata 的开头,很容易就能发现它以 u32 的模式版本 0x3ec 开头,后面跟着一个 u32 的 0x238,这看起来正好是密文的长度。如果我取接下来的 0x20 字节作为加密密钥,那么该节中剩余的字节恰好是 0x238。一切都能对上!布局如下所示:
.rdata 中的数据块布局
我把这个交给 Sidekick,请它解密数据块。然而,尽管我期望很高,结果却是乱码:
00000000: fe 2a 56 4a e3 ff fb 5b 9f 78 91 bd 2b d0 5c 59 .*VJ....x..+.\Y 00000010: 0d 92 1f e9 10 c0 44 8f 0a 2d c5 2d c8 b9 54 7d ......D..-.-..T} 00000020: be bd 53 65 68 2b b4 08 63 f0 69 89 2e 4c a0 7a ..Seh+..c.i..L.z 00000030: 2d ca 1c 50 75 00 80 09 f3 ef 41 8e 78 67 7f 49 -..Pu.....A.xg.I 00000040: f5 0a 1e f2 b1 49 05 b5 8e b2 51 2d 27 44 0f 1c .....I....Q-'D.. 00000050: 36 6f bc 39 8f cb 60 49 ee 1c 46 0e 16 a2 b1 91 6o.9..`I..F..... 00000060: 92 40 27 84 64 02 92 41 a2 ec a8 dc d1 4f 54 3f .@'.d..A.....OT?
我的第一印象是 Sidekick 搞砸了。于是我让 Claude Code 用 Binary Ninja 的 MCP 服务器(https://dev-docs.binary.ninja/guide/mcp.html)重做一遍。我特意只给了它二进制文件而不是分析数据库,这样它就不会受到 Sidekick 的重新命名或类型信息的影响。
这次它写了另一个脚本,产生了一模一样的乱码输出。当我质疑时,它甚至引入了 Unicorn(https://www.unicorn-engine.org/)来模拟执行代码,以此论证自己做得完全正确。
两个 AI 以完全相同的方式出错的可能性似乎很低,所以我怀疑这里有什么古怪的地方。但我一时也说不清到底是什么,就先搁置了一段时间。
找到正确的哈希
在好好思考了一番之后,我决定用谷歌搜索这个密码中使用的几个魔术常量。这就是我们在没有 AI 时的做法!有一些搜索结果,但看起来都没什么用。然后,纯属运气,我决定用 DuckDuckGo 搜索,这次得到了一个很有希望的命中:CalcHash_CS64(https://github.com/276793422/CalcHash_CS64)。它似乎用 C 语言实现了同样的算法。
我仍然半信半疑,但还是把它交给 AI,让它看看是否能有所进展。而这一次竟然真的成功了!数据块被解密成了可读的内容:
00000000: 01 00 00 00 03 00 00 00 0c 00 00 00 28 02 00 00 ............(... 00000010: 01 00 00 00 f4 03 00 00 0c 00 00 00 18 02 00 00 ................ 00000020: 0e 00 00 00 ee 03 00 00 0b 00 00 00 1a 00 00 00 ................ 00000030: 2a 00 5c 00 64 00 6c 00 6c 00 68 00 6f 00 73 00 *.\.d.l.l.h.o.s. 00000040: 74 00 2e 00 65 00 78 00 65 00 ee 03 00 00 0b 00 t...e.x.e....... 00000050: 00 00 12 00 00 00 2a 00 5c 00 72 00 65 00 67 00 ......*.\.r.e.g. 00000060: 2e 00 65 00 78 00 65 00 ee 03 00 00 0b 00 00 00 ..e.x.e.........
而两个版本之间唯一的区别是,CalcHash_CS64 使用 MD5 来派生种子,而我们的重新实现使用 SHA-512。但 UCPD 的代码显然是用 SHA-512 来派生种子的:
UCPD 使用 SHA-512
以下是我认为发生了什么。在某个时间点,微软的某个人看到了这段代码,看到 MD5,决定用 SHA-512 来加强它。但他们只改了驱动,却忘了更新生成密文的代码。
我也不认为这个失败是无声的。那个 DecryptData 失败路径不是调试打印,而是一个 ETW 遥测事件。所以,在微软的某个地方,应该每次解析这个键时都会稳定地触发解密失败事件。需要说明的是,我并没有在一台真实机器上调试来确认这一点,但从代码来看相当清楚。如果你碰巧在微软工作并负责 UCPD:去查查你们的遥测吧!
话虽如此,AI 做得很好,但我们离 AGI 还差一点。
动态规则里有什么
从解密的数据块中我们可以看到 14 个程序名:
*\dllhost.exe *\reg.exe *\rundll32.exe *\powershell.exe *\regedit.exe *\wscript.exe *\cscript.exe *\cmd.exe *\InfDefaultInstall.exe *\pwsh.exe *\wmiprvse.exe *\regini.exe *\bssafe.exe *\mshta.exe
如果你读过我的第一篇 UCPD 文章,这个列表会非常眼熟。这些是微软自己的签名二进制文件——它们可以轻易通过“是否由微软签名”的检查——所以它们有自己的拒绝列表。否则,翻转默认浏览器就像让 reg.exe 帮你改一下那么简单,这就违背了驱动的目的。
那么,为什么要把这个列表放进注册表键呢?同样的名称列表已经硬编码在 UCPD.sys 里了。
来自 UCPD.sys 的程序名列表
记住这个键叫DR。我认为它不是 Disaster Recovery,而是Dynamic Rules(动态规则)。改变驱动的行为通常意味着发布一个新的 UCPD.sys,然后让用户安装更新并重启。有了这个通道,微软可以把策略更新作为注册表值中的签名数据块推送出去,并在下次加载时生效。这类似于杀毒软件的概念更新。
而且它远不止是一个列表。我还让 AI 逆向了分发器。解密的缓冲区是一个嵌套的 TLV 树——先是 u32 计数,然后是 [类型][值类型][长度][载荷] 的记录——由一个分发器遍历,它会在驱动初始化时填充的处理程序表中查找每个类型。其中有五个已注册:
| 类型 | 名称(来自 ETW 事件) | 它配置什么 |
|---|---|---|
| 1 | AntiInjection | 注入强制策略,6 个类型化子字段 |
| 2 | UIA | UI 自动化策略,一个由 32 字节条目组成的表 |
| 3 | DenyListV1 | 被禁止访问受保护键的进程模式 |
| 4 | AllowListV1 | 被允许的进程模式,独立的列表和锁 |
| 5 | StackTrace | 与写入调用栈匹配的模块名 |
我们的数据块是单个类型 3 的记录。拒绝列表和允许列表是独立的结构,带有各自的锁,所以配置可以广泛拒绝,然后划出例外。每个处理程序会获取锁,从头重建其结构,并发出一个带有规则名和模式版本标签的 ETW 事件。
目前它附带的列表与已编译进驱动的列表完全相同,所以目前它并没有改变什么——但这个机制已经就位,为将来某个浏览器厂商找到新的绕过保护的方法做好了准备。
我还为此写了一个 Kaitai Struct(https://kaitai.io/)定义(https://github.com/xusheng6/ucpd_analysis/blob/main/202606_DR/ucpd_dr_config.ksy),并使用我们的 Kaitai UI 插件(https://github.com/Vector35/kaitai)(带有补丁(https://github.com/Vector35/kaitai/tree/kaitai-ide-live-compile))直接在 Binary Ninja UI 中渲染了解析后的树。
用 Binary Ninja 中的 Kaitai 解析解密后的配置
为什么用这种加密?“专利哈希”
故事本来可以到此为止,但还有一条线索把所有东西串在了一起。
在整个分析过程中,AI 一直把这个哈希函数称为“专利哈希”。我原本以为这是幻觉,但微软确实持有这个确切密码的专利:US 6,570,988 B1(https://patents.google.com/patent/US6570988B1/en)(大约在 2020 年过期),“使用简单寄存器操作实现加密原语的简单技术”。
而它正是 Windows 用来计算 UserChoice 关联 哈希的同一个算法。
回想一下前 UCPD 时代。在 UserChoice 键下,有一个程序 ID 和一个 Hash 值。这个哈希混合了你的用户名、程序 ID、扩展名或协议,以及一个时间戳。如果它验证失败,Windows 会忽略你的默认设置并回退到 Edge。这是防篡改层,UCPD 后来被构建为在内核模式下强制执行它。它由 Christoph Kolbicz 早在 2017 年(https://kolbi.cz/blog/2017/10/25/setuserfta-userchoice-hash-defeated-set-file-type-associations-per-user/)逆向出来。Firefox 后来实现了同样的算法(https://searchfox.org/firefox-main/source/browser/components/shell/WindowsUserChoice.cpp),以便在没有引导用户通过 Windows 设置界面的情况下将自己设置为默认浏览器。
现在就很清楚了,为什么微软会自研密码而不是直接用 AES:他们并不完全是自研。他们手头有 UserChoice 时代留下的这个东西,于是复用了它。
关于 AI 和逆向工程
毫无疑问,AI 正在快速进步,越来越多的逆向工程工作可以由它处理。我仍然记得去年 DEF CON 决赛时听到的震撼:一支队伍用 AI 击败了两位人类玩家(https://seeinglogic.com/posts/livectf-ai-debut/),拿下了 LiveCTF 的一个 VM 挑战。整整一年后,这已经完全不令人惊讶了。事实上,AI 在逆向工程上已经变得如此出色,以至于现在很难找到它解不了的挑战。
AI 只会变得更好——比大多数人类逆向工程师更好。然而,正如这篇文章所示,我们仍然需要真人来检查它的结果,并在结果不能自动验证时引导它。此外,只有人类才能从更宏观的意义上欣赏一个坏掉的算法所带来的讽刺。
参考资料
- 探查 Windows 的默认浏览器保护(https://binary.ninja/2025/03/25/default-browser-upcd.html)——我之前对 UCPD 的研究
- FlyTech Videos,“Microsoft Added This Driver to Windows and Said Nothing”(https://www.youtube.com/watch?v=xQUYh4iKsB0)
276793422/CalcHash_CS64(https://github.com/276793422/CalcHash_CS64)——破案的关键参考实现- US 6,570,988 B1 / WO2000078118A2(https://patents.google.com/patent/WO2000078118A2/en)——微软专利
- SetUserFTA:UserChoice 哈希被攻破(https://kolbi.cz/blog/2017/10/25/setuserfta-userchoice-hash-defeated-set-file-type-associations-per-user/)——Christoph Kolbicz 关于专利哈希原始用途的文章
- UCPD.sys - UserChoice Protection Driver - Part 2(https://kolbi.cz/blog/2025/07/15/ucpd-sys-userchoice-protection-driver-part-2/)——Kolbicz 对 UCPD 的后续研究
- https://hitco.at/blog/windows-userchoice-protection-driver-ucpd/
- https://github.com/xusheng6/ucpd_analysis——我所有的 UCPD 分析数据库和工具
202606_DR/(https://github.com/xusheng6/ucpd_analysis/tree/main/202606_DR)——这篇文章的所有内容:.reg抓取结果、提取出的 PE、分析数据库、独立的decrypt_reg.py,以及 Kaitai 定义
相似文章
@DragonsCyberHQ: Windows PnP is the loader here. Attacker-controlled device identities can make Windows fetch and run vendor code as SYS…
Security researchers release a DEF CON 34 talk and tooling showing that Windows Plug and Play can silently download and execute vendor code as SYSTEM via attacker-controlled device identities, including through USB emulation and RDP USB redirection.
@0x0SojalSec: AI Ghidra 和 Radare2:AI 驱动的逆向工程。能够反汇编、反编译、使用 YARA 扫描等的 AI 代理,以及…
Reversecore MCP 是一款企业级的 AI 驱动的逆向工程与安全分析工具,通过模型上下文协议(MCP)与 AI 助手集成,提供超过 50 种工具,用于静态/动态分析、恶意软件分析、漏洞研究等。
Azure DevOps MCP 与代理 PR 审查中的混乱代理问题
一份关于微软 Azure DevOps MCP 服务器的报告揭示了一种混乱代理攻击:隐藏的 PR 文本可以操纵 AI 审查代理(Copilot CLI、Claude Code)以用户的权限进行非预期的工具调用。建议包括使用只读身份并要求单独的审批步骤。
展示 HN:我为 Windows 原生 x64/x86 崩溃构建了一个事后调试器
ForensicDbg 是一个现代化的事后调试器,专为 Windows 原生 x64/x86 崩溃设计,具备集成AI的MCP接口和最先进的用户界面,以实现高效的错误识别和分析。
@MSFTResearch:Project Ire 分析了最新发现的恶意软件样本,并通过逆向工程确定其意图——识别出 LOTUSLIT……
微软的 Project Ire 是一个自主恶意软件分类代理,它通过行为逆向工程成功识别出一个规避了主流 EDR 工具的 LOTUSLITE 变体,且无需依赖 IOC 签名。