We identified three independent remote code execution (RCE) vulnerabilities in the popular Counter-Strike: Global Offensive game. Each vulnerability can be triggered when the game client connects to our malicious python CS:GO server. This post details our journey through the CS:GO binary and conducts a technical deep dive into various identified bugs. We conclude by presenting a proof of concept (POC) exploit that leverages four different logic bugs into remote code execution in the game’s client, triggered when a client connects to the server.
The CS:GO patch dated 04/28/2021 fixed several critical vulnerabilities, including three critical bugs from us. This post describes our approach and how we discovered three critical vulnerabilities. We present a single bug chain, consisting of four logic bugs, and explain how these led to a remote code execution (RCE) on the client by cleverly combining them. Although the post does explain the four logic vulnerabilities, its focus is on the methodology of our research.
First we look at existing research for the CS:GO game and give a general introduction to make reverse engineering of the complex client less painful. The post then introduces basic concepts of the CS:GO network protocol like
Cvars and detail four different logic bugs. Combining the bugs leads to the proof of concept that exploits a CS:GO client by only connecting to a malicous, attacker controlled server.
Table of contents
- Know your target
- The discovery of four logic bugs
- Full logic bug chain
- Closing Thoughts
The free-to-play game Counter Strike: Global Offensive (CS:GO) continues to experience great popularity with 21 million players per month, not least because of the wide variety of game modes offered by the many community-hosted servers. The game from 2012 is based on the even older source engine (2004), known for games such as Portal, Half-Life 2 and Left 4 Dead. The source engine in turn uses components from its predecessors, GoldSrc (1998) and the Quake engine (1996). This history already indicates that the powerful and complex source engine possesses some components, for which security did not yet stand in the foreground while programming.
The many game modes, community servers and modding support take a toll: a large attack surface. The many file formats such as textures, 3D models and AI navigation points go through a wide variety of parsers with completely attacker-controlled data as the data is shipped directly from the CS:GO server. In addition, the source engine implements its own TCP-like network stack based on UDP with all the associated problems in such a complex implementation. The network implementation has already been exploited in other attacks.
Know your target
Security research is not about blindly poking around and looking for security gaps. Because: Only when you have fully understood a target, you are in a position to break through the technical restrictions. The first step should therefore be to obtain as much information about the target as possible. The following sections provide ideas for this “recon” phase:
Software Development Kits
Games with modding support often provide an official software development kit (SDK). While the SDK does not contain the target’s source code, the structures defined there provide valuable information on network packages and class definitions that help to understand the engine. For Valve games in particular, there have also been several source code leaks of the engine or complete games (2003, 2007, and 2020). Although the source code is often outdated and contains many, now fixed, security holes, these leaks are very helpful. Mostly because source code is simply more pleasant to read than compiler-optimized assembly.
CS:GO is well known, thus we were not the first researchers looking for bugs in this game. Therefore, we searched the Internet for helpful blogposts and presentations at conferences. The information described in this public research is often reduced to the essentials and makes it easier to find one’s way around a new, complex target.
Super annoying in the game, loved by security researchers: Cheater communities like UnknownCheats exist. These forums provide detailed reverse engineering posts and internals to the engine. In this case, Felipe had already written a Network Cheat that contributed a lot to the understanding of the network protocol.
Debug symbols contain the otherwise unrecognizable function names and class structures that make reverse engineering much more convenient. Sometimes versions of the game are also intentionally shipped with debug symbols to generate better error reports. However, sometimes programmers forget to remove the debug symbols from the final binaries of the game. Programmers are humans, and humans make mistakes.
The CS:GO version for macOS from April 2017 (shown below) contained full debug symbols. Game files with symbols are many times larger than without and can therefore be identified automatically using SteamDB and old repositories.
csgo/bin/osx64/server.dylib (+9.30 MiB)
bin/osx64/engine.dylib (+5.17 MiB)
bin/osx64/scaleformui.dylib (+3.23 MiB)
csgo/bin/osx64/client.dylib (+12.13 MiB)
bin/osx64/materialsystem.dylib (+2.18 MiB)
While in 2021 it was still possible to specifically download old versions using
SteamCMD, the feature seems to have been disabled by Valve in the meantime.
Despite all the information, you have to invest many hours in reverse engineering the target. Only once you have fully understood which buffer processes the network data in which virtual function with which arguments you can start doing exciting things. But the effort is worth it: we found instant client crashes using Hongfuzz, the public protobuf network structures, and libprotobuf-mutator. These crashes directly provided
instruction pointer control and were thus very likely exploitable! To test the full extent and develop exploit strategies, we decided to implement our own early-stage server in Python.
The discovery of four logic bugs
For a target like CS:GO, due to years of development and public bug bounty program, simple bugs are most likely fixed by now. If you are only looking for stack overflows in random methods of the huge
engine.dll, you will quickly give up in frustration. But it is true: every little anomaly can prove to be valuable in combination with other gaps. During the weeks of staring at the CS:GO disassembly and source-code leaks, we constantly asked ourselves the following questions:
- What primitives do we already have?
- What can we do by combining them?
- What security mechanisms are there?
- What weird edge cases might a developer not have considered?
Memory corruption exploitation is hard. Although two of the three full-chain exploits submitted by us to Valve were memory corruptions, that meant extremely high overhead and always the risk that the client would crash because of an unfavorable memory allocation. Starting CS:GO and connecting to a server loading the map took several minutes each time, which made development very tough.
In this post, rather then explaining weird heap feng shui mechanisms, focus on four logic bugs that together led to our goal of remote code execution on the client. The order of discovery was as follows.
Bug 1: Execution of privileged commands from the server
To verify that our custom python CS:GO server is actually working, we sent the command
echo Hello World! to the client via
CNETMsg_StringCmd and, as expected, received the output
Hello World! on the game console. Randomly, we also tried sending the
quit command. And the game closed! We couldn’t believe that a server is allowed to do that. As it turns out, it is usually not allowed to do so: With the help of SourceMod, a source engine modding framework that can also send messages to the client, we recreated the same setup with an official and modded server. The result:
FCVAR_SERVER_CAN_EXECUTE prevented server running command: quit. Did we find our entry bug? How exactly does the bug occur?
Source engine single-player games internally use a locally hosted source engine server. The single-player client then connects to its own server to join the game. This single-player server should of course have far-reaching rights, e.g., to change the keyboard layout on the client or to take screenshots.
A multi-player server is recognized as a local, and thus privileged, single player server if only a maximum of one client can connect to the server. The vulnerability is in the determination of the server type: The maximal number of clients that can connect to the server is controlled by the variable
m_nMaxClients and is received by the client when connecting to a server. By chance, our Python server had set the variable
m_nMaxClients to 1. And with this we could execute privileged commands on the client!
Bug 2: Arbitrary file download due to extension stripping
Source engine servers can send additional game files such as maps or player models to the client. The data transfer can be done either via the source network protocol or HTTP
fast_dl. To prevent malicious files from being sent to the client, certain file extensions like
*.ini are blocked.
fast_dl option is set, additional content is loaded from a specified HTTP server rather then from the CS:GO server directly. The URL is dynamically generated from the server name and the full file name by the
snprintf(p_cResult, 256, "%s/%s", p_cServerName, p_cFileName) function. The
snprintf function limits the length of the resulting string to 256 characters, thus truncating unnecessary characters from the file name. But both
p_cFileName can have a length of 256 characters each! A file name like
././[..]/file.AAA.BBB can be terminated specifically after the
.AAA extension, as the
.BBB part is truncated by the
snprintf function. The filter for potentially dangerous files can thus be bypassed completely!
The following source snipped illustrates that the extension is stripped:
unsigned char p_cResult;
// String fits into 32 byte and includes the `.bsp` part
snprintf(p_cResult, 32, "%s/%s", "AAAAAAAAAAAAAAAA", "evil.dll.bsp");
printf("%s\n", p_cResult); // Output: AAAAAAAAAAAAAAAA/evil.dll.bsp
// Long enough string to truncate the `.bsp` part
snprintf(p_cResult, 32, "%s/%s", "AAAAAAAAAAAAAAAAAAAAAA", "evil.dll.bsp");
printf("%s\n", p_cResult); // Output: AAAAAAAAAAAAAAAAAAAAAA/evil.dll
This vulnerability was found through code analysis of the
fast_dl protocol, which has not changed much in recent years.
Bug 3: Arbitrary text file write in game directory
At this point, we were not sure how to combine the two previous bugs. Therefore, we searched the CS:GO binary for helpful privileged commands. With the
con_logfile command, we surprisingly discovered that this command could write arbitrary
*.log files to arbitrary game folders. Due to a similar extension stripping bug by
snprintf it was also possible to specify an arbitrary file extension and thus write text files with arbitrary contents and an arbitrary extension.
Specifically, this bug could be used to create a new configuration file
cfg/leak.log with arbitrary CS:GO commands. The
leak.log “config” file could then the loaded by the
exec leak.log command, reading the file from the
Bug 4: Fallback to disabled signature checks
When starting the CS:GO client, the integrity of the game DLLs is verified via matching hash values. Only after this verification it is possible to play on official servers. If the DLL verification fails, a fallback to the
insecure mode occurs. This can also be achieved by the additional command line argument
-insecure. Only in this mode, additional DLLs not located in the
bin/ game path can be loaded. If the attacker succeeds in making the DLL verification fail, they can create their own DLLs, refer to these DLLs in the configuration and achieve command execution. On Windows, an attacker can specify code that is executed when the DLL is loaded into a process. Thus, the attacker can execute arbitrary code on the client system.
Windows prevents the overwriting of DLLs, which are loaded in a running process. Therefore, we had to find a DLL that is verified at game start but is not loaded into the process. Fortunately, we found that the
client.dll had been replaced by the
client_panorama.dll and is therefore no longer loaded, but is still verified! Overwriting
client.dll with arbitrary text (bug 3) thus caused the verification to fail.
Full logic bug chain
The full bug chain uses all four bugs to:
- execute privileged commands on the client
- download a malicious DLL to the game directory
- replace the
gameinfo.txtso that the malicious DLL is loaded on game startup
- corrupt the
client.dllto achieve a fallback to the
To understand the following steps, we still need to introduce two elements typical for source engines: the
All source engine based games are actually “add-ons” to the basic Half-Life game. Assets and DLLs for the game are loaded from a special path defined in the file
game "Counter-Strike: Global Offensive"
SteamAppId 730 // This will mount all the GCFs we need (240=CS:S, 220=HL2).
Game |gameinfo_path|/exploit // NOTE: Added by our exploit
|gameinfo_path|/exploit as first in the
FileSystem array, the engine tries to load missing DLLs from this path. Only if the element to be loaded is not found there, the original game path is used. One DLL that is loaded at game start is
matchmaking.dll. This means that we can place a new
matchmaking.dll and invoke arbitrary code when the CS:GO client loads the DLL.
CVars are a fundamental concept in SourceEngine games and appear everywhere. These variables control pretty much everything there is to set up in the game: paths, key-binds, the appearance of crosshairs, the game mode, etc. Also the legendary
sv_cheats variable, which many Counter Strike players probably have already heard of, is a
CVar. Depending on
CVar, the settings can also be set by the server and thus override local options.
Upon connecting, the client tells the server which local
CVars are set at the client, so that the server can react accordingly. For example, the server can kick the client if
sv_cheats is set to
1 at the client. As an attacker, we need to know the installation directory from the CS:GO client so that we can exploit
bug 2 and
bug 4 by taking a path that is just the right length. Unfortunately, by default, the client does not send along a
CVar that contains the current game directory. We therefore use a trick to set the new
CVAR GAMEBIN and have it sent back to the attacker-controlled server. The basic idea:
- Execute a “script”
leak.logto set the
- Instruct the client to reconnect to the malicious server
- Upon reconnection, all
CVarsand set back to the malicious server
The details involve invoking the
path command from a config file to set the
CVAR GAMEBIN to the installation path of the game. We leverage the attacker-written config file
leak.log, which includes the
path command. The client has to execute the config file, otherwise the
CVar is not stored persistently during the next server connect. The
leak.log file is executed with the
exec command. Afterwards the malicous server instructs the client to reconnect. Upon reconnection, the
CVar is leaked back to the server.
|Client connects to malicious, attacker controlled server
m_nMaxClients = 1
|The server can now execute privileged commands on the client
|The client has
fast_dl http downloads enabled to download missing assets
|The client executes the
path command and stores the result in
|The client reconnects and sends all
CVars to the server, leaking the
The server then creates the
downloadtables with a precisely long filename size such that
the extension is stripped
<fast_dl download code>
|The client downloads the malicious
gameinfo.txt from the HTTP server
bin/client.dll is overwritten with a logfile entry (not a valid DLL anymore)
|The client crashes. The user restarts the client.
|Invalid signature check for overwritten
insecure and load of overwritten
matchmaking.dll results in DLL found in
LoadLibraryA of malicious, attacker
controlled DLL and RCE
We provide a video of the above outlined chain of the four logic bugs (see below). If you stop the video at 00:29 seconds you can notice interesting output in the CS:GO console and in the exploit server:
- The leaked
GAMEBIN: f:\spiele\steam\steamapps\common\couter-strike global offensive\csgo\binis retrieved from the exploit server
- The CS:GO console shows the very long downloaded files, which succeed for the
././[..]/gameinfo.txt.stffiles. As described above, the
.stfextension is stripped during the download, resulting in the download of
Often people ask us how much time we spent on building this exploit chain. Unfortunately, we can not determine the total time spent. For weeks, we met on Discord in the evening to exchange ideas, programm together and analyze our findings until late in the morning. Alain at that time had roughly 250 hours of gameplay in CS:GO and had not played a single online match. We found the bugs “relatively” quickly, but for their bug bounty program, Valve requires a full-chain exploit demonstrating RCE impact. Without the elaborate demonstration, the research would have been completed after 30% of the time. Hence, we invested quite some time in our RCE demonstration.
Speaking of Valve: We became aware of Valve’s high payouts for CS:GO through various and simple looking HackerOne reports. The reports at the time only needed to demonstrate memory corruption to get the full payout. Our initial euphoria quickly sank after our three different reports were quickly declared valid, but still not fixed even after 13 months and multiple requests. After a lot of pressure and the threat of full disclosure, the bugs were finally fixed. The payout was 7.5k per bug, less than we expected. All in all a sobering experience.
For us the CS:GO bug bounty journey was the first time we invested weeks of time into a project together. The takeaways for us personally were mainly:
- Don’t look for cricitial bugs and quick wins only.
- Chain your bugs to unveal their full potential.
- Keep your eyes open for edge cases and things devs didn’t think about.
- Try harder! If run against a wall search for the hole and don’t give up early.
|We send the initial Report with PoC video and exploit setup
|H1 has troubles to reproduce the issue
|We provide an exploit Docker setup for easier reproducability
|H1 still has troubles to reproduce the issue
|We provide a full server setup with OpenVPN for even easier reproducability
|H1 successfully reproduces the issue(s) and marks the report as triaged
|We ask for an update
|H1 states they are still looking into the report
|We ask for an update, as a total of half a year has passed by
|We ask again for an update
|H1 states that Valve is still looking into the reports
|We say “Happy Anniversary” and ask for an update
|We contact other researchers who submitted bugs to Valve and think about complaining in our reports as collective
|We write a statement about our dissatisfaction with the process and “reserve the right to disclose the findings in the upcoming weeks”
|H1 states that they flagged the report to “internal managers” and try to speed up the process
|We notice that the issues have been fixed and ask for coordinated disclosure with Valve
|H1 says “Thanks for the report” and we receive our bounty
|We request report disclosure, no response so far