SquirrelLang is an interpreted, open-source programming language that is used by video games and cloud services for customization and plugin development. For example, the extremely popular game Counter-Strike: Global Offensive (CS:GO) attracts millions of players on a monthly basis and utilizes the Squirrel Engine to enable anyone to create custom game modes and maps.
However, this freedom comes with a price: Anyone who downloads and hosts such an item from the community executes Squirrel code without any warning. Some of the most popular community-created items have been downloaded millions of times in the popular Steam shop. In order to prevent malicious actors from exploiting this, the Squirrel Engine is carefully sandboxed within the CS:GO process.
In this blog post, we break down a vulnerability we discovered in the core of Squirrel which was developed in C. It enables an attacker to bypass the sandbox restrictions and execute arbitrary code within a SquirrelVM, giving the attacker full access to the underlying machine.
An attacker can exploit an Out-Of-Bounds Read vulnerability (CVE-2021-41556) to escape a Squirrel VM and gain access to the underlying machine. This attack vector becomes relevant when a Squirrel Engine is used to execute untrusted code. This is the case with cloud services such as, for example Twilio Electric Imp or video games such as Counter-Strike: Global Offensive and Portal 2 which attract millions of players monthly.
For example, in a real-world scenario, an attacker could embed a malicious Squirrel script into a community map and distribute it via the trusted Steam Workshop. When a server owner downloads and installs this malicious map onto his server, the Squirrel script is executed, escapes its VM, and takes control of the server machine. From here, as our recent research has shown, it would be possible to exploit other vulnerabilities within the game’s network protocol stack that target the CS:GO players connecting to the hijacked server.
We verified that both stable release branches, 2.x and 3.x, of Squirrel, are affected by the vulnerability discussed in this blog post. A patch has been released as a commit to the official Squirrel repository, but at the time of writing, this commit has not been included in a new stable release. The latest official release is from 2016 and does not include patches for numerous other vulnerabilities that have been reported over the years. We did not develop exploits for specific projects that use Squirrel, but we recommend all project owners who depend on Squirrel to rebuild the latest Squirrel version from source code.
In the following sections, we provide some background information necessary to understand this vulnerability. We then go into detail about the bug that led to this security issue and finally provide a high-level exploitation strategy.
Squirrel is an object-oriented programming language similar to PHP. It allows developers to define classes and methods. To get a feel for Squirrel, let’s assume the following example class definition:
The syntax shown in the code snippet above is not very unique. It demonstrates a class definition with some default fields and a method named
In order to understand what is happening internally, let’s look at internal structures and how they would appear in memory at runtime. On a simplified and abstract level, the underlying C code structures of this class definition could look like the following:
The above image shows how a Squirrel class definition (
SQClass) contains a pointer to a dynamic array of methods, in which
someMethod() would be stored, as well as a pointer to a dynamic array of default values. Both
second_field, along with their default values, would be stored here.
In order to access these default values and methods, an SQClass definition also contains a pointer to a HashMap. HashMap
_members maps the name of attributes to their index within either the
_methods array. This relationship is shown in the following graphic:
In order to determine if the retrieved index should be used to access the
_defaultvalues array, a bitflag within the index is used.
The following code snippet shows a call to
_members->NewSlot(), which is called when a class member is defined. We assume that a new default value, for example,
first_field, is added to the
Following the example of defining
key variable would contain the string “
first_field”. The corresponding
_defaultvalues array index stored in the
field_idx variable is then stored in the HashMap. Note, however, that before the index is stored it is modified with the
_make_field_idx() macro. This macro and its counterpart for methods are defined as follows:
As can be seen in line 24, the
_make_field_idx() sets the bitflag
0x02000000 on the index.
The fact that bitflags are set within indexes is problematic as it is entirely possible for an attacker to create a class definition with
0x02000000 methods. As such we can create a very simple PoC:
rawget functions allow us to handily access members of a given class. In this PoC, the squirrel interpreter will dereference a null pointer and segfault because the
_defaultvalues array has not been allocated yet.
The following code snippet shows the vulnerable code, which we will break down in the following paragraphs:
The above code is called when a class attribute is accessed. The call to
_members->Get(key, idx) in line 42 takes in a key, which contains the name of the member that will be accessed. After the call,
idx contains the index to either the
_methods array. Which array should be accessed is determined by checking the bitflags of the index.
_isfield() macro returns
true if the bitflag
0x02000000 is set in the index. The bug lies in the fact that an attacker that is able to insert at least
0x02000000 methods into a class definition can force this check to return true since the bitflag would be set.
To make this more concrete, let’s walk through an example of how an attacker can trigger this vulnerability:
- The attacker creates a class definition with 0x02000005 methods and 0x1 fields
- The attacker accesses the method with the corresponding index 0x02000005
- The _isfield() macro returns true for this index as the bitflag 0x02000000 is set
- The _defaultvalues array is accessed with index 0x5. However, it only contains 0x1 entries and thus the attacker has accessed out of bounds.
In order to understand why this out-of-bounds access is dangerous, let’s have a look at what an attacker can do next.
_defaultvalues array which is subject to the OOB-access contains
SQObjectPtr structures. Thus, the memory that is read outside of the buffer of the array is interpreted as such. On a high level, this structure contains a pointer to a
SQObjectValue, as well as a field that is used to determine what kind of object is referenced by the pointer.
The following graphic demonstrates the relationship between an
SQObjectPtr and a
Through careful preparation of the heap, it is possible to craft a string that imitates an
SQObjectPtr struct and place it next to the array of
In the exploit we developed for demonstration purposes, we tricked the engine into believing that it fetched a pointer to a Squirrel Array. Squirrel Arrays are dynamic arrays, where 2 fields are relevant to exploitation:
- A pointer that contains the address of the current array buffer
- An 8-byte integer that contains the size of the current array buffer
By making the fake
SQObjectPtr point to another attacker-controlled string on the heap, it was possible to trick Squirrel into returning an array that points to the base address
0x0 and contains
This enabled us to abuse the fake array to address the entire process space and read and write values. Ultimately, we were able to hijack the control flow of the program and gain full control of the Squirrel VM. This was achieved by overwriting function pointers. The following graphic shows this chain of attacker-controlled pointer that enabled reading and writing to the entire address space:
|We send the vulnerability details via email to the email address listed in the Squirrel GitHub repository.
|We create a GitHub issue asking for the correct point of contact
|The maintainer replies with an email address we can disclose the vulnerability details to. We disclose the vulnerability details..
|The maintainer acknowledges the vulnerability.
|A commit containing a patch is pushed to the Squirrel GitHub repository by the maintainer.
In this blog post, we explained the details of an Out-Of-Bounds vulnerability in SquirrelLang. We outlined how storing information within an index value can lead to logical bugs if the bits representing this information are set too low. We also discussed how such issues might be exploited to escape a Squirrel VM and execute arbitrary code on a host process. We broke down how this might affect Counter-Strike: Global Offensive players to illustrate how such a vulnerability can be leveraged in the real world. We highly recommend maintainers that are using Squirrel to apply the available fix commit to their projects to protect against these attacks. Last but not least, we would like to thank the Squirrel team for quickly making a patch available for this issue after our reporting.