Buffer Overflow Leading to Code Execution in Left4Dead 2
Left4Dead 2 is a video game released in 2009 by Valve Software for PC, Linux, Mac, and Xbox 360. Even though it is 11 years old, it is played by tens of thousands of players every day and is still being actively maintained by Valve.
It runs on a branch of the Source Engine, which is Valve’s in-house game engine that is built upon an even older engine of theirs, GoldSrc, which in-turn is built upon id Software’s Quake engine. Over the years, the Source Engine has seen its fair share of vulnerabilities, and Valve has always been diligent in fixing the issues.
In this blog post, I will be detailing my own experience in finding and reporting a critical buffer overflow vulnerability in the Left4Dead 2 branch of the Source Engine. Valve has a bug bounty program through HackerOne which the general public can report security vulnerabilities in Left4Dead 2 and other Valve games (read the HackerOne report here). This makes it easy to report issues and keep track of their status. In part two of this blog post series, I will be discussing how to get started with fuzzing video games and go more into depth on the topic.
Vulnerability Hunting Without Source Code
For my vulnerability research into Left4Dead 2, I decided to go with the easiest route I could think of – fuzzing. For a black-box binary as complex as a game engine, I knew that fuzzing would be the most straightforward method of finding a vulnerability. Since the source code is not available, there would be a large amount of reverse-engineering required to manually find vulnerabilities, and fuzzing would lead to results much faster.
Exploring Basic Fuzzing Framework and Trade-offs
My fuzzing tool of choice in this case was CERT’s Basic Fuzzing Framework (BFF) which has a quick setup and is relatively simple to use and understand. BFF also provides a few features that are beneficial to the fuzzing process, such as a test case minimizer. Most importantly, though, it requires no reverse engineering or instrumentation to use with a black-box binary unlike other fuzzers such as American Fuzzy Lop (AFL). These benefits are what made me choose BFF for use with Left4Dead 2.
The main downside to BFF in this case was it’s speed. It was quite slow, with only one fuzzing iteration every 5 or so seconds due to having to initialize the entire Source Engine every time. Compared to something like AFL, which when properly set up can do hundreds of even thousands of fuzzing iterations a second, there was a significant speed disadvantage.
Narrowing the Attack Surface
Source Engine has an enormous attack surface. Due to being a game engine, it processes a large amount of binary formats to load things such as 3D models, sounds, and game levels. There is also a lot of networking built-in due to the engine’s support for online multiplayer. Any of these attack surfaces could have a vulnerability, so I decided to go with something more obscure that I figured wasn’t likely to have been tested the past.
Fuzzing Focus: Navigation Meshes
For my focus, I chose navigation meshes. Navigation meshes are used by the Source Engine to let the in-game AI characters know how to navigate each game level. These are present in every game level, because without them, the AI would not know how to navigate each level. Without the AI, there is no game.
I didn’t know much about the format of a navigation mesh, but I also didn’t need to know this to fuzz this format. BFF in this case would mutate the input navigation mesh randomly to see if it would cause a crash or other unintended behavior.
Fuzzing to Find Crashes
To start the fuzzing process, I configured the game to make it easier to fuzz. Source Engine provides a lot of configuration options for improving the performance of the game, as well as debugging. I disabled all 3D rendering and sound processing, so that the game would load and start parsing the navigation mesh much faster. This shaved a second or so off each fuzzing iteration, which isn’t a lot of time on it’s own, but saving a few hours or days of fuzzing will add up quickly.
After the game was configured, I let the fuzzer run for three days. The fuzzer would mutate a navigation mesh, start the game and make it load straight into the level with the mutated navigation mesh, and then observe any crashes and log information about them. If there was a crash, it would then minimize the test case so it was easier to triage the crash.
After Fuzzing Review for Potential Exploits
After the three day period, I checked the results of the fuzzer and immediately noticed a few notable crashes and began work on identifying whether or not any of the crashes were potential vulnerabilities. I initially anticipated that this would take a long time, but I found a potentially exploitable crash very quickly.
Fuzzing Left4Dead 2 with BFF.
Fuzzing Left4Dead 2 with BFF.
Triaging Crashes for Exploitability
One of the crashes the fuzzer had found was crashing with a very promising Data Execution Prevention (DEP) exception. DEP is a security feature that marks user-writable regions of memory as non-executable and does not allow the processor to execute code from those memory regions. If a program is encountering a DEP exception, this means that the processor is trying to execute data. This let me know that somewhere, the stack was being overwritten, which generally indicates some sort of buffer overflow.
BFF Features To Help Identify Exploitability
One of BFF’s features is that it will assign a rank of exploitability to each crash. While this rank is prone to false positives, it is a good starting point for indicating whether or not a crash is exploitable. With this particular crash, it was showing an exploitability rank of 5, which indicates BFF determined it was very likely to be exploitable.
An example of an exploitability rank for a crash.
Another feature is that it will show you whether or not the crashing address is composed of bytes found in the particular crash-causing test case. In the case of the crashes with a DEP exception, this was true. Since the EIP register is used by the processor to know which instruction to execute next, this was leading to a crash due to an invalid memory address. After seeing this, I knew that I had an exploitable buffer overflow on my hands.
Creating a Proof-of-Concept
After finding the crash, the next step was to create a proof-of-concept to provide to Valve. The simplest proof-of-concept was to simply set the EIP register to 0x41414141 (AAAA in hexadecimal) thus demonstrating that gaining control of the flow of program execution was possible.
Since I had a minimized version of the crashing navigation mesh, it was a matter of comparing it to the unmutated navigation mesh and seeing exactly what part of the navigation mesh was causing the overflow. After that, I changed the responsible bytes in the file to 0x41414141, and loaded up the game to try it out. It worked correctly on the first try.
Gaining control of the EIP register.
Reporting Through HackerOne
Reporting the vulnerability to Valve through their HackerOne bug bounty program was a good experience. I made the report as thorough as I could and included the proof-of-concept navigation mesh to demonstrate the vulnerability. They accepted the bug, let me know when they fixed it, and then awarded a bounty.
I would definitely like to thank Valve for such an excellent response in getting this one fixed up. I will definitely report vulnerabilities in Valve’s products in the future if I come across any more.
Bug Bounty Timeline
April 18, 2018 – Bug found and reported to Valve’s HackerOne program
July 24, 2018 – Valve lets me know that the bug was fixed in the previous Left4Dead 2 update
September 4, 2018 – Valve marks the bug as fixed and issues a bounty for including a valid proof-of-concept and for the quality of the report