详述欺骗性断言如何引发严重的 Windows 内核漏洞 (CVE-2020-0792)

 聚焦源代码安全,网罗国内外最新资讯!

编译:奇安信代码卫士团队

2019年11月,微软发布软件更新,其中对 Windows 内核驱动 win32kfull.sys 的一个小的代码修改引发了一个严重漏洞。该代码修改本应不产生任何危害。从表面看,该修改只是插入了一个断言类型的函数调用来应付某参数中的某些不合法数据。本文将分析相关函数并查看出错的地方。anch0vy@theori 和kkokkokye@theori 报告该漏洞。微软在2020年2月份修复,编号是 CVE-2020-0792。



函数理解

在查看引发该漏洞的代码修改之前,我们首先来讨论下相关函数的运行,它具有侵入性。

该函数是 win32kfull.sys!NtUserResolveDesktopForWOW。前缀 Nt 表明该函数是有时被称为 “Windows Native API”的一个成员,也就是说它是一个高级内核函数,通过 syscall 指令以用户模式调用。我们并不需要理解NtUserResolveDesktopForWOW API (实际上并未记录)的确切目的,我们必须了解的是NtUserResolveDesktopForWOW必须以用户模式调用,而实际的实现存在于低层函数 win32kfull!xxxResolveDesktopForWOW中。函数NtUserResolveDesktopForWOW本身几乎什么都不做。它的主要任务是在用户模式和内核模式之间安全地交换参数和结果数据。

该函数的签名如下:

NTSTATUSNtUserResolveDesktopForWOW(_UNICODE_STRING *pStr)

_UNICODE_STRING* 类型的单一参数是一个 in-out 参数。该调用函数将指针传给用户内存中的 _UNICODE_STRING 结构,最初将充当输入数据的数据填到该函数。在返回前,NtUserResolveDesktopForWOW用新的字符串数据覆写这个用户模式的 _UNICODE_STRING 结构表示结果。

_UNICODE_STRING 结构的定义如下:

MaximumLength 表明,Buffer 以字节为单位分配大小,而 Length 表明在缓冲区中以真实字符串数据字节表示的大小(包括一个空终止符)。

如上所述,NtUserResolveDesktopForWOW的主要目的是在调用xxxResolveDesktopForWOW 时安全地交换数据。NtUserResolveDesktopForWOW函数执行如下对安全至关重要的步骤:


1、接受用户模式下 _UNICODE_STRING* 类型的参数,并验证它是用户模式地址而非内核模式地址的指针。如指向内核模式地址,则会抛出异常。

2、它将 _UNICODE_STRING 的所有字段复制到无法从用户模式访问的本地变量中。

3、从本地变量读取,它验证 _UNICODE_STRING 的完整性。具体而言,它验证 Length 小于 MaximumLength,Buffer 完全存在于用户模式内存中。如其中任意一个测试失败,那么它就会抛出一个异常。

4、再次说明,使用本地变量中的值,它会创建一个新的完全存在于内核模式内存下的 _UNICODE_STRING并指向原始缓冲区的内核模式副本。我们将这种新结构称为 kernelModeString。

5、它将 kernelModeString 传递给底层函数xxxResolveDesktopForWOW。成功完成后,xxxResolveDesktopForWOW将其结果置于 kernelModeString 中。

6、最后,如果xxxResolveDesktopForWOW成功执行,它将xxxResolveDesktopForWOW的字符串结构复制到一个新的用户模式缓冲区中并覆写原始的 _UNICODE_STRING 结构,指向新的缓冲区。

为什么要这么复杂?因为它必须防御的危险是,用户模式进程可能会将一个指针传递到内核内存,或者通过 Buffer 字段或者作为 pStr 参数本身传递。在任意一种事件中,xxxResolveDesktopForWOW将操作从内核内存读取的数据。在这种情况下,通过观察结构,用户模式代码将串联起特定内核模式地址中存在的内容。这将构成从高权限内核模式到低权限用户模式的信息泄露。另外,如果 pStr 本事是一个内核模式地址,则可能会因为 xxxResolveDesktopForWOW 将被写回由 pStr 指向的内存而产生内核内存损坏的后果。

为正确地防御这种情况的发生,仅仅是将指令插入以验证用户模式 _UNICODE_STRING 的做法是不够的。思考一下如下场景:

——用户模式传递指向一个用户模式缓冲区的 _UNICODE_STRING。

——内核代码验证 Buffer 指向用户内存并认为可以安全地继续操作。

——这时,在另外一个线程上运行的用户模式代码修改 Buffer 字段,以便指向内核内存。

——当内核模式代码继续在原始线程时,它将在下次读取 Buffer 字段时使用一个不安全的值。

它是一种 TOCTOU (Time-of-Check Time-of-Use) 类型的漏洞,在这种上下文中,在不同权限级别运行的两端代码访问共享内存被称为 “double fetch”。它是指在上述场景下内核代码执行的两个 fetch。第一个 fetch 检索合法数据,但只有在第二个 fetch 发生时,数据才会被投毒。

