pgain2004 发表于 2017-2-17 21:59

[渣翻]针对Win系统NVIDIA驱动的攻击 嘲讽by Google[fix ver1]

本帖最后由 pgain2004 于 2017-2-19 05:21 编辑

原文:Project Zero: Attacking the Windows NVIDIA Driver
由Oliver Chang发布
由pgain2004翻译

当代显卡驱动的复杂性为提权攻击和从有权限访问GPU的进程(如Chrome的GPU进程)中进行沙箱逃逸提供了相当一块攻击面。本文将探讨针对Win系统下NVIDIA内核驱动的破解,说说我找到的一些漏洞。这次研究属于Project Zero里我的20%项目,共发现了16个漏洞。

WDDM内核接口
显卡驱动的内核态组件被称为display miniport驱动。微软的文档对不同部件间的关联做了不错的图文汇总:
https://ws1.sinaimg.cn/large/73508f0fgy1fcup8tb1ajj20ce07ewef
display miniport驱动的DriverEntry()里,一个DRIVER_INITIALIZATION_DATA结构被与硬件沟通的供应商函数的回调占据,随后由DxgkInitialize()递交给dxgkrnl.sys(DirectX子系统)。这些回调要么被DirectX的内核子系统调用,要么被用户态的代码直接调用。

DxgkDdiEscape
这里较知名的一个切入点是DxgkDdiEscape接口。其可由用户态直接调用,接受从供应商方面解析处理的任何数据(本质上就是个IOCTL)。下文将用“escape”指代基于DxgkDdiEscape函数的特殊指令。
NVIDIA目前写了400余个escape,这部分花了我大部分时间(在内核里加这么多escape的必要性值得质疑):
https://ws1.sinaimg.cn/large/73508f0fgy1fcup82dxn9j20lm0fhmym
// (names of these structs are made up by me)
// Represents a group of escape codes
struct NvEscapeRecord {
DWORD action_num;
DWORD expected_magic;
void *handler_func;
NvEscapeRecordInfo *info;
_QWORD num_codes;
};

// Information about a specific escape code.
struct NvEscapeCodeInfo {
DWORD code;
DWORD unknown;
_QWORD expected_size;
WORD unknown_1;
};
NVIDIA将各escape的private data(DXGKARG_ESCAPE结构里的pPrivateDriverData)做成header结构+数据,格式如下:
struct NvEscapeHeader {
DWORD magic;
WORD unknown_4;
WORD unknown_6;
DWORD size;
DWORD magic2;
DWORD code;
DWORD unknown[7];
};
这些escape由一段32位代码(上面NvEscapeCodeInfo结构里的第一组)定义,按照最高有效字节从1到9进行分组。
在处理escape代码前会先进行一些验证,各NvEscapeCodeInfo包含对应escape的header后附带数据的期望大小,用来验证NvEscapeHeader的大小,而NvEscapeHeader本身则用来验证提交给DxgkDdiEscape的PrivateDriverDataSize域。可是,该期望大小可能为0(通常用于数据是变长的时候),此时由escape handler自身负责验证。由此产生了一些漏洞(1,2)。
在escape handler发现的大部分问题(共13个)都非常低级,例如盲目地向用户提供的指针进行写入,向用户态暴露未初始化的内核内存和错误的边界判定。还有一堆问题(如越界读取)我没上报,因为它们不太能被利用到。

DxgkDdiSubmitBufferVirtual
另一个有趣的入口点是Win10和WDDM2.0(其不提倡旧有的DxgkDdiSubmitBuffer/DxgkDdiRenderfunctions)用来支持GPU虚拟显存的新增函数,DxgkDdiSubmitBufferVirtual。它相当复杂,同样在每次提交指令时从用户态驱动接收厂商专用数据。其中一个漏洞是在这发现的。

其它
还有其它几个WDDM功能函数会接收供应商相关数据,不过简单审查后没发现啥值得关注的。

