A Roadmap to Security Game Testing: Finding Exploits in Video Games

Introduction

In this guide, I'll walk you through how I create tools to find exploits in video games for bug bounty programs. Specifically, I'll focus on my research into the game Sword of Convallaria. This exploration is purely for educational purposes. As such, I have removed some of the assets as an exercise for the user to find.

All source code can be found on GitHub

Game Details

Sword of Convallaria is available on both PC and mobile platforms, currently boasting around 2,000 concurrent active users on Steam (source: SteamDB). The game monetizes through pay-to-win microtransactions and features PvP gameplay. Given its size, the developers should be concerned about potential exploits, yet they lack a formal bug bounty program.

The game is built on Unity and uses Lua for much of its game logic and processing. The network protocol employs HTTPS for the authentication/login flow and utilizes UDP packets with protobuf messages for in-game communication.

High-Level Plan for Reverse Engineering

  1. Extract raw files to outline the packet structures and translating IDs into English strings.
  2. Analyze the login authentication flow and lobby server.
  3. Investigate game traffic and server interactions.

Acquiring and Dumping Game Data

Since Sword of Convallaria is developed in Unity, I used existing tools to extract game data, specifically AssetsTools.NET is helpful for this process.

While extracting the files isn't the most exciting part and is well-documented, it did reveal some intriguing Lua and protobuf files. These files contain everything needed to create network tools. Here is how I dump all of those relevant files:

foreach (var luabase in Directory.GetFiles(temp + "unity3d\\lua"))
{
    var manager = new AssetsManager();
    var bunInst = manager.LoadBundleFile(new MemoryStream(File.ReadAllBytes(luabase)), "fakeassets.assets");
    var fileInstance = manager.LoadAssetsFileFromBundle(bunInst, 0, false);
    var assetFile = fileInstance.file;
    foreach (var asset in assetFile.GetAssetsOfType(AssetClassID.TextAsset))
    {
        var textBase = manager.GetBaseField(fileInstance, asset);
        var m_Name = textBase["m_Name"].AsString;
        var m_Script = textBase["m_Script"].AsByteArray;
        var fileName = temp + @"luac\" + Path.GetFileNameWithoutExtension(luabase) + @"\" + m_Name.Replace("_", @"\\") + ".luac";
        if (m_Name.EndsWith(".proto")) fileName = temp + Path.GetFileNameWithoutExtension(luabase) + @"\" + m_Name;
        if (File.Exists(fileName)) continue;
        Directory.CreateDirectory(Path.GetDirectoryName(fileName));
        File.WriteAllBytes(fileName, m_Script);
    }
}

Let's dig into them.

Converting Lua Bytecode to Human-Readable Scripts

The Lua bytecode appears encrypted based on the entropy of the data. To analyze it, I hooked the slua.dll function responsible for loading Lua code. This allowed me to examine the loaded bytecode and, crucially, dump a stack trace to identify the encryption method. I discovered it uses a straightforward XOR cipher where the first byte is excluded from the rotation and is XOR'd separately.

data[0] = (byte)(data[0] ^ 0x35); 
var key = new byte[] { 0x17, 0xf1, 0xc3, 0x55, 0x78, 0x64, 0x39, 0x40, 0x42, 0x77, 0x59, 0x12, 0x33, 0xcb, 0x7b, 0xb9, 0x35 }; 
for (var i = 1; i < data.Length; i++) 
    data[i] = (byte)(data[i] ^ key[(i - 1) % key.Length]); 

With the Lua payload decrypted, I used a Lua decompiler, specifically UnluacNET. Initially, the decompilation failed due to an incorrect magic number. I verified the expected magic number in slua.dll.

After addressing the magic check, I encountered further failures. A binary diff between my built slua.dll and the game's version revealed differences in the read functions, which led me to identify another layer of encryption.

