Today many games are developed using .Net or a modified .Net Runtime like the Unity engine. This of course means that deserialization vulnerabilities in .Net can also occur in these games.
Serialization functionality seems to mainly be used to store save games. Less frequent uses include user generated content or network transfer of information.
We’ve discovered several such vulnerabilities in games using our own tooling and will elaborate on a few of them as well as common patterns and pitfalls as well as mitigations here.
It goes without saying that all of the vulnerabilities discussed below have already been fixed.
Tooling and Approach
To find potential deserialization vulnerabilities we used our own NetGadget tool, which we plan to release in the near future.
To interactively analyze vulnerabilities and debug potential exploits we used dnSpy which is an amazing .Net decompiler, analyzer and debugger.
As a test for the NetGadget tool we scanned a complete Steam Library for usage of BinaryFormatter, as a potential sink for a deserialization attack, as seen in Figure 1:
Our tool scans any .dll or .exe file it encounters for a method call matching the pattern given by the -m parameter and stores the information about them in one file per dll or exe.
Since we are looking for vulnerabilities in games, the Assembly-CSharp.dll files, which are where Unity stores a game's custom code, are of particular interest and are highlighted below.
Each of the generated output files contains information about which functions were called and from where.
Of particular interest are of course calls to BinaryFormatter::Deserialize, since they are potential sinks for a deserialization attack.
When a game using BinaryFormatter is discovered, we need to check for
deserialization gadgets within it. Because Unity for instance only loads
DLLs included in the games folders, it is necessary to verify which
gadgets actually exist since not all known gadgets for that .Net version
may be available.
Using NetGadget this process can be significantly sped up, since it can
automatically scan for the existance of such gadgets.
Figure 4 for example shows a scan of the game Dragon Cliff. It uses BinaryFormatter insecurely, but the only potential gadget chain our tool reports is a false positive. This particular false positive generally shows up when .Net 2.0 is being used and gives a good indication that no gadget chains are available.
If you compare this to Figure 5, the scan of Tabletop Simulator, a lot more gadget candidates are shown, among them some well known gadget chains, like TempFileCollection or TypeConfuseDelegate^1^.
Once a potential entry point and the existence of deserialization gadgets have been confirmed, as a next step the potential entry points and their call trees need to be evaluated manually with dnSpy or a similiar tool.
This approach has proved efficient in quickly reducing the number of games potentially vulnerable to .Net deserialization attacks. The scanned steam library contained around 400 games (not all of them made in .Net). Eventually around 30 vulnerable games were identified of which 14 were exploitable.
Deserialization Attacks in Totally Accurate Battle Simulator
We will refrain from using screenshots of code that isn't ours. For this reason the following explanations of the details of certain vulnerabilities will discuss the control flow and function names, but not the details of the code in question.
Where is serialization used
Totally Accurate Battle Simulator by Landfall Games is a sandbox game
that simulates battles with ragdoll physics.
BinaryFormatter is used to let Players build custom battles and
campaigns and to share them over the Steam Workshop
NetGadget flagged the following methods as using the call to BinaryFormatter::Deserialize:
- Landfall.TABS.Workshop.CampaignHandler::GetLoadedCampaignFromDisk
- Landfall.TABS.Workshop.CampaignHandler::LoadFactionFromDisk
- Landfall.TABS.Workshop.CampaignHandler::GetLoadedLayoutFromDisk
- Landfall.TABS.Save.SteamSavesLoader::ReadLocal
- Landfall.TABS.Save.SteamSavesLoader::OnRemoteStorageFileReadAsyncComplete
This leads to two distinct vulnerabilities. One involves the loading of save games and affects the last two functions, the other involves the loading of custom campaigns and battles and affects the first three.
Save Game Vulnerability
This vulnerability is short and sweet.
Since OnRemoteStorageFileReadAsyncComplete will eventually also
call ReadLocal, it's enough to investigate what is required to
exploit ReadLocal.
ReadLocal simply reads all bytes of the save file and deserializes them with BinaryFormatter. Thus it's enough to overwrite the save file to trigger a deserialization vulnerability.
After replacing the save game with a TypeConfuseDelegate payload and starting the game, we see Figure 6 - Success!
Workshop Vulnerability
This vulnerability is a little more involved, but can be used remotely through the Steam Workshop. Due to the nature of the workshop this vulnerability was only tested locally to prevent the accidental spread of the exploit.
The loading of a faction, campaign or layout from disk will deserialize the associated data. All of those functions are internally called either when a mod is already installed and is being loaded or through a custom content loader's LoadLocalCustomContent method.
Depending on the type of the content, it will invoke either of the vulnerable functions. The LoadLocalCustomContent itself is only called by the content loader's QuickRefresh method.
The QuickRefresh method is called from a lot of different sections in code, most interestingly by OnDownloadSuccess and OnSubSuccess as well as the onModBinaryInstalled callback.
This means that after a mod has been downloaded it will immediately be deserialized, allowing an attacker to trigger the vulnerability by uploading a mod to the Steam Workshop
Deserialization Attacks in Tabletop Simulator
We will refrain from using screenshots of code that isn't ours. For this reason the following explanations of the details of certain vulnerabilities will discuss the control flow and function names, but not the details of the code in question.
Where is Serialization Used
Tabletop Simulator by Berserk Games is a multiplayer physics sandbox that allows players to create and play tabletop games remotely.
In order to facilitate the exchange of games and the storing of table states the game employs two different methods of serialization: Newtonsofts' JsonSerializer and BinaryFormatter. The vulnerabilities we've found lie in the parts of the code that use BinaryFormatter.
BinaryFormatter is used to load what the code refers to as a PhysicsState.
The deserialization of such a physics state can either be triggered over the network through the LoadPromotedPhysicsState RPC call or through the loading of a mod that isn't in JSON format.
The RPC Vulnerability
Tabletop Simulator has an RPC mechanism that is, among other things, used to administrate a game host remotely.
One of these administrative functions is
LoadPromotedPhysicsState.
It gets passed an array of bytes which are then used to call
GetPhysicsState, which in turn calls
BinaryFormatter::Deserialize with the data. To call this
function two criteria need to be met.
- Firstly it may only be called on the host of a game.
- Secondly it may only be called by users with admin privileges in that hosts session.
- Requiring administrative privileges on a server to exploit the host seems restrictive at first. A server’s host can however migrate the hosting of the game to another client on the server. This causes two main changes in user roles in the game.
- Firstly it makes the client, that was migrated to, a host, fulfilling the first requirement.
- Secondly the former host is promoted to admin in the new hosts session, fulfilling the second requirement.
- Thus a malicious host can meet both requirements by migrating the host to their target. They can then send an arbitrary byte array to the newly created host’s LoadPromotedPhysicsState method, triggering deserialization with arbitrary data.
This allows a deserialization attack to be performed which can execute arbitrary code remotely.
The following video demonstrates this attack from the victims perspective.
The Mod Vulnerability
In Tabletop Simulator users can upload their table setups and
scripts into the Steam Workshop to share them with the community.
These mods can be subscribed to and downloaded to use by any player.
When a mod has finished downloading it will eventually be
deserialized.
When the download from the Steam Workshop has completed the
CallResultWorkshopReadDownload callback method will be invoked.
This method in turn calls SerializationScript::Save which
attempts to load the file contents with the GetPhysicsState
method first and the JSON Serializer second.
GetPhysicsState then invokes BinaryFormatter::Deserialize.
A malicious attacker is able to upload a mod that will execute arbitrary code when deserialized, potentially allowing it to spread through the Steam Workshop.
Mitigations
In its default configuration BinaryFormatter in .Net is insecure. Several well known gadget chains exist that can be easily used to gain arbitrary code execution.
BinaryFormatter does however allow the definition of a Binder class which handles the resolution of types used in the Serialization and Deserialization processes.
Using such a Binder, whitelisting of expected types can be facilitated with relative ease, providing resilience against deserialization gadgets. This method was used to fix all of the vulnerabilities mentioned above. Following is an example Binder that allows the restriction of deserialized types to those required to deserialize the generic type T. It is provided under the GNU All-permissive license.
|
|