Reversing the League of Legends Client

DISCLAIMER: This is for information and learning purposes only, I do not endorse or recommend using this information to make any unofficial tools which can result in bans (or worse).

Back when League of Legends' client was still written in Adobe AIR, I reverse engineered it, located the functions responsible for encrypting and decrypting the RTMPS messages within Adobe AIR.dll, and wrote a hook to capture and dump the messages.

I never ended up doing anything with this code other than walking through it in my book, but the process was quite fun. League of Legends has since released a new client, and I decided pull it apart again.

Initial Probe

In order to understand what I was dealing with, I opted to first take a look at the process tree with Process Explorer:

Looking at this tree, the separate renderer processes reminded me of Google Chrome. Checking the command line confirmed that this was not a coincidence:

LeagueClientUxRender.exe --type=gpu-process --channel="6440.0.1778546861\341657564" --no-sandbox --lang=en-US --log-file="C:\Riot Games\League of Legends\RADS\projects\league_client\releases\0.0.0.93\deploy\debug.log" --supports-dual-gpus=false --gpu-driver-bug-workarounds=3,11,25,54 --gpu-vendor-id=0x1002 --gpu-device-id=0x67b0 --gpu-driver-vendor="Advanced Micro Devices, Inc." --gpu-driver-version=22.19.662.4 --lang=en-US --log-file="C:\Riot Games\League of Legends\RADS\projects\league_client\releases\0.0.0.93\deploy\debug.log" /prefetch:2 --app-name=LeagueClient --ux-name=LeagueClientUx --ux-helper-name=LeagueClientUxHelper --log-dir="LeagueClient Logs" --bugsplat-name=league_client_riotgames_com --project=LeagueClient --app-port=59757 --bugsplat-platform-id=NA1 --app-log-file-path="C:/Riot Games/League of Legends/Logs/LeagueClient Logs/2017-09-14T16-42-39_10160_LeagueClient.log" --primary-ux-log-file-path="C:/Riot Games/League of Legends/Logs/LeagueClient Logs/2017-09-14T16-51-32_6440_LeagueClientUx.log"

