karollooool/CVE-2026-50416-writeup-and-poc

GitHub: karollooool/CVE-2026-50416-writeup-and-poc

该仓库记录了 Windows 11 中 win32k 桌面堆暴露内核指针导致 KASLR 绕过的漏洞分析,并提供了从沙箱内读取内核地址的完整概念验证代码。

Stars: 28 | Forks: 5

# CVE-2026-50416:每个桌面上的内核指针,可从任何沙箱读取 ## TL;DR 在 Windows 11 Insider 版本 10.0.28020.2149 中,win32k 桌面堆的用户模式映射会在偏移量 0x100 处提供一个原始的内核会话池指针。任何拥有桌面的进程都可以读取它。这包括低完整性进程、AppContainer、具有零权限的 LPAC,以及一个看起来与浏览器渲染器完全一样的低完整性 AppContainer。从那个单次泄露的 QWORD 中,你可以恢复内核桌面堆的基地址,然后借助 `gSharedInfo`,计算出桌面上每个窗口、菜单和类对象的精确内核虚拟地址。 这是一个 KASLR 绕过,属于一种信息泄露。它本身并不是代码执行。我想事先声明这一点,因为夸大一个漏洞是让分析文章变得无法阅读的最快方法。它的实质是,从根本不应该触达的地方发生了一次非常干净的泄露。 CVE:[CVE-2026-50416](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-50416)。已报告给 MSRC。这篇文章记录了这个漏洞、概念验证,以及真正让我警觉起来的部分:内核指针暴露时所跨越的沙箱边界。 ## 一句话概述的漏洞 每个加入桌面的进程都会在自身的地址空间中获得一个只读的桌面堆映射。内核将窗口和菜单对象写入此堆,而用户模式将它们读回,这是正常且必要的。不必要的是,该映射的偏移量 0x100 处包含一个指向会话池的存活内核指针。在它对用户模式可见之前,没有人对其进行净化。Windows 已经知道如何净化此堆中的内核指针,它对其他字段使用了 `0x6000000000` 哨兵值。只是这个被遗漏了。 ## 一些背景知识,因为没有它这个技巧就说不通 win32k 是拥有窗口、菜单、光标、钩子以及整个图形对象世界的内核组件。大量 win32k 状态按桌面存储在一个称为桌面堆的结构中。为了使窗口操作变快,内核将这块堆的一部分作为只读共享区映射到加入桌面的每个进程中。你的进程无需 syscall 即可直接从此映射中读取窗口的文本或样式。该映射是让 GUI 感觉瞬时的共享幻象。 从用户模式找到它是轻而易举且有文档记录的。TEB 有一个 `ClientInfo` 结构。桌面堆的用户模式地址位于 `ClientInfo[5]` 中。代码如下: ``` PVOID teb = (PVOID)__readgsqword(0x30); PVOID* clientInfo = (PVOID*)((BYTE*)teb + 0x800); BYTE* desktopHeap = (BYTE*)clientInfo[5]; ``` 这就是整个句柄。目前还没有漏洞。这部分是设计使然。 漏洞就在于该堆偏移量 0x100 处的内容。 ## 泄露 ``` ULONG64 leaked = *(ULONG64*)(desktopHeap + 0x100); ``` 在测试的版本中,该 QWORD 保存的内容类似于 `0xFFFFA804DDE00040`。这是一个规范的内核地址。它是 8 字节对齐的。它指向会话池,即这个完全相同的桌面堆在内核侧向内偏移 0x40 字节的位置。 在相信它之前,我进行了通常的健全性检查。 1. 规范的高位(前 17 位被置位)。是的。 2. 8 字节对齐。是的。 3. 在创建和销毁一堆窗口的过程中保持稳定。是的,之前、期间和之后完全一致。 4. 在同一桌面上的不相关进程中完全一致。是的。 5. 重启后不同。是的。 所以它是一个真实的、持久的、涵盖整个启动会话的内核地址,而不是垃圾数据或陈旧的值。属性四和属性五结合在一起,就是 KASLR 随机化地址的特征指纹,该地址在一次启动内是恒定的。这正是 KASLR 应该隐藏的东西。 出于好奇,我在实际测试会话中的值是 `0xFFFFC600DCC00040`。形状相同,行为相同。你的会有所不同,因为 KASLR。这正是问题所在。 ## 从一个指针到一切事物的地址 单个泄露的地址很不错。但将其转换为任何特定对象的地址才是它真正有用的地方。 有两个事实起到了关键作用。 首先,泄露的值指向内核桌面堆内偏移 0x40 字节处。因此内核桌面堆的基地址就是泄露的值减去 0x40。 ``` kernel desktop heap base = leaked minus 0x40 ``` 其次,user32 导出了一个名为 `gSharedInfo` 的结构。除其他事项外,它为你提供了全局句柄表(`aheList`)和一个句柄表条目的大小(`HeEntrySize`,在 x64 上为 0x20)。每个 HWND 实际上都是该表中的一个索引。取 HWND 的低 16 位,遍历到该条目,该条目的第一个 QWORD 就是该窗口对象在桌面堆内部的偏移量。 将它们组合起来: ``` offset = gSharedInfo.aheList[HWND & 0xFFFF].offset kernel addr = kernel desktop heap base plus offset ``` 这就是支持给定 HWND 的窗口对象的精确内核虚拟地址。不是猜测。不是喷射。而是实际地址。 概念验证创建了六个不同类(STATIC、BUTTON、EDIT、LISTBOX、SCROLLBAR、COMBOBOX)的窗口,并解析了全部六个窗口。它还扫描了堆,在 0x100 之外发现了另外半打未净化的内核指针,因此 0x100 只是最可靠的一个,而不是唯一的一个。 ## 沙箱角度,这才是真正的头条新闻 这是让这个漏洞从“有趣”升级为“真正令人担忧”的部分。 我编写了第二个程序,它在逐渐恶劣的环境中生成子进程,并询问每个进程是否可以读取相同的指针。这些环境按理本应能做到的事情越来越少,按此顺序排列: 1. 中等完整性,标准用户。基线。泄露。 2. 低完整性。泄露。 3. AppContainer,零权限。泄露。 4. LPAC,Less Privileged AppContainer,零权限。泄露。 5. 低完整性加上 AppContainer,零权限。这是 Chromium 渲染器的特征。泄露。 6. 一个完全不同的桌面,使用 `CreateDesktop` 创建。泄露,但值不同,因为它有自己的桌面堆。 环境 1 到 5 全部返回了相同的内核地址,因为它们共享默认的桌面堆。环境 6 返回了不同的地址,因为它是一个不同的堆,但该技术的工作原理完全相同。 第五个是值得我们凝视的。一个在低完整性下运行、在 AppContainer 内、拥有零权限的进程,可以说是 Windows 上用户模式进程中被锁定得最彻底的状态。这就是浏览器放置其渲染器的沙箱。而那个沙箱可以读取内核地址。 这之所以刺痛人是有原因的。浏览器的整个深度防御模型假设,即使攻击者通过单独的引擎漏洞在渲染器内部获得了代码执行权,沙箱也能控制破坏范围并隐藏内核。KASLR 是这种隐藏的重要组成部分。这次泄露直接伸进了沙箱内部,在不需要额外 syscall 且没有权限提升的情况下,将一个内核地址交给了攻击者。沙箱完美地完成了它的工作。然而信息仍然从它眼皮底下泄露了出去,通过的是沙箱模型视为良性的共享区。 ## 你甚至不需要一个窗口 我一直在等待那个附加条件。你肯定必须先创建一个窗口,或者调用某个沙箱会注意到的 GUI syscall 吧。答案是否定的。 在这个版本上,甚至在 `user32.dll` 加载之前,桌面堆映射就已经存在于进程地址空间中了。概念验证在加载 user32 之前和之后分别读取了 `desktop_heap[0x100]`,两次读取都返回了相同的内核指针。没有 `CreateWindow`。没有 `GetDesktopWindow`。什么都没有。任何具有桌面关联的进程,包括无头服务和后台工作进程,都可以在它启动的瞬间读取它。 渲染器概念验证正是依赖于这一点。它启动了一个在 AppContainer 中具有零权限的低完整性子进程,让它加载 user32 且仅加载 user32,它就在启动时立即读取了指针。原样输出: ``` RENDERER|LEAKED|0xffffc600dcc00040|AC=1|IL=0x1000|NoWindowCreated ``` `IL=0x1000` 代表低完整性。`AC=1` 代表位于 AppContainer 内部。`NoWindowCreated` 就是字面上的意思。 ## 一个几乎可以说是独立漏洞的额外发现 在那里的时候,我向这个堆投放了一个扫描器,看看周围还有什么。发现不仅仅是指针。 其他进程的窗口标题。扫描器在堆中以 UTF16 格式发现了二十个属于其他进程的唯一标题,每一个都与 `EnumWindows` 进行了交叉检查,以确认拥有该 PID 的进程确实拥有带有该标题的窗口。Chrome 标签页、Discord、Explorer、Spotify、托盘窗口。这个堆就像一个记录着桌面上每个人正在做什么的小公告板,任何沙箱化进程都可以在不需要任何 IPC 的情况下读取它。 进程 ID。堆中有超过六百个 DWORD 值被验证为真实的正在运行的 PID,每一个都通过两种方式进行了确认:一是 `OpenProcess` 成功,二是该 PID 出现在 `EnumWindows` 列表中。因此,一个沙箱化的进程可以静默地枚举出谁在桌面上拥有窗口。 更多内核指针。每次运行有 6 到 10 个,随桌面活动而变化,而不仅仅是 0x100 处的那一个。 有一件它确实没有暴露的东西,这是我特意检查过的:密码编辑控件的文本。我创建了一个带有 `ES_PASSWORD` 的 EDIT,将其文本设置为 `SecretPassword123`,然后搜索了堆。不在那里。Windows 掩盖了桌面堆中的密码字段内容。很好。小小的仁慈。 所以这个堆会泄露地址和窗口标题,但至少它保住了你的密码。只要能找到,哪怕只有一点好处我也要收下。 ## 为什么如此干净的泄露值得引起关注 KASLR 的存在是为了让内核利用变成概率性事件。在没有地址知识的情况下,win32k 的 use after free(释放后使用)就变成了盲目的池喷射。你向内核分配器发起洪泛,祈祷你的替换对象刚好落在悬空指针指向的位置,然后祈祷好运。在现代池的实现中,这种成功率很低,而且还在变得更低。 现在把这个泄露交给攻击者。他们知道被释放的窗口对象的精确内核地址。他们可以在确切的那个地址精心构造一个替换分配,并精确控制悬空指针引用的内容。盲目的内存破坏变成了确定性的。这就是干净的 KASLR 绕过的真正价值,它是一个不相关的内存破坏漏洞的可靠性放大器,而不是一个独立的漏洞。 具体而言,此次泄露削弱了以下缓解措施: * win32k 会话池的 KASLR。如果没有这次泄露,攻击者需要猜测大约 44 位的熵。有了它,就无需任何猜测。 * 池随机化。攻击者读取 `gSharedInfo` 以获知每个窗口对象的精确偏移量,然后直接计算出内核地址。 * 对象隔离。攻击者知道哪个内核地址映射到哪个 HWND,因此他们可以针对选定的对象进行破坏,而不是进行喷射并祈祷。 通过这单次读取即可获得内核地址的对象包括:窗口对象(tagWND)、菜单对象(tagMENU)、类对象(tagCLS),以及桌面堆上可通过句柄表访问的任何其他对象。一次读取,整个桌面便尽收眼底。 ## 如何重现 PoC 目录下有一个 `compile.bat`,它可以找到 Visual Studio 并构建你选择的任何目标。 ``` compile.bat then choose a number from the menu ``` 这些目标按向你展示的信息量从多到少排列: * `kaslr_bypass_poc.exe`。主要的程序。逐步演示泄露过程,计算内核基地址,解析活跃窗口地址,扫描额外指针,并打印完整的利用链。首先运行这个。 * `kaslr_sandbox_proof.exe`。生成处于 Low IL、AppContainer 内、LPAC 内、Low IL 加 AppContainer 内以及备用桌面上的子进程,然后报告每个子进程是否发生了泄露以及它们是否完全一致。 * `supporting_proof_no_window.exe`。证明在没有任何窗口存在之前泄露就已经起作用了。 * `supporting_proof_no_caps_lpac.exe`。证明在具有零权限的 AppContainer 和 LPAC 中可以实现泄露。 * `supporting_proof_sensitive_data.exe`。证明跨进程的数据暴露(标题和 PID),以及密码文本未被暴露。 * `supporting_proof_exploitability.exe`。解析六个窗口类的内核地址并展示对缓解措施的击败。 * `supporting_proof_remote_trigger.exe`。等同于渲染器环境的情况。 三个快速检查让你自己相信它是真实的: 1. 在同一次启动中运行主要的 PoC 两次。两次泄露的指针相同。 2. 从两个终端,即两个不同的进程运行它。指针相同。 3. 重启。再次运行它。指针不同。 一和二确认它是一个稳定的、共享的、真实的地址。三确认它是 KASLR 随机化的。它们结合起来就构成了这个漏洞。 ## 预期与现实 **预期。** 用户模式桌面堆映射绝不应暴露原始的内核指针。在用户模式能够看到桌面堆元数据中的任何内核指针之前,都应将其净化。 **现实。** 偏移量 0x100 处暴露了一个内核会话池指针。每个拥有桌面的进程都能读取它,包括低完整性、AppContainer 和 LPAC 进程。在测试的会话中,每个环境都返回了 `0xFFFFC600DCC00040`。 ## 修复方案应该是什么样子 最廉价的修复方法与 Windows 已经在此堆中其他地方所做的相匹配。在桌面堆头部中的内核指针到达用户模式映射之前对其进行净化,即对其他字段使用相同的 `0x6000000000` 哨兵处理。 用户模式实际上不需要头页面,那么更强大的修复方法是完全停止通过用户模式映射来暴露该页面。对于微软选择哪一种,我没有强烈的意见。但我强烈认为,一个原始的内核指针不应该只距离一个零权限的渲染器仅有一次 `__readgsqword` 和一次指针解引用之遥。 ## 结语 我总是忍不住回想环境五。一个具有零权限的低完整性 AppContainer 本应是那个没有任何有趣信息可以逃脱的盒子。桌面堆映射属于沙箱模型很久以前就决定可以安全保留映射的那类共享资源,因为它是只读的,而且读取窗口文本是无害的。这两点至今仍然成立。改变的是,一个头部字段将那个无害的只读窗口变成了一个直通会话池的窥视孔。 沙箱并没有失败。沙箱之下的假设失败了。这是两种不同的失败,而后者更难预见,这也许就是没有人发现它的原因。直到你读取了偏移量 0x100。
标签:0day挖掘, KASLR绕过, PoC, UML, Web报告查看器, 信息泄露, 内核安全, 客户端加密, 暴力破解, 漏洞分析, 端点可见性, 路径探测