Discovering Nvidia NvBackend endpoint
I regularly look at what’s installed on my home computers, especially when it’s come to third-party editors. I trust OS companies (Apple, Google, Microsoft) to ship relatively secure software, but much less for the others. In this state of mind, I looked at which processes bind server on my machine :
There is an unknown process which opens a listening socket on 127.0.0.1:23401
. Since it’s bound on localhost, you can’t actually access it from a host on the same LAN, only on the machine itself. However it might be possible to forge cross-origin requests from the browser if the server is not configured properly.
NvBackend.exe
is part of files installed by Nvidia, and it’s description is NVIDIA Update Backend
which initially triggers my interest. An Update backend server that listen to socket requests : that’s a recipe for disaster.
C:\Program Files (x86)\NVIDIA Corporation\Update Core\NvBackend.exe
is a 32-bit application which is written mainly in C++ and has ~5000 functions detected by IDA. You just can’t static reverse your way to the server handler here, which is an error I regularly see among juniors RE people. Before renaming every functions found, we need to have a better understanding of how we can interact with it (usually done in blackbox).
TLDR : Nvidia expose it’s updater APIs via XML-RPC over HTTP (on localhost) which sounds really bad, but thanks to CORS
policy being rolled out by default in modern web browsers we avoid the worst.
Reverse
Let’s try to ping it first :
>>> import requests
>>> r = requests.get("http://localhost:23401")
Traceback (most recent call last):
File "C:/Python36/lib/site-packages/urllib3/connectionpool.py", line 600, in urlopen
chunked=chunked)
File "C:/Python36/lib/site-packages/urllib3/connectionpool.py", line 384, in _make_request
six.raise_from(e, None)
File "<string>", line 2, in raise_from
File "C:/Python36/lib/site-packages/urllib3/connectionpool.py", line 380, in _make_request
httplib_response = conn.getresponse()
File "C:/Python36/lib/http/client.py", line 1331, in getresponse
response.begin()
File "C:/Python36/lib/http/client.py", line 297, in begin
version, status, reason = self._read_status()
File "C:/Python36/lib/http/client.py", line 279, in _read_status
raise BadStatusLine(line)
http.client.BadStatusLine: HTTP 400 Bad request
Tough luck, let’s try POST requests then :
>>> import requests
>>> r = requests.post("http://localhost:23401")
>>> r
<Response [200]>
>>> r.headers
{'Server': 'NVIDIA Stream HTTP server/2008', 'Content-Type': 'text/xml', 'Content-Length': '533'}
>>> print(r.text)
"""
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>12</int></value>
</member>
<member>
<name>faultString</name>
<value><string>data invalid or corrupted in 'RPC::PoppedDataConvertion; broken XML header'
IO error [#0] while [Sock::RecvTm]
</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>
"""
Yes ! We get an answer, and better this is a custom error message. It looks like we have a XML-RPC server endpoint on the other end. Now we search for a matching string in the binary :
Now we have an entry point where we can set up a breakpoint and explore dynamically using a debugger (windbg in my case, but whatever userland debugger will fit the bill). Walking back the stack trace on IDA , we end up on this class (method names are my own) :
rdata:00557A40 dd offset ??_R4BaseUserData@updtSTREAM@@6B@ ; const updtSTREAM::BaseUserData::`RTTI Complete Object Locator'
.rdata:00557A44 ; const updtSTREAM::BaseUserData::`vftable'
.rdata:00557A44 ??_7BaseUserData@updtSTREAM@@6B@ dd offset sub_471C9B
.rdata:00557A44 ; DATA XREF: sub_4775CF:loc_471C94↑o
.rdata:00557A44 ; sub_471C9B+A↑o ...
.rdata:00557A48 dd offset ??_R4updtSTREAMRPC@@6B@ ; const updtSTREAMRPC::`RTTI Complete Object Locator'
.rdata:00557A4C ; const updtSTREAMRPC::`vftable'
.rdata:00557A4C ??_7updtSTREAMRPC@@6B@ dd offset __xml_rpc_push_data_convert
.rdata:00557A4C ; DATA XREF: __xml_rpc_free+11↑o
.rdata:00557A4C ; sub_471CFD+21↑o ...
.rdata:00557A50 dd offset __xml_rpc_pop_data_convert
.rdata:00557A54 dd offset sub_432631
.rdata:00557A58 dd offset __xml_rpc_free
.rdata:00557A5C dd offset __xml_rpc_parse_data
.rdata:00557A60 dd offset __xml_rpc_write_answer
.rdata:00557A64 dd offset sub_476C56
.rdata:00557A68 dd offset sub_472803
.rdata:00557A6C dd offset sub_471D64
.rdata:00557A70 dd offset sub_471DB5
.rdata:00557A74 dd offset sub_471DBF
.rdata:00557A78 dd offset sub_477B93
.rdata:00557A7C dd offset sub_472B78
.rdata:00557A80 dd offset sub_479BB6
.rdata:00557A84 align 8
.rdata:00557A88 ; wchar_t aCDvsP4BuildSwR
Our breakpoint is triggered inside __xml_rpc_write_answer
, but the method right before is much more interesting __xml_rpc_parse_data
. Setting a breakpoint on this function allows us to break the application on new client requests
:
signed int __thiscall _xml_rpc_parse_data(_DWORD *this, int a2, wchar_t *a3, int a4)
{
// [...]
v10 = (_DWORD *)(40 * v6[32] + v9 + 36);
if ( !*v10 )
{
// [...]
if ( *(_DWORD *)(*(_DWORD *)a2 + 20) < 0x10u )
v17 = *(const char **)a2;
else
v17 = *(const char **)v16;
v18 = strstr(&v17[*((_DWORD *)v16 + 7)], "</methodName>");
if ( !v18 )
{
v37 = L"RPC::PoppedDataConvertion; broken XML header";
goto LABEL_41;
}
v20 = *(const char **)a2;
if ( *(_DWORD *)(*(_DWORD *)a2 + 20) < 0x10u )
v21 = *(const char **)a2;
else
v21 = *(const char **)v20;
__methodName_anchor = strstr(&v21[*((_DWORD *)v20 + 7)], "<methodName>");
v40 = __methodName_anchor;
if ( !__methodName_anchor || v18 <= __methodName_anchor )
{
v37 = L"Error in request, queue error";
_send_error_message(v35, (int)v36, v37);
return 0;
}
// [...]
return result;
}
Just looking at strings inside the function, it’s pretty clear it expect a <methodName>
xml node. Let’s try it :
>>> import requests
>>> r = requests.get("http://localhost:23401", data='<?xml version="1.0"?>\n<methodName>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</methodName>')
# well we might get lucky, init ?
>>> r
<Response [200]>
>>> print(r.text)
"""
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>8</int></value>
</member>
<member>
<name>faultString</name>
<value><string>element not found [XMLRPC's Secret header in HTTP]
</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>
"""
The error has changed : we’ve advanced in our reversing ! The error message is pretty interesting since it imply there is a secret seed/token the client must set in its request header. Unfortunately, we don’t even know the custom header we need to use. That’s where it’s important to be efficient using a debugger. I’ve located a strcmp
and set a breakpoint that dump every string compared :
0:006> bp NvBackend+0x70f3 ".printf /D \"%mu=%mu \\n\", ecx, eax;g" // this is a pretty fugly one-liner, but it does the job.
0:006> g
23401=23401
23401=23401
/crossdomain.xml=/
/=/crossdomain.xml
/=/
/=/
X-NVRPC-SECRET=Host
X-NVRPC-SECRET=User-Agent
X-NVRPC-SECRET=Accept-Encoding
X-NVRPC-SECRET=Accept
X-NVRPC-SECRET=Connection
X-NVRPC-SECRET=Content-Length
DF1FCCCC6288WG92ABEE8GG0062DB6=
Interestingly, the xml-rpc server checks the path is either equal to /
or /crossdomain.xml
. The latter value smells funny, unfortunately a GET/POST requests does not return anything at all.
Anyway we found our secret custom header X-NVRPC-SECRET
, and also it’s 120-bit value DF1FCCCC6288WG92ABEE8GG0062DB6
. let’s try it out :
>>> import requests
>>> r = requests.get("http://localhost:23401", data='<?xml version="1.0"?>\n<methodName>myMethodName</methodName>', headers={'X-NVRPC-SECRET':"DF1FCCCC6288WG92ABEE8GG0062DB6"})
>>> r
<Response [200]>
>>> print(r.text)
"""
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>9</int></value>
</member>
<member>
<name>faultString</name>
<value><string>Unknown Function name</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>
"""
The returned error changed again ! We understanbly did gave a correct function name since we don’t know which ones are valid. However, the strcmp
breakpoint has dumped another interesting strings :
23401=23401
23401=23401
/crossdomain.xml=/
/=/crossdomain.xml
/=/
/=/
X-NVRPC-SECRET=Host
X-NVRPC-SECRET=User-Agent
X-NVRPC-SECRET=Accept-Encoding
X-NVRPC-SECRET=Accept
X-NVRPC-SECRET=Connection
X-NVRPC-SECRET=X-NVRPC-SECRET
DF1FCCCC6288WG92ABEE8GG0062DB6=DF1FCCCC6288WG92ABEE8GG0062DB6
myMethodName=GetApplicationList
myMethodName=GetVersion
myMethodName=SearchPaths_Get
myMethodName=Streaming_GetOpsValues
myMethodName=VOPS_GetPath
myMethodName=VOPS_GetStatus
Now we have access to certain function names exposed by the server. GetVersion
seems pretty straightforward, let’s try to call it :
>>> import requests
>>> r = requests.get("http://localhost:23401", data='<?xml version="1.0"?>\n<methodName>GetVersion</methodName>', headers={'X-NVRPC-SECRET':"DF1FCCCC6288WG92ABEE8GG0062DB6"})
>>> r
<Response [200]>
>>> print(r.text)
"""
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>23</int></value>
</member>
<member>
<name>faultString</name>
<value><string>XML-RPC request invalid: wrong header
</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>
"""
Huh, that didn’t work. Before spending hours tracing calls and stepping instructions, let’s see how an xml-rpc call is spec’ed (http://xmlrpc.scripting.com/spec.html):
Well, we better wrap our methodName
xml node in a methodCall
one :
>>> import requests
>>> r = requests.get("http://localhost:23401", data='<?xml version="1.0"?>\n<methodCall><methodName>GetVersion</methodName><params></params></methodCall>', headers={'X-NVRPC-SECRET':"DF1FCCCC6288WG92ABEE8GG0062DB6"})
>>> print(r.text)
"""
<?xml version='1.0' encoding='utf-8' ?>
<methodResponse>
<params>
<param>
<value>
<struct>
<member>
<name>Major</name>
<value>
<i4>10</i4>
</value>
</member>
<member>
<name>Minor</name>
<value>
<i4>4</i4>
</value>
</member>
<member>
<name>BuildNumber</name>
<value>
<i4>0</i4>
</value>
</member>
<member>
<name>Description</name>
<value>
<string>NVIDIA Update Backend</string>
</value>
</member>
<member>
<name>ReleaseDate</name>
<value>
<dateTime.iso8601>2015-06-29T00:00:00Z</dateTime.iso8601>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodResponse>
"""
Success ! We managed to make a successful call to this interface endpoint. Now let’s look at endpoint security.
Security
This xml-rpc server is listening on a unknown port (I didn’t managed to locate the port init routine in IDA) and expose “dangerous” API like GetHardwareInfo
or Downloader_AddURL
, which might be accessible from a remote attacker.
Firstly, it only listen to localhost
so these API are not exposed to an attacker with an access to the same LAN. However it can be vunlerable to CSRF
if not configured correctly. Fortunately, there is two things that mitigate this attack vector :
CORS
Web browsers companies clearly identify this attack vector and worked to limit it. Their hypothesis revolve more around CSRF between two websites/remote servers, but it also work to block local-only attacks :
The xml-rpc server only listen to HTTP requests (not HTTPS) and since the new big push to Let’s Encrypt, there not many HTTP-only websites left (you can’t make HTTP requests from an HTTPS page due to mixed content policy). Fortunately, there are still some web security experts which keep on delivering source code over HTTP :p.
The browser almost comptely shut down the CSRF
attack vector, since we have to fallback on non-cors requests which are severly constrained.
X-NVRPC-SECRET
: this a custom HTTP request header implemented by the XML-RPC server which holds a “secret” key needed to access the xml-rpc APIs. By chance, the use of this header combined with CORS policy completly destroyed myCSRF
approach sinceno-cors
requests (the only one not blocked by the browser) can’t set up custom headers in their request. Additionnaly, this secret is a GUID generated at process’s runtime usingole32!CoCreateGuid
and written down in a local section.\Local\{AA45D379-DBB1-4AF4-833B-39E697EDCCA4}
which is not accessible from a remote attacker.
All in all, there is a string of complications which make the xml-rpc endpoint not exploitable from a remote scenario, but it’s pretty tenuous security in my opinion. Since this endpoint is clearly meant to be accessible only from a local machine, it would be better to use an appropriate link layer, like a NamedPipe
or a ncalpc
RPC server.
APIs
methodName |
---|
AddFeedback |
Application_ApplyOPS |
Application_GetLastSettingsAction |
Application_GetSettings |
Application_HasSettings |
Application_RevertOPS |
ApplicationScanningEnabled |
AutoApplyEnabled |
CheckUpdatesNow |
ClaimLicense |
Downloader_AddURL |
Downloader_GetList |
Downloader_Pause |
Downloader_Proceed |
Downloader_Remove |
EnableApplicationScanning |
EnableAutoApply |
EnableEventLogging |
EventLoggingEnabled |
EventLoggingEnabled |
FRLGetState |
FRLSetState |
GetApplicationList |
GetCheckFrequency |
GetEnableAutomaticDriverDownload |
GetEnableUpdates |
GetEnableUpdateType |
GetHardwareInformation |
GetLastCheckTime |
GetLastGeolocation |
GetLastOPSChangeTime |
GetLastOPSStatus |
GetPackageList |
GetSearchBetaVersions |
GetSHIM |
GetSUGAR |
GetSupportedApplications |
GetTranslation |
GetUpdatesList |
GetVersion |
Installer_EnableDRS |
Installer_EnableGFE |
Installer_EnableNotifius |
Installer_EnableUpdatus |
IsFRLSupported |
IsUpdateTypeSupported |
MarkOOTB |
NeedOOTB |
NewPollInfo |
ScrubApplications |
SearchPaths_Add |
SearchPaths_Get |
SearchPaths_Remove |
SetCheckFrequency |
SetEnableAutomaticDriverDownload |
SetEnableUpdates |
SetEnableUpdateType |
SetSearchBetaVersions |
Streaming_GetApplicationDataPath |
Streaming_GetOpsValues |
Streaming_SetOps |
Streaming_UnsetOps |
TCSaveInformation |
VOPS_GetPath |
VOPS_GetStatus |