Cover art for Reverse Engineering File Formats with ImHex

Reverse Engineering File Formats with ImHex

Learn how to analyze custom binary files by decompiling game code and writing declarative pattern definitions in ImHex.

21 cards · 3 min · tap to begin

From · · · 3 min

Reverse Engineering File Formats with ImHex

Learn how to analyze custom binary files by decompiling game code and writing declarative pattern definitions in ImHex.

In brief

Learn how to analyze custom binary files by decompiling game code and writing declarative pattern definitions in ImHex. Reverse engineering unknown binary files relies on finding the parsing logic in decompiled code and incrementally building declarative pattern definitions to decode every byte. Originally reported by…

Reverse engineering custom binary files

Demystifying raw binary formats requires pairing decompiled program logic with declarative pattern definitions. The save format of the game FEZ serves as an ideal subject.

We’ll go from a completely custom binary save file for the game FEZ to a full definition written in the Pattern Language...

Inspecting raw uncompressed bytes

Opening a file in a hex editor like ImHex reveals immediate clues. Readable ASCII strings and repeating byte patterns indicate whether data is compressed, encrypted, or missing magic headers.

The file seems to be uncompressed and unencrypted, as seen by the plain-text strings and other patterns in the file...

Decompiling the application binary

Managed languages like C# (.NET) decompile back to clean source code using tools like JetBrains Rider. Navigating assembly references quickly locates file I/O handlers.

The game is written in the C# programming language, which is generally really easy to reverse engineer.

Locating the serialization logic

Find usages in JetBrains Rider

Searching for save file naming strings leads directly to the core write methods. In FEZ, PCSaveDevice.cs uses BinaryWriter to serialize game data into byte streams.

In the constructor of that class, we can also immediately see string str = "SaveSlot" + (object) index;...

Starting the ImHex pattern

Pattern files map binary structures declaratively onto raw offset addresses. Defining a top-level struct with the @ operator anchors data decoding at offset zero.

We can start simply by creating a struct FezSaveFile and placing it at the start of the file using the @ placement operator.

Decoding standard Windows timestamps

Highlighted Bytes in the Hex Editor and decoded value in the Pattern Data View

Binary headers frequently embed standard system timestamps. Using ImHex's imported standard time library parses 64-bit Windows FILETIME numbers into readable dates.

This simple change now turns that unreadable number from before into a nice, human readable representation of the actual time value...

Enforcing fixed buffer bounds

Many binary writers enforce fixed byte sizes and zero-fill remaining space. Attaching [[fixed_size]] attributes documents expected memory bounds directly in the pattern code.

This maps incredibly well to the [[fixed_size(0xA000)]] attribute that can be attached to FezSaveFile to ensure that.

Mapping primitive struct fields

Sequential scalar types in C# serializers map directly to primitive types in pattern code. Version integers, timestamps, and boolean flags form the foundational layout.

Here we can see aaaaaaalll the different fields that are being written out to the binary.

Validating versions with assertions

Inline pattern logic allows format verification during parsing. Adding std::assert guarantees that incompatible save versions trigger clean errors before parsing corrupts.

...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.

Modeling optional nullable objects

The final Object<String> type being parsed by ImHex

Custom serializers often write a boolean presence flag prior to optional values. Generic template structs evaluate these flags dynamically to conditionally parse payload fields.

First, a bool is written to the file that represents whether or not the object is null.

Decoding variable integer encodings

.NET string lengths use 7-bit encoded integers where the most significant bit signals whether additional bytes follow. Reading loop patterns decode these compressed sizes.

...all this does is use the MSB of each byte as a flag to tell the parser if there’s another byte still coming.

Transforming custom bitwise data

ImHex attributes [[format]] and [[transform]] execute custom bitwise shift algorithms during parsing, presenting clean mathematical values in UI tree views.

Additionally, to make this type a bit easier to work with, we can use the [[format]] attribute to display the decoded integer value...

Parsing dynamic string structures

Combining 7-bit length decoding with dynamic character array fields creates reusable string components that mirror native framework string writers.

Now that all of this is done, we can finally define our String type.

Expressing generic list collections

The final List< type being parsed by ImHex

Serialized dictionaries write an integer count followed by paired items. Generic template definitions easily handle repetitive key-value parsing across entire files.

All of this together now lets us finally decode the list.

Replacing integers with enums

Enumeration types substitute raw integer codes with human-readable textual labels in hex editors, making parsed binary properties instantly recognizable.

We could just treat it as an int like the serializer code does, but it would be nicer to keep the names available in ImHex as well.

Handling nested complex structs

Hierarchical game data translates into nested pattern structs. Serializers invoking sub-functions map cleanly to distinct sub-struct definitions.

This maps really nicely to a new struct that we can call LevelSaveData and just keep going in there as before...

Modifying decoded binary data

Fully decoded Save File in ImHex

Completing a pattern highlights every byte in the hex viewer. Users can visually navigate nested tree structures and edit binary values directly in place.

You can now browse through the Pattern Data View and inspect what all these different values mean and even modify them by double-clicking the value!

Step 1: Detect known formats

Begin reverse engineering by checking magic bytes and signatures. Tools like binwalk and ImHex auto-detection identify standardized compression or container wrappers.

This can be done in various ways, ImHex magic detection and tools like binwalk can help a lot.

Step 2: Locate target program logic

Use decompilers tailored to the binary language, Rider for .NET, Ghidra or Binary Ninja for native executables, and search for file I/O strings to locate serialization code.

Rider works great for .NET, Ghidra, IDA or Binary Ninja for native-compiled programs, Recaf for JVM languages.

Step 3: Identify structural primitives

Map high-level language constructs to raw byte representations by cataloging how the target program writes primitives, strings, arrays, and flags.

Identifying them is the first step to understanding the file step by step

Step 4: Build declarative patterns

Write pattern files iteratively to verify assumptions and document binary schemas. Patterns serve as live documentation and interactive parsers for binary reverse engineering.

Patterns are great not only for decoding the file once you know how it works but also for documenting and verifying your findings along the way.

The headline

Reverse engineering unknown binary files relies on finding the parsing logic in decompiled code and incrementally building declarative pattern definitions to decode every byte.

Read the original on WerWolv

React

Sign in to react and comment.

Comments (0)

Life is short. Keep it sweet. Respect others' opinions and be kind!

    Recommended next

    More decks on reverse engineering and related topics.