作者:Aaron Adams
翻译:55-AA
译注:本文部分地方采用了意译,如有疑问请参阅原文。
术语:
今年早些时候,我接触到一个有趣的关于win32k.sys(CVE-2015-0057)的漏洞,并且实现了在 32 位和 64 位系统上的稳定利用,其适用范围从 XP 到 Windows 8.1 (也有一些例外)。本文详细描述了我是如何在这两个平台上完成利用的,结尾部分也附带了一些别的东西。另外也描述了如何在开启 SMEP 的 Windows 8.1 上实现低完整性权限下的利用。
本文很长,我努力提供尽可能多的细节来展示利用这个漏洞的复杂性,而不是隐藏这些细节,当然我也回避了一些细节。希望这些细节对大家有所帮助。
2015年2月10日,微软公布了 MS15-010 的相关细节。这个 BUG 由 enSilo 的 Udi Yavo首先发现。Udi 在 breaking malware blog 上给出了很好的分析"one bit rule-bypassing windows 10 protections using single bit"。我推荐认真阅读这篇文章以便深入了解这个 BUG,虽然我将在本文中将给出尽可能多细节,这些细节涉及了在触发漏洞时必须克服的一些障碍。这个漏洞的利用非常有意思,许多细节来源于 Udi 的博客,下面是他声明:
合理披露:虽然这个博客是技术性的,但我们不会透露任何代码和完整的细节,以防止任何技术专家重现这个漏洞利用。
作为利用这个漏洞的额外奖励,我们得到了一个口袋妖怪的进化:技术精灵。我想我得给 Udi 一些荣誉,因为他发现这个 BUG,并在博客上提供相关情况以及利用这个 BUG 的细节,这些东西太有用了。
之前,我从来没有利用过 win32k.sys 的漏洞,也不熟悉用户模式回调和许多相关的 API,所以我也致谢一些著名的安全研究人员在网上提供的新奇的资源,如 Skywing、Tarjei Mandt、Alex Ionescu 和 j00ru 等。这些人提供如此多的技术信息公开,他们都应该得到褒奖。我大量参考的是 Tarjei Mandt 的一篇文章Win32k.sys exploitation paper。
在我写这个漏洞利用的时候,一个优秀的逆向工程师实现了CVE-2015-1701的稳定利用,其中关于用户模式回调的实例代码是很有用的,感谢该作者。
值得注意的是,我下面的分析是在 Windows 7 上完成的,因为它似乎是唯一的版本,这个版本的 win32k.sys 中的所有结构都有对应的符号。这些符号大多数可用于别的版本的 Win32k.sys 中的结构。不知什么原因,微软将这些符号从 windows 8 中取消了。
最后,我想说的是,我的利用方法相当复杂。完全可能有一个更容易的方法实现它,只是我没有发现。我很想听到有人使用了不同的方法。无论哪种方式,我希望所有这些对研究 win32k.sys 的漏洞是有帮助的。
下面我们在 win32k!xxxEnableWndSBArrows 的反汇编代码中一睹这个 BUG 的芳容,这是一个相当精妙的 BUG:
未打补丁的情况:
.text:FFFFF97FFF1B157D mov r8d, r13d
.text:FFFFF97FFF1B1580 mov rdx, r14
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar ; 触发usermode callback
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
[...]
.text:FFFFF97FFF1B1519 mov eax, [rbx] ; 引用 tagSBINFO 指针而没有经过检查
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh
.text:FFFFF97FFF1B1520 xor eax, esi
在上面的代码中,Win32K!xxxdrawscrollbar 可以在适当的情况下回调到用户空间,在用户空间代码中 tagSBINFO 的指针可能被攻击者释放掉,再次返回到上面的代码时,0xFFFFF97FFF1B1519 处的代码将引用一个无效的指针。
打补丁的情况:
.text:FFFFF97FFF1D69C3 xor r8d, r8d
.text:FFFFF97FFF1D69C6 mov rdx, rbp
.text:FFFFF97FFF1D69C9 call xxxDrawScrollBar ; 触发usermode callback
.text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h] ; 检查 tagSBINFO 指针是否正确
---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; 如果正确则继续原来的流程
| .text:FFFFF97FFF1D69D7 mov rcx, rbp
| .text:FFFFF97FFF1D69DA call _ReleaseDC
| .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958 ; 跳转到函数退出
|->.text:FFFFF97FFF1D69E4 mov eax, [rbx] ; 放心地使用正确的 tagSBINFO 指针
.text:FFFFF97FFF1D69E6 xor eax, r14d
在上面的补丁版本中,我们看到在使用 tagSBINFO 指针前进行判空。相关的结构信息将在后面给出。
在实现这个利用时,我们进行了多次腐蚀(corruption)。并在其中的一次腐蚀中触发这个漏洞。
造成这个 BUG 的技术根源是一个桌面堆的 UAF(use-after-free)。起先,这个让我很困惑,由于我不熟悉 win32k.sys 的用户模式回调机制,也不清楚它是如何运作的。因此,我认为这是一个锁的条件竞争导致 UAF。事实上,那个结构的锁是被正确使用的,而且其流程是符合预期的。简言之,问题真正的原因是:
就是这些了,不考虑用户模式回调,这一阶段是相当明了的。
但是,我们如何进行腐蚀、为什么要腐蚀呢?正如 Udi 的博客中提到的那样,你能够在某个位置设置或清除 2 比特,而这个位置被系统代码认为是 tagSBINFO 结构中的 WSBflags 字段。这不是正规的 UAF 利用思路,但是文章给出了一个如何操作的提示,我将在下面章节中说明。首先让我们了解如何操纵控制这些比特位。
tagSBINFO 结构(32位和64位是一致的):
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
这个 UAF 漏洞位于 win32k!xxxEnableWndSBArrows() 函数中,该函数用于开启或关闭一个或两个(水平或垂直)滚动条控件的箭头。滚动条控件是一个用于操纵滚动条的特殊窗口。可以通过 CreateWindow() 函数以内置的 "SCROLLBAR" 窗口类为参数来创建它。
win32k!xxxEnableWndSBArrows() 的函数原型:
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);
参数 WSBflags 的含义和在 WinUser.h 中定义的一致,用于表明是哪些滚动条将被操作:
#define SB_HORZ 0
#define SB_VERT 1
#define SB_CTL 2
#define SB_BOTH 3
参数 wArrows 表示箭头的状态是可用还是不可用。被置位表示箭头不可用,否则表示可用。 参数 wArrows 的最低两位表示水平滚动条,接着的两位表示垂直滚动条,其余的位与本次利用的目的无关。
下面的代码取自 win32k!xxxEnableWndSBArrows() 函数,如果 SB_HORZ 或 SB_BOTH 被置位,则设置或取消水平箭头的相关比特位:

