攻击Windows平台NVIDIA驱动程序-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

攻击Windows平台NVIDIA驱动程序

简介:

前言

现代图形驱动程序是十分复杂的,它提供了大量有希望被利用的攻击面,可以使用具有访问GPU权限的进程(比如Chrome的GPU进程)进行提权和沙箱逃逸。在这篇文章中,你们将看到如何攻击NVIDIA内核模式的Windows驱动程序,以及在此期间我发现的一些bug。我的这项研究是Project Zero的一个20%项目的一部分,在此期间我总共发现了16个漏洞。

内核WDDM接口

图形驱动程序的内核模式组件被称为显示微端口驱动程序。微软的官方文档为我们提供了一个很好的结构图,总结了各个组件之间的关系:

在显示微端口驱动程序 的DriverEntry()函数中,DRIVER_INITIALIZATION_DATA结构被由厂商实现的函数(实际上与硬件进行交互)的回调进行填充,该函数通过DxgkInitialize()传递给dxgkrnl.sys(DirectX的子系统)。这些回调要么由DirectX内核子系统调用,要么在某些情况下直接从用户模式代码调用。

DxgkDdiEscape

一个众所周知的潜在漏洞的入口点是 DxgkDdiEscape 接口。它可以直接在用户模式下被调用,并且可以接受任意数据,该数据以厂商指定的方式(本质上是IOCTL)解析和处理。在后文中,我们将使用术语“逃逸”来表示由DxgkDdiEscape 函数支持的特定命令。

截止写作时,NVIDIA有着数量惊人的400多个逃逸,所以这里也是我花费了绝大部分时间的地方(这些逃逸中的绝大多数是否有必要处在内核空间中是一个问题):


  1. // (这些结构体的名称是我命名的) 
  2. // 表示一组逃逸代码 
  3. struct NvEscapeRecord { 
  4.   DWORD action_num; 
  5.   DWORD expected_magic; 
  6.   void *handler_func; 
  7.   NvEscapeRecordInfo *info; 
  8.   _QWORD num_codes; 
  9. }; 
  10.    
  11. // 有关特定逃逸代码的信息 
  12. struct NvEscapeCodeInfo { 
  13.   DWORD code; 
  14.   DWORD unknown; 
  15.   _QWORD expected_size; 
  16.   WORD unknown_1; 
  17. }; 

NVIDIA为每一个逃逸都单独实现了其私有数据(DXGKARG_ESCAPE 结构体中的pPrivateDriverData),格式为“头部+数据”。头部格式如下:


  1. struct NvEscapeHeader { 
  2.   DWORD magic; 
  3.   WORD unknown_4; 
  4.   WORD unknown_6; 
  5.   DWORD size
  6.   DWORD magic2; 
  7.   DWORD code; 
  8.   DWORD unknown[7]; 
  9. }; 

这些逃逸由32位代码(上面NvEscapeCodeInfo结构体的第一个成员)标识,并根据它们的最高有效字节进行分组(从1到9)。

在处理每个逃逸代码之前都会做一些验证。具体来说,每个 NvEscapeCodeInfo 应当包含头部后面的逃逸数据的预期大小。这将根据NvEscapeHeader中的大小来验证,NvEscapeHeader自身又通过传递给 DxgkDdiEscape的PrivateDriverDataSize字段进行验证。但是,预期大小有时可能为0(通常当逃逸数据为可变大小时),这意味着逃逸处理程序负责进行自身的验证。这将导致一些bug(1,2)。

在逃逸处理程序中发现的大多数漏洞(总共13个)都是些非常基本的bug,例如盲目地向用户提供的指针进行写入操作,向用户模式公开未初始化的内核内存以及不正确的边界检查。还有许多我发现的问题(例如OOB读取)没有报告出去,因为它们似乎没有可以利用的地方。

DxgkDdiSubmitBufferVirtual

