Vampire Crawlers Save Game Reverse Engineering

screenshot_from_steam

I stumbled on Vampire Crawlers and being a fan of such games, I played the demo and bought it afterwards.

But of course, while playing the game is fun, I always ask myself if there is any protection against “save modification” (e.g., cheats). Come with me as I walk through the small act of reverse engineering the protection.

Gathering information

First of all, the save game location is, on Windows %USERPROFILE%/AppData/LocalLow/Nosebleed Interactive/Vampire Crawlers/Save.

The format is also a bit documented on the (official ?) wiki of Vampire Crawlers.

The game itself is programmed with Unity.

The save file

The save itself looks like this at the beginning:

{
  "Version": 1,
  "Checksum": "JLtluSsAwpKKCDc5P7PZ9fOuDOM=",
  "Data": {
    "ProfileId": "SaveProfile0",
    "saveSystemVersion": "0.22.0",
    "BuildSKU": 0,
    "GameSaveData": {
      "ProfileId": "SaveProfile0",
      "SaveObjects": []
    },
    "MoreData....": {}
  }
}

From a quick glance, it’s quite clear that the actual save (seems to be format 0.22.0) is itself wrapped inside another JSON block. I can infer such because is one of the game I recently helped with, I did almost exactly the same things (although without a Checksum field). The principle is that you have your “real” save protected by an outer layer that helps you with versionning, corruption, migration, etc. The actual save also have a version because the game versionning itself is handled at the lower level.

The interresting bit is of course the Checksum field, indicating there is some control at some point. One clue is that the game keeps a Backups folder next to the actual save. Could it be that the save are sometimes corrupted, and the developers added a way to detect it ? I can’t know for sure, but since the whole save is plain text, I would err on the side of not preventing modification and increasing resilience to corruption.

Anyway, if you modify the Checksum field, the game will immediately complain that the save game is corrupted. So it is mandatory that the checksum and “other” stuff have to be in sync.

From this point, it really piqued my curiosity and I wanted to know it was computed.

The game itself

The game is programmed with Unity, which itself uses C# as its programming and scripting language. However, developers have two options when releasing a game: either releasing as plain .Net IL, or compiling to machine code with il2cpp. The later makes reverse engineering much more difficult as you have no IL to work with.

It’s pretty obvious from a simple ILSpy glance that the game is indeed compiled with il2cpp, probably for speed (and security).

I had now two paths: either go to the full rabbit hole and fire up Ghidra, look for signatures of functions etc… or try to be “smart” about the save game. I must admit that my installation of Ghidra wasn’t up to date, and my computer was not really powerful, so the idea of waiting an eternity for a “simple” reverse engineering wasn’t that exiting.

Back to the save game

We can see that there are only 3 top level fields inside the JSON file. I say it’s a JSON file but I do not actually have proof of it, however, it is extremely common to use JSON in Unity and C#.

The 3 fields are:

NameValue
Version1
ChecksumMysterious Value
DataJSON like object

So, from a meta perspective, unless the checksum includes itself (fun fact: when I was creating the Nintendo NES/Famicom Mini, we stumbled upon a NES game that had exactly that: a checksum that includes itself. But that’s a story for another time), the checksum should concern the Data. But… How ? The Data isn’t exactly “defined”. Is it the whole string including Data ? A compact version ? Anything in betwwen ?

Also, we still lack info on the checksum. However, people used to web development or reverse engineering will immediately note that this is very probably a base64 string. Upon decoding, this string yields exactly 20 bytes of binary (no string or anything else). And what does takes up exactly 20 bytes ? A SHA-1 !

Tying a SHA-1 to the Data field

I fired up a LLM and asked it to generate a simple python program that would take the Data field, as is, or compacted, sorted (sorting would remove some randomness in the serialization), and compute the SHA-1 and then base64 it. Alas, no luck… nothing matched. I tried a few more variations but never got the right SHA-1. I was starting for download the update for Ghidra when I had a tought: if I was the game developer, I would probably do a pseudocode like this:

var data = m_SaveData;
string serialized = JSON.Serialize(data);
var sha1 = SHA1.Compute(serialized);
var sha1b64 = base64.encode(sha1);

var WrappedSave = new { Version = 1, Checksum = sha1b64, Data = m_SaveData};

var WrappedSaveString = JSON.Serialize(WrappedSave);

//Write to file..., make backups, etc.

That means that when computing the SHA1, the data has been serialized one step up in the hierarchy. Since the JSON is pretty formatted, is means that two spaces have to be removed to have the “original” value. So I tried to remove two spaces… and voilà! SHA-1 is indeed the right call !

From this point, it’s pretty easy to make an editor/fixer of save games. I will leave this as an exercice for the reader and go back to killing vampires !

Conlusion

Sometimes, it’s useful to put yourself in the boots of the original author of the code, in order to have a “sense” of what would be logical in the implementation. But thruth to be told, I was pretty lucky in this find. Happy hunting !