被暴露的设备
NVIDIA还令一些额外设备可被任意用户访问:
- \\.\NvAdminDevice,被NVAPI调用。许多ioctl处理程序似乎都调用到DxgkDdiEscape。
- \\.\UVMLite{Controller,Process*},可能和NVIDIA的“统一内存”相关。和一个漏洞相关。
- \\.\NvStreamKms,GeForce Experience默认安装组件,可在安装时撤销。必要性不明。和另一个漏洞相关。

更有趣的漏洞
我是配合一些自定义IDA脚本,用手动逆向解析发现大部分漏洞的。结果还写了个简单但意外有效的Fuzzer(查漏工具)。
大部分漏洞都很无趣(诸如没验证之类的小儿科),但也有些挺好玩的。

NvStreamKms
该驱动会利用PsSetCreateProcessNotifyRoutineEx函数注册一个进程创建通知回调。该回调将检查系统中的新进程是否符合此前提交IOCTLs设定的映像名。
这个创建通知流程包含了一个漏洞:
(Simplified decompiled output)
wchar_t Dst[BUF_SIZE];

...

if ( cur->image_names_count > 0 ) {
// info_ is the PPS_CREATE_NOTIFY_INFO that is passed to the routine.
image_filename = info_->ImageFileName;
buf = image_filename->Buffer;
if ( buf ) {
    filename_length = 0i64;
    num_chars = image_filename->Length / 2;
    // Look for the filename by scanning for backslash.
    if ( num_chars ) {
      while ( buf[num_chars - filename_length - 1] != '\\' ) {
      ++filename_length;
      if ( filename_length >= num_chars )
          goto DO_COPY;
      }
      buf += num_chars - filename_length;
    }
DO_COPY:
    wcscpy_s(Dst, filename_length, buf);
    Dst[filename_length] = 0;
    wcslwr(Dst);
流程中逆向检索‘\’,将映像名称从PS_CREATE_NOTIFY_INFO的ImageFileNamemember处提取,再用wcscpy_s复制到栈缓冲区(Dst),所传递长度却是名字的长度,而非目标缓冲区的长度。
就算Dst是固定大小的缓冲区,这也不会简单地溢出。Win的大部分系统路径都不允许超过255字,加上ImageFileName属于已规范化路径,所以对反斜杠的搜索一般都是有效的。
然而,传递一个被规范化后仍保留正斜杠作分隔符的UNC路径却是可能的(感谢James Forshaw告知)。于是我们可以通过“aaa/bbb/ccc/...”格式的文件名制造溢出。
例:CreateProcessW(L"\\\\?\\UNC\\127.0.0.1@8000\\DavWWWRoot\\aaaa/bbbb/cccc/blah.exe",…)
除此之外,跟随在坏样本后的wcslwr实际上并未限制溢出的内容(只要求是UTF-16),这点也很有趣。由于计算后的文件名长度不包括null terminator,wcscpy_s认为目的缓冲区不够大,就在开头写入空字节,清空目标字符串(在复制内容到文件名长度字节后进行,故仍会发生溢出)。由于错用wcscpy_s,wcslwr变得无效,这部分代码也就根本没正常工作过。
既然驱动编译时没开栈cookie,被拿来用也是自然的了。原issue里附带了一个通过建立假WebDAV服务来利用漏洞的本地提权利用代码(ROP,迁移栈指针到用户缓冲区,再ROP分配一段可读可写可执行的含shellcode内存,跳过去)。

UVMLiteController内的错误验证
NVIDIA驱动还在\\.\UVMLiteController处出现能被任意用户(包括从沙箱化的Chrome GPU进程)访问设备的漏洞。该设备的IOCTL handler将结果直接写入Irp->UserBuffer,即传给DeviceIoControl的输出指针(微软的文档反对反对这么干)。IOCTL code里指定了METHOD_BUFFERED,意味着Windows内核在把IOCTL传递给驱动前会检查用户提供的这个地址范围是否用户态可写。
这些handler缺乏对输出缓冲区的边界检查,意味着可以传长度为0并指定一个任意地址(这样能通过ProbeForWrite的检查),导致一个有限制的write-what-where状态(这里的“what”限定为32-bit 0xffff、32-bit 0x1f、32-bit 0、and 8-bit 0在内的特定值)。
原issue里也给了一段简单的提权漏洞利用代码。

远程攻击的可能性?
发现了如此数量的bug,我开始研究其中是否有不用先搞定沙箱,能从远程环境进行的(例如通过浏览器的WebGL或视频软件加速)。
好在答案为“否”。也不奇怪,毕竟这些漏洞API都相当底层,必须先经过多层(对于Chrome就是libANGLE -> Direct3D运行环境和用户态驱动 -> 内核态驱动)来访问,通常由用户态驱动里的合法参数调用。

NVIDIA的回应
上面发现的漏洞情况说明NVIDIA有可观活要干了。其驱动包含大量可能不该在放内核的代码,被发现的漏洞多数是些低级错误。其中一个驱动(NvStreamKms.sys)到现在都没做最基本的缓解处理(栈cookie)。
不该他们的反应迅速而积极。大部分漏洞在期限内得到很好的修复,内部还额外查出了一些漏洞。他们还说正基于安全原则重构内核驱程,但仍不能分享具体细节。

时间线
2016-07-26向NVIDIA反馈了第一个bug。

2016-09-21在372.90版中悄悄修复了其中6个漏洞。和NVIDIA讨论了补丁空窗期问题。
2016-10-23修复当时(375.93)剩余的共14个漏洞。
2016-10-28发布P0漏洞相关说明公告。
2016-11-04   https://bugs.chromium.org/p/project-zero/issues/detail?id=911漏洞未被正确修复,告知NVIDIA。
2016-12-14发布针对911漏洞的修复补丁和对应公告。
2017-02-14修复最后两个漏洞。


补丁空窗期
NVIDIA的首个补丁修复了我汇报的6个漏洞,但没写具体公告(发布记要只说是“安全更新”),打算延后一个月再公开细节。我们得知后,告诉对方这不是个好做法,攻击者可以在公众察觉前逆向补丁,创造出很大的空窗期。
后面8个漏洞是在公告出来前5天发的补丁。NV貌似也想减少空窗期,但就近期的公告来看实在是有点反复。

总结
由于显卡驱动提供了很大的攻击面,第三方代码普遍渣,就成了沙箱逃逸和EoP非常好的攻击目标。GPU厂商应试着将这些弱点尽量挪出内核,限制攻击手段。


看http://bbs.saraba1st.com/2b/thread-1478082-1-2.html 9楼提及,就兴起翻了。
非IT专业,100%确定有很多错漏,特别是对专有概念的错误理解,望不吝指正。

qwased 发表于 2017-2-17 22:22

怪不得372.90开始驱动不稳定的一笔,原来在修底层的漏洞?

—— 来自 Jiayu S3, Android 7.1.1上的 S1Next-鹅版

马金葛 发表于 2017-2-17 23:26

也就是说老黄去年的驱动问题多多和在花功夫解决这些问题有关吗?

eroneko 发表于 2017-2-17 23:31

我看第一段还以为都是机翻

—— 来自 Xiaomi Redmi Note 3, Android 7.1.1上的 S1Next-鹅版

pgain2004 发表于 2017-2-17 23:46

eroneko 发表于 2017-2-17 23:31
我看第一段还以为都是机翻

—— 来自 Xiaomi Redmi Note 3, Android 7.1.1上的 S1Next-鹅版 ...

请赐教……

cuda 发表于 2017-2-18 00:10

起码有驱动,linux下那个,天呐

Geminize 发表于 2017-2-18 00:27

烈之斩 发表于 2017-2-18 04:27

你这图床不支持外链。

pgain2004 发表于 2017-2-18 16:25

烈之斩 发表于 2017-2-18 04:27
你这图床不支持外链。

一开始直接用了退酷的图,现在换上微博图床的了。

258921 发表于 2017-2-19 02:50

本帖最后由 258921 于 2017-2-19 02:51 编辑

pgain2004 发表于 2017-2-17 23:46
请赐教……
谨慎怀疑Google Translate的翻译质量真的会比你的好。

当代显卡驱动的复杂性为EoPs和访问GPU进程(如Chrome的GPU进程)的沙箱逃逸提供了一块很大的空子。
EoPs: Elevation of Privileges, 也就是通常说的权限提升,或者提权。
“和访问GPU进程的沙箱逃逸”: 这句子错了不说还读不通,应该是“和从有权限访问GPU的进程(如Chrome的GPU进程)中进行沙箱逃逸”
另外把攻击面这个词翻成了“空子”让人觉得……呃……就算是为了通俗化也不应该如此吧?

这次研究属于Project Zero中20%项目的一部分,该项目共发现了16个漏洞。
原文的意思是这项研究是他本人的一个20%项目,到了你这怎么就成Project Zero中20%项目了……

显卡驱动的内核态组件被称为显示微端口驱动
对待这个miniport最好的办法就是不翻,你翻了也不能使能看懂的人数增加(反而可能减少)。

显示微端口驱动的DriverEntry()里,一个初始驱动数据结构被与硬件沟通的供应商函数的回调所污染,随后由DxgkInitialize()递交给dxgkrnl.sys(DirectX子系统)。这些回调扫描要么被DirectX的内核子系统调用,要么被用户态的代码直接调用。
DRIVER_INITIALIZATION_DATA是一个结构体的名字,那你咋不把DriverEntry也给翻译成中文咧???
污染:“填充”
与硬件沟通的供应商函数: “厂商实现的与硬件沟通的函数”
回调扫描:???

这里较知名的一个切入点是DxgkDdiEscape接口。
还是直接翻成入口点比较好。

访问从供应商方面解析处理的任何数据
accepts arbitrary data that is parsed and handled in a vendor specific way
访问从哪来的?

NVIDIA将各escape和对应私有数据(DXGKARG_ESCAPE结构里的pPrivateDriverData)做成header与内部参数
这句话说的是每个escape的private data的形式都是一个header跟上对应的数据……

由其关键字节(从1到9)进行分组
most significant: 最高有效字节
按照最高有效字节从1到9分成9组。

每个NvEscapeCodeInfo还特地包含对header内数据大小的预测
每个NvEscapeCodeInfo包含对应的escape里header后面附带的数据期望的大小。
预测是什么鬼??
另外整个这一段里的验证关系也没说清楚。

然而预测值可能为0(通常在escape数据将被预测值时发生),即该escape处理器对自身进行了验证。
这个期望大小可能为0(通常用于数据是变长的时候),此时由escape handler自身负责验证。

OOB读取
OOB: Out of Bounds, 越界。

不支持旧有的DxgkDdiSubmitBuffer/DxgkDdiRenderfunctions
deprecating不是就立刻不支持了……

受影响设备
exposed就是暴露,这里说的是暴露出来能被用户态访问到的“设备”。

将映像名称从PS_CREATE_NOTIFY_INFO的ImageFileNamemember处解压出来
extract: 提取。

再用wcscpy_s复制到堆栈缓冲区(Dst)
“堆栈”这个坏毛病不知道谁发明的,直接说栈。

所递交长度却是名字的计算长度
你平时是说递交参数还是说传递参数?
计算出来的名字的长度。计算长度是什么?

Win的大部分系统路径组都不允许超过255字,加上ImageFileName属于canonicalised路径,所以对反斜杠的搜索一般都是有效的。
组:你要是硬要管C:\aa\bb\cc\dd\ee里的aa、bb、cc、dd、ee都叫一个路径“组”那我也没办法。
canonicalised: 规范化-ed
UNCpath: 就是UNC路径。

然而,被canonicalised后,使用正斜杠作为分隔符的UNCpath却可能被通过(感谢James Forshaw告知)。
It is however, possible to pass a UNC path that keeps forward slash (‘/’) as the path separator after being canonicalised (credits to James Forshaw for pointing me to this).
然而可以传递一个被规范化后仍会保留正斜杠作为路径分隔符的UNC路径。

除此之外,跟随在坏样本后的wcslwr实际上并未限制内容溢出(只要求是UTF-16),这点也很有趣。
bad copy说的就是上面那个写错了的wcscpy_s,样本是什么……最起码也翻成拷贝吧?
另外是“实际上并未限制溢出的内容”而不是“限制内容溢出”,结合后面的只要求是UTF-16是不是发现顺多了?

wcscpy_s会认为目的区不够大,进而先用空字节清掉目标目标字符串(在复制内容到文件名长度字节后进行,故仍会发生溢出)
“先”字太多余了吧。整个过程是这样的,你理解一下再看看翻译的就知道有什么问题了:
1. wcscpy_s进行拷贝,由于程序员喝多了传错了参数,导致了可以溢出。
2. wcscpy_s在拷贝到最后的时候,发现目的缓冲区不够大,于是在目的缓冲区开头写了个0把它变成空串。

wcscpy_s的调用和部分代码不起效,wcslwr也随之废掉了。
This means that the wcslwr is useless because this wcscpy_s call and part of the code never worked to begin with.
这是说由于这个wcscpy_s(写错了)的原因,wcslwr其实根本就没用(你对着一个空串调当然没用了),这部分代码从来就没有正常工作过。(就是说这部分代码不止是有漏洞,本来功能上就是坏的)

这是挺琐碎的事,毕竟驱动也不像千禧年前那样和堆栈cookie一起编译。
翻译反了……
由于驱动编译的时候没有开栈cookie,利用方法是显然的。(类似于证明是显然的)

原问题链接里就提到用本地提权建立假WebDAV服务来暴露弱点(ROP,用户缓冲区枢栈,重新ROP来分配包含shellcode的rwx mem并跳转过去)的事。
issue中附带了一个通过建立一个假的WebDAV服务来利用漏洞的本地提权利用代码(ROP,迁移栈指针到用户缓冲区,再ROP分配一段可读可写可执行的包含shellcode的内存,跳过去)。

效果是本地提权,也就是开头的EoP。后面括号里说的是具体的exploit步骤。

IOcontrol代码阐释了METHOD_BUFFERED,由此Windows内核将认为提交上来的地址范围在送给驱动前可以由用户进行改写。
IOCTL code里指定了METHOD_BUFFERED,意味着Windows内核在把IOCTL传递给驱动之前会检查用户提供的这个地址范围是不是用户态可写的。

这些处理器缺乏对输出缓冲的边界检查,用户环境下可以提交一个包含任何地址的0长度结果(略过ProbeForWrite检查)来造成一个受限的write-what-where状态。
然而,这些handler缺乏对输出缓冲区的边界检查,意味着用户态可以传长度为0并指定一个任意地址(这样能通过ProbeForWrite的检查),导致一个有限制的write-what-where primitive。

原问题里也描述了一个简单的提权漏洞。
里面带的不是“描述了一个简单的提权漏洞”,而是一个简单的利用这个漏洞的漏洞利用代码。

我开始研究其中是否有能绕过沙箱,从远程环境进行的(例如通过浏览器的WebGL或视频软件加速)。
这里的意思是是否有能不用先搞定一个沙箱内的进程的方法。

其中一个驱动(NvStreamKms.sys)到现在都没做最基础的缓冲处理(cookies堆栈)。
最基本的缓解处理。
栈cookie。

pgain2004 发表于 2017-2-19 05:19

本帖最后由 pgain2004 于 2017-2-19 05:22 编辑

258921 发表于 2017-2-19 02:50
谨慎怀疑Google Translate的翻译质量真的会比你的好。



确实犯了一堆低级错误,Google机翻都更好。非专业还各种想当然,另一些则纯粹是翻得眼晕看错词了。
总之谢指正,你提的部分都修正了,下次我会彻底查清相关概念并复查一两次后再发的,这次太马虎了。

treexper 发表于 2017-2-19 06:03

先为楼主的辛勤劳作点个赞。不过我觉得这些没什么必要翻译,不说英语有多溜,愿意看完这个全文的能有几个英语阅读有障碍的。

zatsuza 发表于 2017-2-19 13:38

这些代码有可能还是xpdm时代DrvEscape里的残留...
页: [1]
查看完整版本: [渣翻]针对Win系统NVIDIA驱动的攻击 嘲讽by Google[fix ver1]