Windows 7 的安全漏洞,尤其是在内核驱动层面的缺陷,始终是安全研究人员密切关注的重点。本文讨论的是一例典型的 win32k.sys 漏洞,该漏洞在 Windows 7 RC 版本(7100,090421)中被发现,影响范围较为广泛。漏洞位于 NtUserGetDc 和 NtUserGetDcEx 这两个函数中,核心问题在于共享临界锁使用不当,导致任意权限的 GDI 程序都能触发系统蓝屏(BSOD),从而实现拒绝服务攻击。
win32k.sys 是 Windows 的图形驱动程序(GDI 驱动),负责大量图形界面处理任务。由于代码中许多部分继承自 Windows 3.1 时代并经过多次修改,历史包袱沉重,因此漏洞数量较多。在 Windows Vista 中,进入这两个函数前会调用 UserEnterUserCritSec 进入临界区,同时将 gptiCurrent 设置为当前线程的 WIN32THREAD。该函数的具体实现如下:
PWIN32THREAD UserEnterUserCritSec()
{
PWIN32THREAD pwin32Thread;
pwin32Thread = ExEnterPriorityRegionAndAcquireResourceExclusive(gpresUser);
gptiCurrent = pwin32Thread;
gbValidateHandleForIL = 1;
return pwin32Thread;
}
ExEnterPriorityRegionAndAcquireResourceShared 是 NTOSKRNL 导出的共享资源锁函数,它锁定 gpresUser 资源,并返回当前线程的 Win32Thread(即 KeGetCurrentThread->Win32Thread)。对应的独占版本 ExEnterPriorityRegionAndAcquireResourceExclusive 功能类似。在 Windows XP 中,则是先调用 KeEnterCriticalRegion,再用 ExAcquireResourceExclusiveLite 锁定 gpresUser,然后通过 PsGetCurrentThread->PsGetThreadWin32Thread 获取线程的 win32kthread,并存储到 gptiCurrent 中。
然而,到了 Windows 7,这两个函数进入前变更为调用 UserEnterSharedCrit,进入共享临界区。其实现非常简单:
PWIN32THREAD EnterSharedCrit()
{
return ExEnterPriorityRegionAndAcquireResourceShared(gpresUser);
}
注意,这里不再设置 gptiCurrent。实际上,NtUserGetDc 和 NtUserGetDcEx 会将这个函数返回的指针保存在寄存器中,以便后续使用 CurrentWin32Thread 中的数据。从独占临界改为共享临界,本意是提升效率并减少锁竞争,但这种修改存在缺陷。
为什么是缺陷?因为在共享临界模式下,没有修改 gptiCurrent,很可能遇到 gptiCurrent 处于非法状态的情况。例如,如果一个线程没有执行过 PsConvertToGuiThread,那么 gptiCurrent 就会为空。而 WIN32K 中有大量内部例程(通常以 zzz、xxx 开头),它们都假设调用自身之前 gptiCurrent 是有效的。其中就包括 xxxDestroyWindow。
一个典型的蓝屏触发路径是:NtUserGetDc → GetWindowDc → GetDcEx → SpbCheckDce → SpbCheckRect → SpbCheckRect2 → FreeSpb。当调用到 FreeSpb 时,需要释放一个 spb 对象,随后进入 FreeSpb → HMAssigmentUnlock → HMUnlockObject → HMUnlockObjectInternal → HMDestroyUnlockedObject,最终调用到 ganti 表中的函数。由于这里是 GetWindowDc,因此会调用 xxxDestroyWindow。xxxDestroyWindow 的第一行代码便无检查地引用了 gptiCurrent 指针中的数据,自然直接导致 BSOD。
解决方案
等待微软发布官方补丁。微软可能的修补方向有两个:
- 牺牲效率,将
NtUserGetDc等函数中的共享锁替换回独占临界锁。 - 或者,将所有
NtUserGetDc等函数可能调用的内部函数中的gptiCurrent引用去除,改为不依赖gptiCurrent的其他实现。
无论采用哪种方式,该漏洞的根源都在于设计上的“优化”忽略了共享锁下的线程安全性。对于安全研究人员而言,这也提醒我们:在追求性能的同时,绝不能忽视那些看似微小的状态一致性假设。