这个 BUG 存在于设置水平滚动条和垂直滚动条标志的时候。在刷新水平滚动条之后,一旦该滚动条对应的窗口在桌面上可见时,win32k!xxxEnableWndSBArrows() 函数将调用 win32k!xxxDrawScrollBar(),早先的文章中曾提到这可能触发一个潜在的用户模式回调。
在我们讨论用户模式回调前,继续讨论在调用 win32k!xxxDrawScrollBar() 之后将发生什么。这实际上和水平滚动条有相同的逻辑,仅仅有几个比特位的区别。如果我们选择关闭垂直滚动条,并假定我们触发了 UAF,那么这将会把 2 比特位写入到 tagSBINFO 堆块的某个地方。因此,如果原来的值是 0x2,现在就变成了 0xe。如下图所示。

这一比特的改变足够导致最终的代码执行。我没有深究如何通过清除比特位是完成利用,但它是有可能的。
上面所述的重点是,为了同时能操作水平和垂直滚动条,必须以某种方式创建具有这两个元素的滚动条控件。这个通过调用 CreateWindow() 并设置 WS_HSCROLL 和 WS_VSCROLL 标志来实现。代码如下:
g_hSBCtl = CreateWindowEx(
0, // No extended style
"SCROLLBAR", // class
NULL, // name
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // 垂直+水平
10, // x
10, // y
100, // width
100, // height
g_hSpray[UAFWND], // 无模式父窗口
(HMENU)NULL,
NULL, // window owner
NULL // extra params
);
可以通过下面的代码确保其可见(通常这是缺省的,这里显式调用一下):
result = ShowWindow(g_hSBCtl, SW_SHOW);
滚动条默认是开启的,当我们准备尝试触发漏洞代码时,我们可以设置为滚动条不可用,以便腐蚀我们需要的比特位:
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
尽管我们上面描述了 BUG 的细节以及如何触发相关代码,但是我们依然忽略了最重要的步骤,就是拦截由 win32k!xxxDrawScrollBar() 发起的用户模式回调,以便我们能在 win32k!xxxEnableWndSBArrows() 继续执行前改变堆的内容。我们需要真正触发这个 BUG,但如果不了解任何 Win32k.sys 相关的的情况以及相关的API,就像我开始的样子,这对自己来说就是一次冒险。
先前的文章中有一个很好的调用栈的示意图,深入展示了这个过程,通过 win32k!xxxDrawScrollBar() 进行触发,然后 ClientLoadLibrary() 被调用,并且通过KeUserModeCallback() 进行派发处理。我们需要确实搞清楚 KeUserModeCallback() 的调用情况,以便我们在自己的进程中进行 HOOK。
我发现一些好的论文中或多或少提及了用户模式回调的相关资料。其中涉及到 win32k 的部分是非常有用的: