In the Steam client, eliminate the dangerous vulnerability that has been hiding there for ten years

image

Lead researcher Tom Kort from Context, an information security company, talks about how he managed to detect a potentially dangerous bug in Steam client code.

Carefully monitoring security PC players noticed that Valve recently released a new update to the Steam client.

In this post, I want to justify myself for playing games at work and telling the story of the associated bug that existed in the Steam client for at least ten years, which until July last year could lead to remote code execution (RCE) in all 15 million active clients.

Since July, when Valve (finally) compiled its code with the included modern exploit defenses, it could only lead to client failure, and RCE was possible only in combination with a separate information leakage vulnerability.

We announced Valve about the vulnerability on February 20, 2018, and, to the credit of the company, it eliminated it in the beta branch less than 12 hours later. The fix was transferred to a stable branch on March 22, 2018.

image

Short review


The basis of the vulnerability was damage to the heap inside the Steam client library, which could be called remotely, in the part of the code that was engaged in restoring a fragmented datagram from several received UDP packets.

The Steam client performs data exchange through its own protocol (Steam protocol), which is implemented over UDP. There are two areas in this protocol that are particularly interesting due to the vulnerability:


The error was caused by the absence of a simple check. The code did not verify that the length of the first fragmented datagram is less than or equal to the total length of the datagram. This seems to be a common oversight, given that for all subsequent packets that transmit fragments of the datagram, the check is performed.

Without additional data leakage bugs, heap corruption on modern operating systems is very difficult to control, so remote code execution is difficult to implement. However, in this case, thanks to Steam's own memory allocator and the ASLR that was not in the binary steamclient.dll (before last July), this bug could be used as the basis for a very reliable exploit.

Below is a technical description of the vulnerability and its associated exploit until
implement code execution.

Vulnerability Details


The information you need to understand


Protocol


Third parties (for example https://imfreedom.org/wiki/Steam_Friends ) based on an analysis of the traffic generated by the Steam client, performed reverse engineering and created detailed documentation of the Steam protocol. The protocol was initially documented in 2008 and has not changed much since then.

The protocol is implemented as a transmission protocol with a connection established over a UDP datagram stream. Packages, in accordance with the documentation for the link above, have the following structure:

image

Important aspects:


Encryption


The useful information of the datagram packet is encrypted with AES-256 using a key negotiated between the client and the server in each session. Key reconciliation is performed as follows:


Vulnerability


Vulnerability is present inside the CvDPConnection class RecvFragment method . In the release version of the steamclient library, the characters are missing, however, when searching through the binary strings in the function of interest, we find a link to " CUDPConnection :: RecvFragment ". Entry to this function is performed when a client receives a UDP packet containing a Steam datagram of type 0x6 (“datagram fragment”).

1. The function begins by checking the connection status to make sure that it is in the " Connected " state.
2. Then the data_len field in the Steam datagram is checked to ensure that it contains less than 0x20000060 bytes (it looks like this value is chosen arbitrarily).
3. If the test is passed, the function checks whether the connection collects fragments of a datagram, or is it the first packet of the stream.

image

4. If this is the first packet in the stream, then the split_count field is then checked to find out how many packets this stream will stretch.
5. If the stream is divided into several packets, then the seq_no_of_first_pkt field is checked to ensure that it matches the sequence number of the current packet. This ensures that the packet is first in the stream.
6. The data_len field is checked again against the limit of 0x20000060 bytes. It also checks that split_count is less than 0x709b packets.

image

7. If these conditions are met, then a boolean value is specified, indicating that we are now collecting fragments. It also checks that we do not yet have a buffer allocated for storing fragments.

image

8. If the pointer to the fragment collection buffer is not zero, then the current fragment collection buffer is freed and a new buffer is allocated (see the yellow rectangle in the figure below). This is where the error occurs. It is expected that the fragment collection buffer is allocated in the size of data_len bytes. If everything is completed successfully (and the code does not perform the check — a minor error), then the useful information of the datagram is copied into this buffer using a memmove , trusting that the number of bytes to be copied is indicated in the packet_len .

The most important developer's oversight was that the " packet_len is less than or equal to data_len " check is not performed. This means that it is possible to transfer data_len less than packet_len and have up to 64 KB of data (due to the fact that the packet_len field is 2 bytes wide) copied into a very small buffer, which leads to the possibility of heap damage exploit.

image

Exploitation of Vulnerability


This section assumes that there is an ASLR bypass method. This leads to the fact that before the start of operation, the starting address of the steamclient.dll is known.

Packet Spoofing


In order for an attacker’s UDP packets to be received by the client, it must examine the outgoing (client -> server) datagram, which is sent in order to know the client / server connection identifiers, as well as the sequence number. Then, the attacker must spoof the IP addresses and source / destination ports along with the client / server IDs and increment the learned sequence number by one.

Memory management


To allocate more than 1024 (0x400) bytes of memory, a standard system allocator is used. To allocate memory less than or equal to 1024 bytes, Steam uses its own allocator that works the same on all supported platforms. This article will not discuss in detail this distributor, except for the following key aspects:

  1. From the system distributor, large blocks of memory are requested, which are then divided into fragments of a fixed size for use under requests for the allocation of memory by the Steam client.
  2. The selection is performed sequentially, there is no metadata between the fragments used.
  3. Each large block stores its own list of free memory, implemented as a single-linked list.
  4. The top of the free memory list points to the first free fragment in memory, and the first 4 bytes of this fragment point to the next free fragment (if it exists).

Memory allocation


When allocating memory, the first free block is detached from the top of the free memory list, and the first 4 bytes of this block, corresponding to next_free_block , are copied to the freelist_head member variable inside the distributor class.

Memory release


When a block is freed, the freelist_head field is copied to the first 4 bytes of the block to be released ( next_free_block ), and the address of the block to be released is copied to the member variable freelist_head of the distributor class.

How to get a primitive record


A heap buffer overflow occurs in the heap, and depending on the size of the corrupted packets, memory allocation can be controlled either by a standard Windows allocator (when allocating more than 0x400 bytes of memory) or its own Steam distributor (when allocating less than 0x400 bytes). Due to the lack of security measures in my own Steam distributor, I decided that it was easier for an exploit to use it.

Let us return to the section on memory management: it is known that the top of the free list of blocks of a given size is stored as a member variable of the distributor class, and a pointer to the next free block in the list is stored as the first 4 bytes of each free block in the list.

If there is a free block next to the block in which the overflow occurred, damage to the heap allows us to overwrite the next_free_block pointer. If we consider that the heap can be prepared for this, then the rewritten next_free_block pointer can be set to the address for recording, after which the subsequent memory allocation will be written to this place.

What to use: datagrams or fragments


A memory corruption error occurs in the code responsible for processing fragments of datagrams (type 6 packets). After the occurrence of damage, the RecvFragment () function is in a state in which it waits to receive further fragments. However, if they arrive, then a check is performed:

fragment_size + num_bytes_already_received < sizeof(collection_buffer)

But it is obvious that this is not the case, because our first packet has already violated this rule (the existence of an error may skip this check) and an error will occur. To avoid this, you need to avoid the CUDPConnection :: RecvFragment () method after memory corruption .

Fortunately, CUDPConnection :: RecvDatagram () can still receive and process type 7 packets (datagrams) being sent , while RecvFragment () is not in effect, and this can be used to start the record primitive.

Encryption issues


The packets received by RecvDatagram () and RecvFragment () are expected to be encrypted. In the case of RecvDatagram (), decryption is performed almost immediately after receiving. In the case of RecvFragment (), it occurs after receiving the last fragment in the session.

There is a problem of exploiting the vulnerability, because we do not know the encryption key that is created in each session. This means that any OP code / shell code we send will be “decrypted” using AES256, which will turn our data into garbage. Therefore, it is necessary to find a way of exploitation, possible almost immediately after receiving a packet, before the decryption procedure will be able to process the useful information contained in the packet buffer.

How to achieve code execution


Considering the decryption limitation described above, the operation must be performed before the decryption of the incoming data. This imposes additional restrictions, but the task is still executable: you can overwrite the pointer so that it points to the CWorkThreadPool object stored in a predictable place inside the data section of the binary file. Although the details and the internal functionality of this class are unknown, it is possible to assume from its name that it supports a pool of threads that can be used when it is necessary to perform “work”. After examining several debug strings in a binary file, one can understand that among such jobs there is encryption and decryption ( CWorkItemNetFilterEncrypt , CWorkItemNetFilterDecrypt ), so when these tasks are queued, the CWorkThreadPool class is used . By rewriting this pointer and writing the right place to it, we will be able to simulate the vtable pointer and its associated vtable, which allows us to execute code, for example, when calling CWorkThreadPool :: AddWorkItem () , which always takes place before any decryption process.

The figure below shows the successful exploitation of the vulnerability up to the stage of gaining control over the EIP register.

image

From this point on, you can create a chain of ROP, leading to the execution of arbitrary code. The video below shows how an attacker remotely launches a Windows calculator in a fully patched version of Windows 10.


Summing up


If you get to this part of the article, thank you for your perseverance! I hope it became clear to you that this is a very simple bug that was fairly easy to exploit due to the lack of modern means of protection against exploits. The vulnerable code was probably very old, but otherwise it worked well, so the developers did not see the need to examine it or update the build scripts. The lesson here is that it is important for developers to periodically review old code and build systems to ensure that they meet modern security standards, even if the code functionality itself remains unchanged. It was amazing to find in 2018 such a simple bug that has such serious consequences on a very popular software platform. This should be an incentive to search for such vulnerabilities for all researchers!

Finally, it’s worth talking about the process of responsible disclosure. We reported this bug to Valve in a letter to its security service ( security@valvesoftware.com ) at about 4 pm GMT and just 8 hours later, a fix was created and launched into the beta thread of the Steam client. Thanks to this, Valve is now in first place in our (imaginary) table of the contest “Who will fix the vulnerability faster” - a pleasant exception compared to revealing the mistakes of other companies, which often often results in a lengthy approval process.

A page that describes the details of all client updates.

Source: https://habr.com/ru/post/413003/


All Articles