另一个有趣的入口点是DxgkDdiSubmitBufferVirtual函数,它首次在Windows 10和WDDM 2.0中被引入,主要用来支持GPU虚拟内存(而旧的 DxgkDdiSubmitBuffer / DxgkDdiRender 函数已被弃用)。这个函数相当复杂,并且还接受来自用户模式驱动程序提交的每一个由厂商特定的数据。我在这里找到了一个bug。

其他

还有一些其他WDDM函数接受厂商特定的数据,但快速浏览后没有发现任何有趣的东西。

暴露的设备

NVIDIA暴露了可由任何用户打开的一些其他设备:


  1. \\.\ NvAdminDevice 

似乎用于 NVAPI。很多ioctl处理程序似乎都会调用DxgkDdiEscape。


  1. \\.\ UVMLite {Controller,Process *} 

可能与NVIDIA的“统一内存”相关。在这里找到1个bug。


  1. \\.\ NvStreamKms 

作为GeForce Experience的一部分默认选择安装,但也可以在安装期间选择停用。不是很明白为什么这个驱动程序是必要的。在这里也发现了1个bug。

更多有趣的Bug

我发现的大多数bug是通过手动逆向和分析得到的,并且使用了一些自定义的IDA脚本。我还写了一个模糊工具。最终结果成功得有点令人惊讶,这也说明了这些bug的简单性。

虽然大多数bug相当无聊(缺乏验证之类的简单案例),但也有一些比较有意思。

NvStreamKms

此驱动程序使用 PsSetCreateProcessNotifyRoutineEx 函数注册进程创建通知回调。该回调检查系统上创建的新进程是否和先前通过发送IOCTL设置的映像名称相匹配。

这个创建通知的例程包含一个bug:

(简化的反编译输出)


  1. wchar_t Dst[BUF_SIZE]; 
  2.    
  3. ... 
  4.    
  5. if ( cur->image_names_count > 0 ) { 
  6.   // info_是传递给例程的PPS_CREATE_NOTIFY_INFO 
  7.   image_filename = info_->ImageFileName; 
  8.   buf = image_filename->Buffer; 
  9.   if ( buf ) { 
  10.     filename_length = 0i64; 
  11.     num_chars = image_filename->Length / 2; 
  12.     // 通过扫描反斜杠来查找文件名 
  13.     if ( num_chars ) { 
  14.       while ( buf[num_chars - filename_length - 1] != '\\' ) { 
  15.         ++filename_length; 
  16.         if ( filename_length >= num_chars ) 
  17.           goto DO_COPY; 
  18.       } 
  19.       buf += num_chars - filename_length; 
  20.     } 
  21. DO_COPY: 
  22.     wcscpy_s(Dst, filename_length, buf); 
  23.     Dst[filename_length] = 0; 
  24.     wcslwr(Dst); 

此例程通过向后搜索反斜杠('\')的方法从PS_CREATE_NOTIFY_INFO的ImageFileName 成员中提取映像名称,然后使用 wcscpy_s 将其复制到堆栈缓冲区(Dst),但传递的长度是计算出的名称长度,而不是目标缓冲区的长度。

即使 Dst 是大小固定的缓冲区,这也不能被视为一个直接溢出。因为它的大小大于255个wchar长度,并且对于大多数Windows文件系统路径组件来说其不能超过255个字符。而因为ImageFileName 是规范化的路径,所以扫描反斜杠在大多数情况下也是有效的。

然而,上述规则可以通过如下方式绕过:对于一个符合通用命名规约(UNC)的路径,其规范化后保持以正斜杠('/')作为路径分隔符(感谢James Forshaw向我指出这一点)。这便意味着我们可以得到一个“aaa / bbb / ccc / ...”形式的文件名从而引发溢出。

例如:


  1. CreateProcessW(L"\\\\?\\UNC\\127.0.0.1@8000\\DavWWWRoot\\aaaa/bbbb/cccc/blah.exe", …) 

另一个有趣的关注点是,跟随受损副本的wcslwr实际上并不限制溢出的内容(唯一的要求是有效的UTF-16编码)。因为计算的filename_length不包含null终止符,所以wcscpy_s 会认为目的地太小,然后以在开始处写入null字节的方式来清除目的地字符串(发生在内容复制到 filename_length 字节之后,因此溢出仍然发生)。这意味着 wcslwr是无用的,因为对 wcscpy_s的调用和一部分的代码从来没有工作过。

利用这个漏洞就不那么复杂了,因为驱动程序没有使用堆栈cookie编译过。在以前的漏洞中附加过一个本地特权提升漏洞利用程序,它配置了一个伪造的WebDAV服务器来利用漏洞(ROP,从主堆栈到用户缓冲区,再次ROP来分配 读写执行内存,用来存放shellcode并跳转进去)。

UVMLiteController中错误的验证

NVIDIA的驱动程序还在 \\.\ UVMLiteController路径中暴露了一个可以由任何用户打开的设备(包括从沙箱中的Chrome GPU进程)。该设备的IOCTL处理程序直接将结果写入Irp->UserBuffer中,作为将要传递给 DeviceIoControl 的输出指针 (微软的文档中指出不要这样做)。IO控制代码指定使用METHOD_BUFFERED,这意味着在Windows内核检查地址提供的范围并将其传递给驱动器之前,用户具有写操作的权限。

然而,这些处理程序还缺少对输出缓冲区的边界检查,这意味着用户模式上下文可以通过任何任意地址传递值为0的长度(可以绕过ProbeForWrite的检查), 这样做的结果是创造出一个受限的Write-what-where情景(这里的“what“仅限于一些特定的值:包括32位0xffff,32位0x1f,32位0和8位0)。

在原始问题中附加了简单的提权漏洞利用 。

远程攻击途径?

考虑到已发现的bug数量如此之众,我做了一个调查,是否可以在不必首先破坏沙盒进程的前提下,完全从远程环境中访问其中任意一个bug(例如通过浏览器中的WebGL或通过视频加速)。

幸运的是结果似乎并非如此。但这并不令人惊讶,因为这里的易受攻击的API是非常底层的,只有经过许多层才能访问得到(对于Chrome而言,需要经历libANGLE -> Direct3D运行时和用户模式驱动程序 ->内核模式驱动程序),并且通常需要在用户模式驱动程序中构造有效的参数才能调用。

NVIDIA的回应

发现的bug的性质表明NVIDIA仍有很多工作要做。他们的驱动程序包含的很多可能不必出现在内核中的代码,而发现的大多数错误是非常基本的错误。事到如今,他们的驱动程序(NvStreamKms.sys)仍然缺乏非常基本的缓解措施(堆栈cookie)。

不过,他们的反应倒是快速且积极的。大多数bug在截止日期之前已经修复好了,并且他们自己内部也在做一些寻找bug的工作。他们还表示,他们一直在努力重构他们内核驱动程序的安全性,但还没有准备好分享任何具体的细节。

时间线

补丁间隔

NVIDIA的第一个补丁,其中包括我报告的6个bug的修复,但是没有在公告中详细说明(发布说明称作“安全更新“)。他们原本计划在补丁发布后一个月再公布详细信息。我们注意到了这一点并告诉他们这样做并不恰当,因为黑客可以通过逆向补丁来找到之前的漏洞,而当大众意识到这些漏洞细节的时候已经晚了。

虽然前6个bug修复后在30多天内都没有发布修复的详细信息,但剩余的8个bug的修复补丁发布后5天内就发布了细节公告。看上去NVIDIA也一直在尝试减少这种差距,但是就最近的公告来看两者的发布仍有很大的不一致性。

结论

鉴于内核中的图形驱动程序所暴露出来的巨大攻击面,以及第三方厂商的低质量代码,它似乎是挖掘沙箱逃逸和特权提升漏洞的一个非常丰富的目标。GPU厂商应该尽快将其驱动代码从内核中转移出去,从而缩小攻击面使得这种情况得以限制。

作者:overXsky
来源:51CTO

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章