Skip to content
HN On Hacker News ↗

Reverse Engineering Unknown File Formats with ImHex | WerWolv

▲ 255 points 51 comments by carlos-menezes 5d ago HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,643
PEAK AI % 0% · §1
Analyzed
Sep 2
backend: pangram/v3.3
Segments scanned
1 windows
avg 1643 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,643 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

Introduction Over the years I’ve been asked the same question countless times: Person on Discord asking for help reverse engineering a file format I usually couldn’t really give them a good answer except, “Look at the decompiled code of whatever program reads/writes these files and work backwards from there.” This post is meant to change that. We’ll go from a completely custom binary save file for the game FEZ to a full definition written in the Pattern Language, which is part of ImHex, the hex editor I’ve been developing for the past few years. It is free, open source and available on any operating system (or even through the browser if you prefer that: ImHex Web). ImHex VersionAt the time of writing, some features used here are not in a release yet but only available in the Nightly build (that can also be downloaded above from the same link). If you’re on ImHex v1.38.1 or below and experiencing issues, consider upgrading to the Nightly build Getting Started Spoiler WarningFEZ was released all the way back in 2012. Still, if you haven’t played it yet and want to get the full experience, I highly recommend playing it before you continue reading. Some of the code shown here will contain heavy spoilers for secrets and endgame content that might ruin your experience. You have been warned. The first thing we need is the save file. I downloaded the game from Steam (the latest full release currently available, released 2. December 2016), started it and played for a little bit until it saved. Then I went looking through my filesystem and found the save file under /home/werwolv/.local/share/FEZ/SaveSlot2. On Windows, it will be elsewhere. Opening the file in ImHex shows this: Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F00000000 3E 74 E1 41 6B BA DA 01 06 00 00 00 00 00 00 00 >t.Ak...........00000010 3A AC 78 49 C9 B4 CF 01 01 00 01 00 00 01 12 00 :.xI............00000020 00 00 01 11 44 4F 54 5F 4C 4F 43 4B 45 44 5F 44 ....DOT_LOCKED_D00000030 4F 4F 52 5F 41 00 01 10 44 4F 54 5F 4E 55 54 5F OOR_A...DOT_NUT_00000040 4E 5F 42 4F 4C 54 5F 41 00 01 0B 44 4F 54 5F 50 N_BOLT_A...DOT_P00000050 49 56 4F 54 5F 41 01 01 11 44 4F 54 5F 54 49 4D IVOT_A...DOT_TIM00000060 45 5F 53 57 49 54 43 48 5F 41 00 01 0F 44 4F 54 E_SWITCH_A...DOT00000070 5F 54 4F 4D 42 53 54 4F 4E 45 5F 41 01 01 0C 44 _TOMBSTONE_A...D00000080 4F 54 5F 54 52 45 41 53 55 52 45 00 01 0B 44 4F OT_TREASURE...DO00000090 54 5F 56 41 4C 56 45 5F 41 01 01 13 44 4F 54 5F T_VALVE_A...DOT_ This already reveals a few things. The file seems to be uncompressed and unencrypted, as seen by the plain-text strings and other patterns in the file that can be easily spotted by just looking at the bytes and characters. The data also doesn’t have a file magic (some readable text at the start of the file to make it more easily identifiable), and it doesn’t look like anything standard, as ImHex can’t identify its type directly either. Magic file information from ImHex Without any more information, we’re basically stuck here. The data can mean anything, and only the program generating and parsing it can make sense of it. Decompiling the Game Finding the right files Clicking on the gear icon on the Steam page and selecting Manage -> Browse local files brings us to the game’s binary location. What immediately sticks out are files like System.Core.dll or mscorlib.dll. The game is written in the C# programming language, which is generally really easy to reverse engineer. Tools like JetBrains Rider can decompile the binaries back to what looks like the original source code. For that, we can open the game’s folder as a project and then simply Right Click -> View in Assembly Explorer for all the .dll files that look interesting. To me, particularly interesting were FEZ.exe, FezEngine.dll, Common.dll, ContentSerialization.dll and EasyStorage.dll. The rest are system libraries or external dependencies that look unrelated to what we’re trying to do here. Finding the right functions Just clicking through the namespaces quickly reveals an interesting-looking file: EasyStorage -> PCSaveDevice. In the constructor of that class, we can also immediately see string str = "SaveSlot" + (object) index;, which looks like it’s building the name of our file, SaveSlot2, so we found the right place for sure. Scrolling down a bit, we can find a function called Save that creates a byte buffer and starts filling it in using a BinaryWriter stream before saving it to our save file location. Bingo! public virtual bool Save(string fileName, SaveAction saveAction){ // ... byte[] buffer = new byte[40960 /*0xA000*/]; using (MemoryStream output = new MemoryStream(buffer)) { using (BinaryWriter writer = new BinaryWriter((Stream) output)) { writer.Write(DateTime.Now.ToFileTime()); saveAction(writer); if (output.Length < 40960L /*0xA000*/) { long length = 40960L /*0xA000*/ - output.Length; writer.Write(new byte[length]); } else if (output.Length > 40960L /*0xA000*/) throw new InvalidOperationException( "Save file greater than the imposed limit!" ); } } // ...} Writing the ImHex Pattern Humble Beginnings Now that we’ve found where the save file is being generated, we can start writing a Pattern file in ImHex to decode the data. Open the Pattern Editor tab to reveal a text editor where we can write our source code. We can start simply by creating a struct FezSaveFile and placing it at the start of the file using the @ placement operator. struct FezSaveFile { // Struct Definition};FezSaveFile saveFile @ 0x00; This instantiates the FezSaveFile pattern object at address 0x00 of our file. Next, in the save file generation code, we see writer.Write(DateTime.Now.ToFileTime());, which writes the current timestamp as a Windows File Time to the output. As seen in the Remarks section of the docs, this is simply a little endian, 64-bit value (a long in C#) that represents the number of 100 ns intervals that have passed since the year of our lord 1601 A.D. We could, of course, properly decode this value and everything but to get started we can simply place a s64 in its place in the Pattern to read it. Alternatively, we can also write type aliases with the using keyword to make the code in our pattern resemble the types used in the real code even more closely. These simply define a new type that has the exact properties of the type on the right hand side but with a potentially more descriptive name. using int = s32;using long = s64;struct FezSaveFile { long fileTime;};FezSaveFile saveFile @ 0x00; After clicking the button at the bottom of the Pattern Editor (or pressing the F5 key), the region of that value is now highlighted in the Hex Editor View, and it also appears in the pattern tree in the Pattern Data View. Highlighted Bytes in the Hex Editor and decoded value in the Pattern Data View For this particular case though, we’re in luck and the standard library already implements a type for decoding a Windows FILETIME value. To get access to it, we can import the type.time library which defines that type and then use it like any other type in our code: import type.time;struct FezSaveFile { type::FILETIME fileTime;};FezSaveFile saveFile @ 0x00; This simple change now turns that unreadable number from before into a nice, human readable representation of the actual time value: Decoding the FILETIME value using the `type::FILETIME` type from the standard library And that’s it for the start, congrats! You wrote your first pattern! [[fixed_size]] attributeOne thing we can also see in the code is that the Save() function ensures that the save file is always 0xA000 bytes long. If it’s shorter, it will pad it out with zeros, and if it is longer, an InvalidOperationException will be thrown.This maps incredibly well to the [[fixed_size(0xA000)]] attribute that can be attached to FezSaveFile to ensure that. This is entirely optional but helps document the official behavior. The Actual Save Data Back to the C# code, the next thing that’s done is to call out to the saveAction callback, which is implemented elsewhere. Thankfully, Rider helps here, as you can just Ctrl-click on the name of the Save function to find definitions. There we see a few places it’s called from, but the interesting one is in GameStateManager.cs SaveInternal(). Find usages in JetBrains Rider private void SaveInternal(bool ngpBackup){ // ... this.ActiveSaveDevice.Save( "SaveSlot" + (object) this.SaveSlot, new SaveAction(this.DoSave) ); // ...} There we can see that the actual dumping of the save data is delegated to the DoSave() function, which calls SaveFileOperations.Write(). This is the juicy stuff now. Here we can see aaaaaaalll the different fields that are being written out to the binary. public static void Write(CrcWriter w, SaveData sd){ w.Write(6L); w.Write(sd.CreationTime); w.Write(sd.Finished32); w.Write(sd.Finished64); w.Write(sd.HasFPView); w.Write(sd.HasStereo3D); w.Write(sd.CanNewGamePlus); w.Write(sd.IsNewGamePlus); // ...} Looking at the types of those values allows them to be easily converted to the ImHex Pattern: struct FezSaveFile { // From PCSaveDevice.cs type::FILETIME fileTime; // From SaveFileOperations.cs long version; // Checked in the `Read()` function to be 6 long creationTime; bool finished32; bool finished64; bool hasFpView; bool hasStereo3d; bool canNewGamePlus; bool isNewGamePlus;}; The first field seems to be a save file version as can be seen in the Read() function below which reads that field, makes sure it is also 6 and throws an exception if it’s not. We can simply parse that field but if we want to be extra fancy and make sure that we only load files that are actually compatible with our pattern, we can easily assert on this field. In the Pattern Language, we can have conditions and function calls intertwined with our type definitions which makes things like this possible: