Loading HuntDB...

CS:GO Server -> Client RCE through OOB access in CSVCMsg_SplitScreen + Info leak in HTTP download

Critical
V
Valve
Submitted None

Team Summary

Official summary from Valve

Title: CS:GO Server -> Client RCE through OOB access in CSVCMsg_SplitScreen + Info leak in HTTP download Scope: csgo.exe Weakness: Out-of-bounds Read Severity: Critical (9.6) Link: https://hackerone.com/reports/1070835 Date: 2021-01-04 00:22:02 +0000 By: @simonscannell Details: We managed to write an extremely reliable exploit for CS:GO on Windows by chaining an OOB access that leads to RIP control and an Info leak. This report consists of two critical bugs. The memory leak was initially reported in #1064367, where the other bug in the same report was a duplicate and the OOB access in #1064809. Altough #1064809 is still open as an explot was expected, we still wanted to make a new report where both bugs are in one place, as they are both two completely unique bugs. Here is a PoC video of the exploit in action: {F1143323} # Vulnerability details ## Bug #1: Information leak through HTTP downloads During vulnerability research, we found out that CS:GO dedicated servers can host custom map and game modes, all of which depend on custom files. Game client's can connect to these servers and download these files dynamically. Usually, a CS:GO server maintainer would create a list of required files and write them into a `.res` file, which has the same name as the map the server is running on. We found this feature highly interesting, as it involves interaction with yet another untrusted server. After reversing the code that actually handles the HTTP download on the client side on Linux, we found out that it was handled by curl and the logic seems to be the following: 1. Create a CURL object and set all necessary options (URL, timeout, User-Agent etc) 2. Register a callback that is called for every HTTP header the server sends 3. Register a callback that is called when the data of the body of the HTTP response is received This logic can be seen in the following screenshot: {F1143263} After looking at the code that implements the callbacks, we found that the following logic applies: 1. The header callback looks for the `Content-Length` header and uses its value to allocate a buffer to fit the file into. It does so through a case-sensitive string search. 2. The write callback then writes the data that is received into the buffer 3. After the download finished, it is written to the destination file. Here is a screenshot showing how a case-sensitive search is implemented and how it is used to allocate a buffer: {F1143269} The bug that occurs here is that if two `Content-Length` headers are sent, but one is written with uppercase characters (`Content-Length`) and the other header with all lower case characters `content-length`, then the CS:GO code will reocognize only the first and allocate a buffer with whatever size is within the field. The second header is however missed by the CS:GO code but not by curl. Here is an example HTTP response to demonstrate this: ``` HTTP/1.1 200 OK Content-Type: text/html Content-Length: 1337 content-length: 0 Connection: closed ``` If such a response is received, a buffer of size **1337** is allocated. However, the second `Content-Length` header tells the HTTP client, in this case curl, that no data will follow. This means that no data is ever actually written into the file buffer. The next bug is that CS:GO does not check if the data that has been received matches the size of the buffer. It will still write the allocated buffer into a file. However, since no data has been written, an attacker controlled size of uninitalized Heap memory is written to the file. An attacker can then use the `NETMsg_File` message to have the client upload the just created file containing uninitialized memory. The attacker is then able to view memory contents of the user's process that have been freed before. In our case, we parsed the file and searched for pointers that we used to break ASLR in the client's game process and further exploit the client. ## Bug #2: OOB access in CSVCMsg_SplitScreen The source engine supports splitscreen users in games. We noticed this fact when looking through the available protobuf messages for CS:GO clients when we spotted the following message: ``` message CSVCMsg_SplitScreen { optional .ESplitScreenMessageType type = 1 [default = MSG_SPLITSCREEN_ADDUSER]; optional int32 slot = 2; optional int32 player_index = 3; } ``` This message was interesting to us, as it contained an index. As it turns out, it is not the `player_index` but the `slot` field that is used as an array index into a statically sized, global array based in `engine.dll`. The following screenshot must be understood with the context in mind that `ecx` points to a global array in the `.data` section of `engine.dll`. `edx` is controlled by an attacker and corresponds to the `slot` field. There are no checks made on this value: {F1143270} As can be seen, a pointer to some object, probably an object representating a split screen user is fetched from the array. The first byte of the object is then tested for a NULL byte. if the first byte of whatever the object is pointing to is 0, the function continues execution and arrives at a section that is highly interesting to an attacker: {F1143272} Keep in mind that `ebx` was a value that was derived through an OOB access and could be attacker controlled. What we see here is that a vtable is dereferenced at offset **8** from `ebx`. This vtable is then used to jump to a dynamic location. This means that if an attacker can control the contents of what `ebx` points to, he can point to a fake vtable under his control and ultimately control the Program Counter and thus gains the ability to execute code remotely. # Exploitation With these two powerful bugs at hand, we went ahead and wrote an exploit. The exploitation strategy was to use the info leak to break ASLR and obtain the base address of the `engine.dll` file in memory. From there, we could use the OOB access to hijack the `eip` register and make it point somewhere useful in `engine.dll` and execute a ROP chain. ## Breaking ASLR Breaking ASLR happens in two steps: 1. Allocate and deallocate an object containing a pointer to `engine.dll` thousands of times on the heap. 2. Have the client write multiple files of uninitialized memory of the same size as the object created in the previous step to disk and upload it to the server. By abusing known behavior of the Windows Heap allocator and of the objects, it is extremely likely that one of the objects containing a pointer to `engine.dll` can be found in the file. During testing, this exploit fails around 2/100 times. We found that the message `CSVCMsg_SendTable` can be sent a variable amount of times to the client, where each message allocates a buffer of a variable number of `sendprop_t` objects on the heap. As it turned out, this object contains a function pointer into `engine.dll`. It's definition is shown here: ``` message CSVCMsg_SendTable { message sendprop_t { optional int32 type = 1; // SendPropType optional string var_name = 2; optional int32 flags = 3; optional int32 priority = 4; optional string dt_name = 5; // if pProp->m_Type == DPT_DataTable || IsExcludeProp optional int32 num_elements = 6; // else if pProp->m_Type == DPT_Array optional float low_value = 7; // else ... optional float high_value = 8; // ... optional int32 num_bits = 9; // ... }; optional bool is_end = 1; optional string net_table_name = 2; optional bool needs_decoder = 3; repeated sendprop_t props = 4; } ``` We created this message and sent it `256` times with some unique values, as demonstrated here: ```Python def spray_send_table(s, addr, nprops): table = nmsg.CSVCMsg_SendTable() table.is_end = False table.net_table_name = "abctable" table.needs_decoder = False for i in range(nprops): prop = table.props.add() prop.type = 0x1337ee00 prop.var_name = "abc" prop.flags = 0 prop.priority = 0 prop.dt_name = "whatever" prop.num_elements = 0 prop.low_value = 0.0 prop.high_value = 0.0 prop.num_bits = 0x00ff00ff tosend = prepare_payload(table, 9) s.sendto(tosend, addr) ``` The reason for these unique values is that we can search them for them in the files the client uploads, which contain uninitialized memory. If we find these values, we can be sure that a `sendprop_t` object, along with a function pointer has been included in the info leak files. Here is the corresponding code that parses the uninitialized memory sent by a client: ```Python for i in range(len(data) - 0x54): vtable_ptr = struct.unpack('<I', data[i:i+4])[0] table_type = struct.unpack('<I', data[i+8:i+12])[0] table_nbits = struct.unpack('<I', data[i+12:i+16])[0] if table_type == 0x1337ee00 and table_nbits == 0x00ff00ff: engine_base = vtable_ptr - OFFSET_VTABLE ``` By simply subtracting the offset of the vtable within `engine.dll`, it was possible to leak the client's `engine.dll` base address in memory. ## Hijacking RIP We mentioned that the second bug, an OOB access can be used to control a pointer. Since the array where the OOB access occurs in is a global variable and located within the `.data` section of `engine.dll`, we thought that it would be best to search for a controlled value within this `.dll` file to make the pointer, contained within `ebx` as shown previously, point to an attacker-controlled fake object containing a fake vtable. As it turns out, one of the mechanisms through which server and client setup a game are `convars`. These `convars` contain information such as the URL from which to download files from or any other configuration settings of a server or client. They are sent through the `CMsg_CVars` message ``` message CMsg_CVars { message CVar { optional string name = 1; optional string value = 2; optional uint32 dictionary_name = 3; // In order to save on connect packet size convars that are known will have their dictionary name index sent here } repeated CVar cvars = 1; } message CNETMsg_SetConVar { optional CMsg_CVars convars = 1; } ``` As can be seen here, a variable amount of `convar`s is sent to the client by the server, each containing a controlled name and value field. These `convars` are global objects, located within `engine.dll`. When a server sends a `convar`, the string value is copied to the heap and a pointer to it saved within the global `convar`'s object. This means that if our exploit setup a fake object by crafting a string value containing a fake object and vtable, a pointer to it is stored at a known offset within the global binary. Here is an illustration showing the chain of pointers that is going to happen: {F1143293} The illustration shows how the splitscreen array OOB is used to interpret the pointer to the string value of a `convar` as an object. The fake object's vtable pointer points back into `engine.dll` into another convar. The `convar`s turned out to be a very powerful gadget, as they could take both string and integer values and store them each into their object's memory. With this target memory layout in mind, let's look at the code that dereferences the fake object to vtable to `eip` again: {F1143272} Keep in mind, `ebx`, if the `slot` field is set accordingly, contains the pointer to the controlled string value of a `convar`. It then dereferences it at offset **8** and moves the result into `eax`. This corresponds to the fake vtable pointer pointing back at the convar in the illustration. `eax` is then dereferenced at offset `0xAC`. By setting up another convar with an integer value, we can control the result of this pointer dereference which is then `call`'d and thus arbitrary RIP control is gained. Since all offsets are known, this step of the exploit chain is 100% reliable. ## ROP We then utilized a technique called `ROP`chaining to setup a an attacker controlled stack and finally execute the `ShellExecuteA()` Windows standard library function to execute arbitrary system commands. In our case, we simply spawned a calculator. # Reproduction Attached is a `poc.zip` file containing all required files to launch the PoC exploit. Unpack it into a directory and activate its Python3 virtual environment via the command (on Windows' cmd): ``` .\poc_env\Scripts\activate ``` Then execute the script through `python .\poc.py` Then, on Windows, start the CS:GO game and activate the developer console: 1. Start the game and within the game settings enable the developer console. Game-> Enable Develoepr Console-> yes 2. Then, within the settings go to Mouse / Keyboard-> UI keys -> Toggle Developer Consoleand set it to your preferred key Finally, open the developer console and connect to the fake server via the command: `connect YOUR_IP:1337` Replace `YOUR_IP` with your local LAN IP (192.*) A calculator should be opened. If the exploit fails for some reason, just try again. Please do not hesitate to point out exploit failures or questions on reproduction. We have tested the exploit highly successfully against 3 different Windows 10 machines. ## Impact This exploit allows an attacker in control of a malicious CS:GO server to execute arbitrary code, including malware, on any client that connects to the server. An attacker could steal Steam credentials or take over the machine and use it for further malicious purposes. These two vulnerabilities can be exploited extremely reliable on both Windows and Linux clients.

Reported by simonscannell

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Bounty

$7500.00

Submitted

Weakness

Out-of-bounds Read