for (var i = 2ul; i < (ulong)buffer.Length; i++)
{
    var key = 0x20210507 * i;
    var idx = i % 3;
    if (idx == 1)
        buffer[i] = (byte)(((byte)((key >> 16) & 0xFF) - i) ^ buffer[i]);
    else if (idx == 2)
        buffer[i] = (byte)(((key >> 21) | i) ^ buffer[i]);
    else
        buffer[i] = (byte)(((key >> 28) + (key & 1) + i) ^ buffer[i]);
}

After fixing the read function, I still faced issues with strings. A further diff of slua.dll revealed a crucial offset.

sizeT.m_big -= 10;

Now, I was able to read the raw text of all the decompiled Lua scripts, which contain both game logic and data tables.

Understanding the Network Protocol

Most games that prioritize security will prevent common tools like Fiddler from functioning, but it's always worth trying, as many developers overlook security. In this case, the developers had implemented some protections, but since the game is built on Unity, it was relatively easy to bypass these restrictions and enable system proxies. There are many others that do il2cpp mod tutorials, and I recommend those, with the key part being hooking HttpClientHandler.SendAsync.

With Fiddler active, I could observe the login flow and how tokens are transmitted. In this example, there are some basic client identifiers, such as a ClientId and AppId, with the actual user content being in the post params. For guest accounts, it's a randomly generated string. For Steam accounts and Google accounts, it's the standard token you receive from those OAuth services. The main part of the auth response that is important is the AccessToken and MacKey, as this will be used to identify yourself to the game server.

Game traffic can be easily monitored using Wireshark. Upon analyzing the UDP data, I recognized it as protobuf, which I confirmed using a generic protobuf decoder (I recommend this one). The packet header typically includes the packet length, opcode, and occasionally other details like a counter or encryption/compression status. This packet header is as follows:

var length = BitConverter.GetBytes(packetBytes.Length);
Array.Copy(length, 0, packetBytes, 0, 4);
var opcode = BitConverter.GetBytes((ushort)Enum.Parse<CtoSPacketMessageIds>(packet.GetType().Name));
Array.Copy(opcode, 0, packetBytes, 4, 2);
var count = BitConverter.GetBytes(counter);
Array.Copy(count, 0, packetBytes, 6, 4);
Array.Copy(payload, 0, packetBytes, 10, payload.Length);

The final step was to identify the opcodes, which are hardcoded in the Lua scripts as tables.

foreach (var mode in modes)
{
    opcodes[mode] = new Dictionary<int, string> { };
    foreach (var c2s in Directory.GetFiles("temp\\lua\\pb\\", "*proto.lua", SearchOption.AllDirectories).Where(e => e.Contains(mode)))
    {
        var luaLines = File.ReadAllLines(c2s);
        foreach (var l in luaLines)
        {
            if (l.Contains(".id = "))
            {
                var hasOpcode = int.TryParse(l.Split(' ').Last(), out int opcode);
                if (!hasOpcode || opcode == 0) continue;
                var name = l.Split(' ')[0].Split('.')[1];
                opcodes[mode][opcode] = name;
            }
        }
    }
}

Automating Updates

When the game updates, it is critical to make it easy to update. This is an extremely important step to make sure the tooling doesn't break from week to week. I have included how to download the raw asset files directly from the game servers in Downloader.cs.

The dumper project glues together all the above steps to make it a one-button push to update to the latest version.

Putting It All Together

With all these components in place, I can integrate them to conduct security testing. I usually create a simple project that logs in and sends various packets for quick and efficient testing.

Here is a quick test I did to check to basic Gacha functionality.

await client.SendPacket(new CSOnlineGacha { Id = 2, Times = 10, Consume = new DBConsume { Type = 114, Param0 = 1, Param1 = 10 } });

This is where I'd try negative numbers, funky patterns, etc.

If I had infinite time, I'd also check some lower level vulnerabilities such as protobuf parsing failures.

Conclusion

This guide on security game testing in Sword of Convallaria provides a framework for identifying vulnerabilities in video games. By using the techniques outlined above, you can enhance your skills in finding exploits and contribute to a more secure gaming environment. If you have any questions or insights on security testing, feel free to reach out to me!

Source

GitHub


Call to Action: If you found this post helpful, please share it on social media or check out my other articles on game security and testing.