解决 double fetch 漏洞的方法是确保内核从用户模式收集的数据仅被 fetch 一次且被复制到无法从用户模式篡改的内核模式状态。这就是上述第2步和第4步操作NtUserResolveDesktopForWOW的原因所在,该函数将 _UNICODE_STRING 复制到内核空间。注意,只有在第2步结束后,才会继续 Buffer 指针的验证,以使验证能够在被复制到防篡改存储区域之后才对数据进行验证。

NtUserResolveDesktopForWOW 甚至将字符串缓冲区本身复制到内核内存中,这是消除和可能的 double fetch 相关联的所有问题的真正安全的方法。MaximumLength 表明,当把内核模式缓冲区分配给字符串数据时,它分配的是和用户模式缓冲区大小一样的缓冲区。之后复制字符串的真实字节。要使该操作安全进行,需要确保 Length 不大于 MaximumLength。上述第3步也包含了这个验证步骤。

尽管如此,但我应该说该函数的签名是:

NTSTATUS NtUserResolveDesktopForWOW(volatile _UNICODE_STRING*pStr)

关键字 volatile 警告编译器,外部代码可能随时修改 _UNICODE_STRING 结构。如没有 volatilve,则 C/C++ 编译器本身可引发不存在于源代码中的 double fetches。这就是另外一个故事了。

漏洞详述

漏洞存在于第3步的验证中。在2019年11月发布软件更新前,验证代码如下:


MmUserProbeAddress是一个全局变量,其中包含一个划定用户控件和内核空间界限的地址。与该值的比较用于确定地址是指向用户空间还是内核空间。

代码*(_Byte*) MmUserProbeAddress = 0用于抛出异常,因为该地址永远不可写。

如上所示代码运行正常。但是,在2019年11月,微软做出了微小的更改:

注意,length_ecx 只是我为将 Length 字段复制到其中的局部变量指定的名称。此局部变量的存储恰好是 ecx 寄存器,因此取名。

我们可以看到,该代码在做其它检查之前先做了一个验证检查:确保 length_ecx &1 为0,也就是说确保指定的 Length 是一个偶数。Length 为奇数是无效的,这是因为 Length 指定了字符串占用的字节数。由于字符串中的每个 Unicode 字符均由2个字节的序列表示,因此在进行其它检查前,它可以确保 Length 是偶数,而且如果检查失败,则正常处理停止,转而使用断言。

但是,果真如此吗?

这就是问题所在。事实证明,函数MicrosoftTelemetryAssertTriggeredNoArgsKM根本就不是断言!断言会抛出一个异常,但该函数仅生成一些遥测数据发送至微软,之后返回给调用方。遗憾的是,”Assert” 这个词出现在函数名称中,而实际上这个名称似乎导致微软内核开发人员增加了对 length_ecx 的检查。开发人员似乎认为调用这个函数会终止对当前函数的执行,因此其余检查可以安全地转移给 else 字句。这意味着通过给 Glength 指定一个奇数值,我们可以跳过所有其它检查。

这个问题有多严重呢?非常糟糕。回想一下,为了确保最大的安全性,NtUserResolveDesktopForWOW将字符串数据本身复制到一个内核缓冲区。它分配的内核缓冲区和原始的用户缓冲区大小也就是 MaximumLength 一样。之后根据 Length 中指定的数字复制字符串的字节。因此为了避免出现缓冲区溢出问题,必须增加验证,确保Length 不大于 MaximumLength。如果我们可以跳过该验证,那么就会在内核内存中直接看到缓冲区溢出。

因此,在这种极具讽刺意味的情况下,安全检查稍有缺陷的组合所产生的结果可能比代码最初需要防范的结果更加可怕。只需通过为 Length 字段指定一个奇数值,攻击者就可以在内核池分配结束后写入任意字节序列。

如有兴趣,可自己通过如下的 PoC 代码尝试:

它会分配大小为2的内核池缓冲区并尝试从用户内存复制 0xffff 字节。感兴趣的读者可以在启动了 Special Pool 的 win32kfull.sys 上运行以确保可预测的崩溃情况。

结论

微软在2020年2月修复了这个漏洞问题。该补丁的实质是,代码现在在调用MicrosoftTelemetryAssertTriggeredNoArgsKM函数之后会直接抛出一个异常。这是通过写入*MmUserProbeAddress完成的。即使微软将此列为对“Windows 图形组件”的更改,但仍引用 win32kfull.sys 内核驱动,该驱动在渲染图形中起着至关重要的作用。





推荐阅读

奇安信代码卫士帮助微软修复Windows 内核漏洞,获官方致谢和奖金

奇安信代码卫士帮助微软修复Edge浏览器和Windows内核高危漏洞,获官方致谢和奖金

原文链接

https://www.thezdi.com/blog/2020/5/7/how-a-deceptive-assert-caused-a-critical-windows-kernel-vulnerability

题图:Pixabay License

本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 www.codesafe.cn”。

奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的

产品线。

    点个 “在看” ,加油鸭~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值