Many of the switches here, such as --type=gpu-process and --no-sandbox, are indicative of Google Chrome (you'll see them on Chrome if you check, hehe). With this information, I realized they must be using the Chrome Embedded Framework (CEF).

This tells me that the client is really just a host for a website.

Intercepting CEF

I decided that my next step should be investigating the boundary between the client's code and the CEF library. There are quite a few ways to go about this, and the best method really depends on the scope of the investigation. In order to figure out what I was dealing with, I fired up Dependency Walker and took a look at the exports from libcef.dll. The list was quite long, but it looked something like this:

Because of the laundry list of functions, I decided that writing my own hook was, for now, out of the question. Instead, I opted to use API Monitor.

Custom API Monitor Definitions

API Monitor has a feature called External DLL:

Using this feature, I was able to intercept all function calls within libcef.dll. This allowed me to understand which functions were being called with what frequency, which is awesome. One drawback, though, is that the feature is not capable of decoding parameter and return values, which meant I couldn't really do much.

I'm a big fan of API Monitor, however, and my frequent use of the tool has given me some insight into how its designed. Inside of the tool's root folder, there is a folder called API. This folder contains multiple nested folders with many .xml files inside. These files contain the definitions for the types, enumerations, and functions for the tool to intercept.

I set out to write my own set of definitions for every function which External DLL was able to intercept. Adding my own definitions was very easy, since the markup is quite intuitive and new .xml files are automatically picked up when the tool restarts.

After reading some existing definitions, I got the hang of it. The definitions I wrote are too bulky for this blog, so I've put them on github.

Monitoring API Calls

With my custom definitions in hand, I fired up League, attached API Monitor, and spent a few minutes trying to click every button I could find. Afterwards, I started sorting through the intercepted API calls. The amount of verbosity enabled by my custom definitions made this extremely easy, and I quickly focused in on a function called cef_parse_url:

There were some other interesting functions called, but they didn't give me even a small fraction as much information as that single function.

Ripping the API

With literally hundreds of calls parsing URLs, I started sorting through the logs. Most URLs were in the format https://riot:<session password>@127.0.0.1:<random port>/<path>, where <session password> and <random port> are unique across client restarts and <path> specified an API to call. Nearly every URL could be cleanly hit from a browser. Luckily, the listener is bound only to localhost, so computers on the same network are unable to hit it (that's some good extra security, on top of the need for a session token).

This painted a very clear picture: the client is hosting a local webserver with both a backend API and a set of frontend webpages, and this is all rendered by CEF.

Backend API Calls

I managed to intercept dozens of API calls to the web backend. Each one responds with a blob of JSON, and their paths are pretty descriptive. There are calls for active sessions details:

/lol-login/v1/session

Player profile:

/lol-honor-v2/v1/profile

Match history:

/lol-match-history/v1/delta
/lol-match-history/v1/matchlist
/lol-match-history/v1/games/{gameid}
/lol-match-history/v1/game-timelines/{gameid}
/lol-match-history/v1/recently-played-summoners

Summoner metadata:

/lol-summoner/v1/current-summoner
/lol-summoner/v1/summoners?name={summoner name}

Notifications:

/player-notifications/v1/notifications

Summoner inventory:

/lol-collections/v1/inventories/chest-eligibility
/lol-collections/v1/inventories/{summonerid}/backdrop
/lol-collections/v1/inventories/{summonerid}/champion-mastery
/lol-collections/v1/inventories/{summonerid}/champion-mastery/top?limit=3

Generating replays:

/lol-replays/v1/metadata/{gameid}/create/gameVersion/{Version}/gameType/MATCHED_GAME/queueId/400

Friends and conversations:

/lol-chat/v1/friends
/lol-chat/v1/conversations
/lol-chat/v1/conversations/{convid}/participants
/lol-chat/v1/conversations/{convid}

Loot:

/lol-loot/v1/ready
/lol-loot/v1/player-display-categories

Assets (there's also a query for every single .png, .wbem, font, and sound):

/lol-game-data/assets/v1/champion-summary.json
/lol-game-data/assets/v1/champions/{champid}.json
/lol-game-data/assets/v1/profile-icons.json

And configuration:

/lol-platform-config/v1/namespaces/LootConfig
/lol-platform-config/v1/namespaces/ClientSystemStates/storeCustomerEnabled
/lol-platform-config/v1/namespaces/ShareMatchHistory/MatchDetailsUrlTemplate

Interestingly enough, it seems like Riot is also collecting telemetry to understand how their players spend time in the client:

/telemetry/v1/events/time_spent_on_home
/telemetry/v1/events/time_spent_on_home_section

Injecting UI

One end-game for information like this might be into inject custom UI elements or extensions into the client. The fact that it is web-based means that modification and customization should be extremely familiar to anybody who's worked on a website.

I've not gone this far, but I have some pretty solid theories of good entry points for code injection. First, there's a file called index.html exposed by the backend. It is a visually blank page, but it has some interesting headers:

<html><head>
    <meta name="riot:plugins:url-pattern" content="/fe/$shortname">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <link rel="riot:plugins:dependency-graph" href="/graph.json">
    <link rel="riot:plugins:websocket" href="/">
    <link rel="import" href="/fe/plugin-runner/index.html">
    <link rel="import" href="/fe/lol-startup/index.html">
  </head>
  <body data-env="public">
  </body>
</html>

I haven't taken the time to intercept /fe/plugin-runner/index.html yet (the API requires some special request headers and I haven't wired up a proxy as of now), but I'd wager that it hosts some Javascript with access to the entire DOM. Moreover, I'd wager that any script running in the context of index.html may have access to the entire DOM, meaning Javascript injection into either of these files should give one unfettered access to the UI.

If that's doesn't work, I have another theory. When returning to the home screen, the client decodes this URL:

https://frontpage.na.leagueoflegends.com/en_US/lol/home/overview#protocol=https:&port=<port>

If you hit this page in a browser, you see the home screen for the client. The port in this URL matches the port being used by the local API, which leads me to believe that it is allowed to talk to the backend. Moreover, there are buttons on this page which can navigate to other pages within the client, leading me to believe it has at least some access to the DOM. Thus, injecting code into this page might also work.

That's All, Folks!

Thanks for reading! Feel free to give me feedback or share your own analysis in the comments!