Riot's Approach to Anti-Cheat
Combating cheats is an ever-evolving arms race. The scope and complexity of cheat development grows every year along with the stakes in online gaming. The pressure is on for game studios to level up when it comes to detecting and preventing bad actors. I’m Michael “Perma” VanKuipers, and I used to be one of those bad actors; I spent over a decade developing cheats for various games and earned the ire of at least one large game studio in the process. These days I work on Riot’s Anti-Cheat team, helping secure League of Legends from scripts, bots, and exploits. In this article, I’m going to show you some of the details and strategies behind our latest anti-cheat initiative, including a technical overview of the steps we took to mitigate certain types of cheating. For a more high-level understanding of how we handle security, check out our article on the Evolution of Security at Riot.
We’ve worked with many popular third-party software vendors and creators to ensure their tools will continue to be compatible with the game. If you’re still getting critical errors or have concerns about your legitimate third-party products, please check out our known issues and fixes or contact player support for more individualized assistance.
Before we get started, I should preface the article with this: talking about anti-cheat is tricky. We have to juggle open and honest conversation with protecting the secret sauce. There are some things that we can’t share for the sake of players and the integrity of the game. That said, we don't think efficacy depends solely on secrecy, and we've been hard at work developing technical solutions for this problem space. With this article I hope to give you a general understanding of the types of threats we see in the cheating space and how we are changing League of Legends to mitigate them - with a few gritty details along the way.
Our philosophy on cheating
When it comes to keeping our products fair, my team focuses primarily on three important tenets: prevention, detection, and deterrence.
For many years, detection and deterrence were the strategies seen across most game studios. In general, most techniques boil down to letting cheaters play a few games, detecting their anomalous behavior (either by tracking what changes they made to the game or analyzing their gameplay patterns), and then removing them from the player population. Today, this approach is still the source of most common anti-cheat solutions that we see. It makes sense, but it means that the barrier to entry for creating cheats in the first place is still relatively low. In addition, players have exposure to cheaters in their games while we wait for our systems to detect them. It works, but it’s not ideal.
A more effective solution prevents cheating in the first place. If we succeed at that, our players wouldn’t have to experience a bot or scripter ruining their game. Detection and response systems would still be important, but we’d be able to use them as little as possible. This is a lofty goal, and it’s been the driving force behind the changes that we’ve made to our anti-cheat technology and strategy.
Types of cheats
There are certain behaviors that we discourage in League of Legends because we never want our players to find themselves questioning the integrity of the game. We classify these behaviors as cheating because they ruin the competitive experience that makes us love League. Our solution will focus mainly on technical cheating - the kind of cheating that requires the use of third-party applications that interact with the game. This category of bad behavior can mostly be divided into two subcategories - botting and scripting - which can be combined and implemented in creative ways. We'll explore a few use cases later in this article.
The first of the cheating subcategories is scripting, where third-party programs forcefully attach themselves to the game client and use the client’s memory and functionality to accomplish otherwise difficult or impossible tasks. For example, some common techniques we see include helping players dodge skillshots, zoom out farther than they normally could, or perform perfectly executed combos to smash their opponents’ faces. These scripts can range from being very simple (like zoom hacking), to having enormous complexity that caters to the specific nuances of each champion (such as giving Kalista extra maneuvering skills). Despite the level of sophistication in some of the scripts that we see for League of Legends, most scripters only see a nominal increase in their win rate.
In the image below, you can see a typical scripting user interface, which draws the ranges of various abilities, indicates which minions are ready to be last-hit, and shows the path of skillshot projectiles.
The second subcategory that we commonly see is botting. For the most part, botting is just a third-party application simulating game input to run down a lane and die, or give some semblance of random gameplay that tries to disguise the fact that they’re a bot. Bots are utilized mainly to level accounts for sale, or to farm in-game currency.
Our path to mitigation
With the goal of making cheating difficult in the first place, we started with the game client. The most vulnerable piece of any game is the client, as it resides on a cheater’s machine where they have full control of the software that’s running and the permissions granted to the game. With that in mind, we wanted to make the game client as difficult to analyze and exploit as we could.
Some of this comes down to good game design. We’ve been fortunate with the current state of League of Legends because a lot of good design decisions have already been made:
- We don’t share the state of other players if it doesn’t need to be shared, so we can avoid common cheats like “map hacks” (revealing all players on the map).
- We let the server’s game simulation make the authoritative game decisions and generally don’t trust the information received from the client, which helps prevent common cheats like “god mode” and “disconnect hacks,” barring any overlooked exploits.
- Our network protocol has been obfuscated, and we change this obfuscation regularly so that making a network-level bot is much more difficult.
Now that you have an idea of the types of challenges we face, let’s take a look at some of the ways we combat common cheats. I’ve picked out three specific examples of techniques we’ve implemented in response to the ways cheat authors traditionally attack our game client. In each section, I’ll discuss how cheaters approach compromising the game, and how our newest anti-cheat efforts hamper their progress.
Encrypting game code
One aspect of the game client traditionally left unprotected is the game code itself. Cheaters have historically been able to analyze the game executable using their favorite debuggers or disassemblers, and subsequently find the functions they intend to use. From there they can “hook” these important functions to get information about the current state of the game.
Hooking is a very common technique in cheat development that involves redirecting in-game logic into custom code. This allows the cheat application to perform special actions during those routines; for example, if a cheater hooks the function where particles and spell effects are drawn, they can use the location and direction of those spell effects to determine if they need to move the player out of the way of a skillshot.
Usually this type of hooking is done by the cheat application injecting some code into the game client. A popular method of loading that custom code in Windows involves injecting a DLL file into the game. The malicious DLL can then write a jump or call instruction in the target game function, changing the flow of the program into the custom code within the DLL. Once the custom code is finished executing, the cheat application passes execution back to the game code. The process is illustrated below.
In addition to calling their own code in response to game events, a hooked DLL allows cheaters to call game code to perform actions like issuing attacks and moving the champion. This enables the relatively easy development of scripts and bots once a cheater understands the code layout of the game. Even a relatively new cheat developer can pick up a memory searching tool and begin learning where things are in our game and how to manipulate them. We detect these memory changes and interactions with the game client, but we wanted to do a better job of preventing this kind of behavior in the first place.
With this goal in mind, we set out to make tampering with the League of Legends game client more difficult by encrypting the game code. There are commercially available applications - commonly known as “packers” - that do this already, but we wanted to come up with a solution in-house that would give us more granular control over performance and quality. This solution also gives us the chance to integrate security measures more tightly into the game, which makes protection more responsive to the game state while also making it more entropic to cheaters.
In order to implement the encryption of the game code, we needed to single out the portions of the game’s executable file that contained actual code. Understanding how we accomplished this requires a general idea of how the Portable Executable (PE) file format in Windows works. The PE file format is what the operating system uses to load and execute files with runnable code. This includes common file extensions such as EXE and DLL files, system drivers, and more. Each PE file is divided up into a number of sections. Each section of the file has an accompanying “section header” that provides pertinent details about the section including its name, size, and location in the file. This structure is illustrated in more detail below.
Because our solution focuses on encrypting the code portions of the game client, we singled out the .text section within the file, which is where the executable code of a program is emitted by a compiler by default. Traditional packers perform their encryption by creating a bootstrapping utility that takes the original game binary, finding the relevant sections that require encryption by examining the PE section headers, and then applying the encryption algorithm to those sections. We also used this same approach, focusing on the .text section.
Once the .text section in the binary was encrypted, we needed to decrypt it when it executed. If someone were to run the game client as it was, it would crash, because the entry point of the application would be a bunch of encrypted garbage. Many packing tools deal with this issue by injecting their own unpacking code into the executable, and overwriting the PE file’s “address of entry point” member to point to their unpacking code. This causes the unpacking code to gain execution first when the program is run. Once it has decrypted the .text section, it can pass execution to the original entry point and the program runs normally.
Unfortunately, this particular technique had some limitations that we wanted to overcome. As we worked on this solution, we realized that it would be advantageous if we could validate some of the game’s dependencies before they were loaded. This would allow us to check for any unusual changes to libraries used by the game.
We wouldn’t be able to achieve that level of validation using the aforementioned “stub code injection” technique. The operating system would load our dependencies before calling the program’s entry point and allowing our unpacking code to run. This would mean that any altered libraries would already have been loaded and we wouldn’t be able to reliably verify their integrity. We knew we needed a better approach.
We discovered that we could use the way that the operating system loaded dependencies for PE files to our advantage. When an application is compiled, import descriptors are generated and inserted into the executable for any external libraries that are referenced by the program. From there, each specific imported function or data variable is listed as part of the import descriptor. This way, the operating system can know which DLLs need to be loaded in order for your program to run correctly. These imports are stored as strings or ordinals during compile time in an array called the Import Name Table (INT), which is referenced by that library’s import descriptor. When the program is loaded, the operating system resolves the locations of the referenced imports in the INT and populates the Import Address Table (IAT) with the actual addresses of those imports. You can see this process below.
What if we generated an external library that did our unpacking rather than embedding it in the game itself? Using this approach, we ensure that our unpacking library is the only one loaded by modifying the game’s import descriptors to list only our library. As a result, we’re loaded first and have the opportunity to validate the other dependencies. It doesn’t come without drawbacks, though - it also means we have to manage and load the other dependencies ourselves. The game has many imports and since we replaced all of them with just our bootstrapping module, we had to make sure that we kept track of what previously needed to be imported. We then had to load those external libraries ourselves during initialization and populate the game’s IATs for each of those libraries since the operating system could no longer do it for us. In this instance, we were willing to trade away the convenience of having the operating system load our imports in exchange for having the ability to validate them before they were loaded. We think it was worth it.
At this point, we’d accomplished our goal of making static analysis difficult. If someone opened the game in a disassembler, they’d be greeted with a bunch of garbage code. On top of that, we made sure that only portions of the game code are decrypted during regular gameplay rather than all of it at once - this helps protect us from keeping the entire .text section decrypted in memory after it’s loaded.
The next problem we had to tackle was dynamic analysis - what happens after the game has been loaded and part of it decrypted? At this point of our process, game code could be freely analyzed by a debugger. That would make our effort so far significantly less effective.
Making life difficult for debuggers
As an alternative to static analysis of the game, a cheater will often attach a debugger or binary instrumentation tool to the game client while it’s running. Doing this allows them to step through the code as it executes and examine how it works. There’s a lot of research online on anti-debugging techniques that can make things very difficult for someone trying to debug a running executable. We felt that it was prudent to include these, with as much variation as was reasonably possible.
The game itself and the bootstrapper were both areas that we wanted to protect, and ideally we could do so in a way that makes automated analysis difficult. During the bootstrapping process we insert random debugger checks throughout the code and we randomize which checks appear every build. At compile time, a randomly selected type of anti-debug check is inserted into each of the locations where a check was requested in the code. In some cases, the check is not inserted, so that some places in the code will appear to have no checks in one build, but will have a check in another build. This changes from build to build, as does the code itself for many of the checks.
To increase the complexity of analysis, different checks produce different results. For example, during one check we might log that a debugger was detected, and for another check we may change the behavior of some code down the line, or cause the game to crash five minutes later. This unpredictability helps keep the detection more difficult to spot and also helps us get telemetry on how and when people try to analyze the game client.
Protecting our data
Once the game’s code was protected and we felt comfortable with the level of anti-debugging measures, there was one last common attack vector where we thought we could raise the bar in terms of difficulty. While all of our code was now protected, the globals and class members - whose pointers and values resided in the .data section of the PE file and the heap, respectively - were not.
A lot of script and bot developers use memory searching tools to find data in the game - things like health, mana, etc. - and then use those memory locations to find out other details. For example, health and mana may be part of a larger class or structure that represents the player, and that structure might contain other things such as player position, spell book, level, the direction they’re facing, etc. These types of information are particularly useful for scripts that try to automate behavior like skillshot dodging.
Memory searching tools work by walking through the heap of the game and looking for user-specified values - say, a health value of 100. The user will then take damage in the game, reducing their health to 90, and then search the memory locations that were previously found by the tool to see which ones changed to 90.
They continue this over and over again until they reduce their search results to only a few addresses, and then figure out which one actually corresponds to health with some trial and error testing.
We block this very common technique by making sure that when the value is changed by taking damage, the value is actually moved as well. This means that we won’t write the 90 to the same memory location that the original 100 resided in.
Already this could cause problems for most memory searching tools. Ideally, the values of 100 and 90 would never be stored in the first place. This would force cheat developers to search only for changed values rather than specifying an exact value, which makes the process significantly more arduous. To achieve this level of obfuscation, we added encryption to the data as well so that the original values can’t be found in the heap. To introduce more entropy, we also made sure that each value uses slightly different encryption.
We’re now able to sprinkle these protected values liberally throughout the game code in the areas where we feel things are most important. We determined priority by analyzing existing cheats and seeing which data they use.
Putting it all together
As we move forward into the next year, we remain committed to focusing on our efforts to protect League of Legends, and we know that there will be even more hurdles ahead. We expect that the cheating space will continue to be an escalating arms race, but we see the encryption, anti-debugging, and obfuscation measures discussed in this article as an impactful step to improving the experiences of our players around the world.
Frankly, this is only a fraction of the considerable effort and functionality that we’ve crafted into our latest anti-cheat solution. We mentioned that there was a great deal we couldn’t talk about, and we did leave a lot out. After all, we need to keep something secret to fight the robot AI in the future. With that said, I hope that by talking more openly about our security solutions we can lay the groundwork for more open technical discussion in the future.
As always, if you have questions or want to prod us a little bit deeper on what we’ve discussed here today, please feel free to hit us up in the comments.