Custom structures

Malcat can apply custom structures to raw bytes, even when the regular File parsers parser did not identify a structure at that location. This is useful when you are reverse-engineering an unknown format, when a parser missed an embedded structure, or when you want to annotate a buffer with a layout you already know.

Custom structures can be applied from the Apply a custom type dialog in the Hexadecimal and Text views, or from a script with analysis.struct.force.

Available type sources

The custom type dialog shows two kinds of definitions:

  • static types: C-like structure declarations stored as .h files in data/usertypes and <user data dir>/usertypes.

  • dynamic types: Python structure classes found in data/filetypes, data/usertypes, <user data dir>/filetypes and <user data dir>/usertypes.

For most custom layouts, static types are the simplest option. They are parsed once, previewed by the dialog, and can be applied anywhere outside an existing file structure.

Dynamic types are more powerful, because they use Malcat’s Python field classes directly. They are better suited for big-endian fields, formatted values, GUIDs, or layouts which are easier to express with Python. Some dynamic types defined for full file parsers may depend on parser context and can fail when applied as a standalone user type.

Defining static types

To add your own static structures, create a .h file directly inside the usertypes subdirectory of your User data directory. For instance:

<user data dir>/usertypes/myformat.h

Malcat rescans user type files before each analysis. After editing a type file, reanalyze the file or hit Ctrl+R.

Static type declarations use a small C-like syntax. A practical example:

#define MY_NAME_SIZE 16

typedef struct _MY_HEADER {
    uint32 magic;
    uint16 version;
    uint16 flags;
    char   name[MY_NAME_SIZE];
    void  *next;
} MY_HEADER;

typedef struct _MY_ENTRY {
    MY_HEADER header;
    uint64    timestamp;
    uint8     key[32];
} MY_ENTRY;

The exported type names for this file will be usertypes.myformat.MY_HEADER and usertypes.myformat.MY_ENTRY. These are the names shown in the custom type dialog, and the same names should be used from scripts.

Static type files support:

  • integer primitives: char, uint8, int8, wchar, uint16, int16, uint32, int32, uint64 and int64;

  • pointers, whose size follows the current analysis architecture;

  • fixed-size arrays such as uint8 key[32];

  • char and wchar arrays, displayed as strings;

  • nested structures and local typedef aliases;

  • integer constants with #define.

There are a few limits to keep in mind:

  • each .h file is parsed independently, so put the aliases and constants it needs in the same file;

  • only fixed-size arrays are supported;

  • unions are not supported by the static parser;

  • typedef names starting with _ are considered internal and are not shown as user-applicable types;

  • static C-like types are best for simple fixed layouts. Use a Python dynamic type if you need richer field classes or custom parsing logic.

Defining dynamic types

Dynamic user types are Python classes stored directly inside <user data dir>/usertypes. They use the same field classes as Malcat file parsers.

Example:

from filetypes.types import *

class MyTinyHeader(StaticStruct):

    @classmethod
    def parse(cls):
        yield UInt32(name="Magic")
        yield UInt16(name="Version")
        yield UInt16(name="Flags")
        yield Bytes(16, name="Key")

If this file is saved as <user data dir>/usertypes/myformat.py, the dialog will expose the type as usertypes.myformat.MyTinyHeader.

Dynamic types can also be discovered from global structure classes in File parsers parsers. This is convenient when an existing parser already defines the structure you want to apply manually.

Applying a custom type

In the GUI, right-click on a byte outside an identified structure and choose Set user data type. The dialog lets you search for a type, preview it at the selected address, and apply it.

Applied user types are saved in the Malcat project file, so they are restored when the file is reopened. The operation is also integrated with undo/redo.

From Python, use analysis.struct.force with an effective address and a full type name:

address = analysis.v2a(0x401000)
analysis.struct.force(address, "usertypes.myformat.MY_HEADER")

To remove a user type:

analysis.struct.unforce(address)

When running a script outside the GUI, call malcat.Analysis.run() after adding or removing user types so dependent analyses are refreshed. The GUI does this refresh for you when needed.

Troubleshooting

If a type does not appear in the dialog, check that the file is directly below <user data dir>/usertypes and that it has the right extension: .h for static C-like types or .py for dynamic Python types.

If parsing fails, Malcat reports an error while parsing user types during analysis. Start with a minimal structure, re-run the analysis, and add fields back gradually. For static types, the most common causes are a missing local typedef, a non-constant array size, or an unsupported union.

If a dynamic type previews with an error, it may depend on parser state that is only available during a full File parsers parse. In that case, create a smaller standalone StaticStruct in <user data dir>/usertypes for manual application.