# Reverse Engineering File Formats with ImHex

> Learn how to analyze custom binary files by decompiling game code and writing declarative pattern definitions in ImHex.
- Title: Reverse Engineering File Formats with ImHex · WerWolv
- Summary: Learn how to analyze custom binary files by decompiling game code and writing declarative pattern definitions in ImHex. Reverse engineering unknown binary…
- Keywords: reverse engineering, imhex, hex editor, binary formats, decompilation, technology, startups, Reverse, Engineering, File, Formats, WerWolv
- Source: WerWolv — https://werwolv.net/posts/file_format_reverse_engineering
- Author: WerWolv
- Read time: 3 min
- Topics: reverse engineering, imhex, hex editor, binary formats, decompilation, technology, startups
## 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
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
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
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
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
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.
## Key takeaway

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