Zircon

Unreal Engine 4.22 – 5.7 · Unity IL2CPP and Mono · Windows x64

The whole type system,
out of the running game.

Point it at an Unreal game — running, crashed, or just sitting on disk — and get back every class, struct, enum, property offset, function signature and Blueprint script. Then render it as a C++ SDK, a .usmap, types for IDA, Ghidra or Binary Ninja, Frida bindings, Python stubs, or a report on what a patch just broke.

It does the same for Unity IL2CPP games: every C# type, field offset, method RVA and enum value, read out of the running game rather than by parsing global-metadata.dat — which is why encrypted metadata stops mattering, and why a new Unity release does not break it.

Since 0.10.0 it also hands you the addresses a mod starts from — GWorld, ProcessEvent and AppendString — read out of the game's memory and code from outside, no injection. On Escape the Backrooms all three match Dumper-7 exactly, and zircon fingerprint prints them in about a second.

Since 0.9.0 it does the same for Unity Mono, the other backend, where the game ships its code as real .NET assemblies. Field offsets come from the runtime because the CLI does not store them; enum values and IL offsets come from the assemblies because a const has no storage for the runtime to read. Both halves, merged, with neither number invented.

Since 0.7.0 it reads that metadata file too, and --mode dual takes both at once. The runtime knows what is true at execution — offsets, method addresses, the concrete generics that only exist once something has run. The file knows the exact type set, does not move between two reads, and answers for targets that will not run here at all: an APK, a console build, a game with anti-cheat, a game that crashes the moment the walk touches it. Each record says which side it came from, and every disagreement between them is recorded rather than quietly resolved.

That file is read without a version table as well. Its layout is worked out from constraints it cannot satisfy by accident — the spans tile the file, a record table's leading fields all land on a name, a start/count pair partitions the table it indexes. Across metadata versions 24 through 39, ten of eleven installed games solve completely, and the eleventh refuses with its reason instead of guessing.

There is no engine-version table anywhere in it. Every offset is worked out at runtime from something structural to the engine, which is why it holds up on licensee forks, renamed executables, and engine versions that did not exist when it was written. Both Unity paths hold to the same rule.

Windows x64 · 4.2 MB · no runtime to install

zircon — one command, everything
> zircon dump --pid 25244 --script --defaults -o game.json --emit all
[info ] object array at 0x7ff7625a70d0: 72561 objects, index offset +0xc
[info ] name pool blocks at 0x7ff7624c3590 (confidence 0.95)
[info ] UObject layout: index +0xc, class +0x10, name +0x18, outer +0x20
[info ] UClass layout: CDO +0x110 (300/300 named Default__*)
[info ] UFunction layout: next +0x28, flags +0xb0, func +0xd8
 
packages 618 properties 48692
classes 5252 functions 17698
 
cpp_sdk 622 file(s) binja 1 file(s)
usmap 1 file(s) frida_js 1 file(s)
docs 635 file(s) python_stubs 620 file(s)
… 13 s, all twelve formats
16games verified, UE 4.22 to 5.7
11output formats, plus a plugin ABI
4ways to attach, including offline
0hardcoded offsets, any version

Capability

What it does that the alternatives don't

Dumper-7 and UEDumper are mature, widely used and good at what they do — if all you want is a C++ SDK from an injected DLL, they are well-trodden paths. This is a comparison of what the tools set out to cover, not a scoreboard.

CapabilityDumper-7UEDumperZircon
How it attaches injected DLL external injected, external, minidump, or a PE on diskonly
Analyse a crash offline, with no game running — — yes — dump once, analyse foreveronly
Primary output C++ SDK C++ SDK + live viewer 11 formats + plugins
Blueprint bytecode decompiler — — ~90 Kismet opcodesonly
Build-to-build diffing — — 27 change kinds, CI exit codeonly
Find objects by what they hold — — --where Health<50only
Reverse reference index — — xref -f Actoronly
Reads CDO default values — — into the SDK, docs and diffonly
Live object browser — yes standalone and in-process
Edit values in a running game — yes yes, opt-in, with value freezing
Callable function wrappers in the SDK yes — yes, ProcessEvent slot derived — no setup
Scriptable / extensible — — C plugin ABI + vendored Luaonly
Checks its own output for contradictions — — validate --strictonly
Behaviour when it is unsure — — refuses rather than guessing
That last row is the one that shapes everything else. An unresolvable type becomes an opaque byte array of exactly the right size. An unknown bytecode opcode stops the walk and says where. An ambiguous match is declined. A missing name you can see — a member at the wrong offset you cannot, and it corrupts everything after it.

The parts worth showing

Four things nothing else does

Each of these came out of needing it, not out of a feature list.

It reads Blueprints back into something you can follow

Roughly ninety Kismet opcodes, with names, properties and called functions resolved through the same reflection data as everything else. Every line carries its bytecode offset, so you can cross-reference a raw disassembly.

100% of 2.1 MB decoded on the largest target tested. When an opcode isn't recognised the walk stops and says where — a misaligned decode produces confident nonsense, which is worse than stopping.

zircon scriptGUI Script tabinto the JSON dump
The Script tab showing decompiled Kismet bytecode with bytecode offsets
Decompiled Blueprint graph, offsets in the left column

It tells you exactly what an update broke

Dump before the patch, dump after, and the diff reports every class that changed size, every property that moved and every function that vanished — graded by how badly it breaks code you already wrote.

It exits with code 8 when something breaking changed, so it drops straight into a build script. Twenty-seven change kinds, including a property whose default value moved — a patch that shifts nothing and changes how the game behaves no longer produces an empty report.

zircon diff--breaking--style json|markdown
Terminal showing the emitter list and a diff reporting no differences
Every format, and a clean diff across two builds

It shows you the game while it runs — and lets you change it

Enums resolve to their names rather than integers, object properties to their paths, structs to their members, and unreadable memory says so instead of showing zeroes. Select a class and you get its designer-set defaults; select a live instance and you get what it holds right now.

Tick Allow edits and the value column becomes editable. Seven bools commonly share one byte, so setting one reads the byte, changes that bit and writes it back — the other six survive. Values the game owns can be frozen, re-applied every 30 ms, which beats a 60 Hz tick.

standaloneor injectedzircon write--where Health<50
The browser showing CharacterMovementComponent properties with live values
Live values, named types, real defaults

It finds the game, and works out the layout by itself

You don't name an engine version and you don't pick your game from a list of supported titles. Every running process is scored on what is actually inside it, and the layout is derived from memory.

Every offset comes with a confidence value and the evidence behind it, and every conclusion lands in the dump header — so a member that looks wrong can be checked against the reasoning that produced it, rather than trusted because the file parsed.

On a running game fingerprint also lists GObjects, FNamePool, GWorld, ProcessEvent with its vtable slot, and AppendString — everything a mod menu's offsets file needs, from one command.

zircon detectzircon fingerprintevidence in the headerGWorld · ProcessEvent · AppendString
Terminal showing zircon detect scoring processes and fingerprint reporting the engine version
Scored on contents, not matched against a list

One dump, twelve renderings

Everything downstream is the same operation

The reflection data is read once into a versioned JSON intermediate representation. Every output is a pure function of that file, which is why adding a format never touches the code that reads memory — and why you can write your own against dump.json in any language.

cpp_sdkHeaders with every class, correct offsets, compile-time checks, and a callable wrapper per reflected function
csharpA C# source tree for a Unity game, either backend — one folder per assembly, one file per type, offsets and RVAs in the margin
usmapUE4SS / FModel mappings
idaA Python script importing every struct into your IDA database
ghidraThe same, for Ghidra
binjaThe same again, as a Binary Ninja type import
reclassA ReClass.NET project
frida_jsA Frida module with real property accessors — read and write a live game from JavaScript
python_stubs.pyi stubs so your editor completes the game's own classes
docsBrowsable Markdown API reference with an index
graphsInheritance graphs in DOT and Mermaid
jsonThe raw IR, plus a C plugin ABI and vendored Lua for your own
… yoursA plugin is a DLL exporting one function. The shortest useful emitter is a Lua file you drop in a folder
zircon emit all game.json -o out/
zircon emit cpp_sdk,usmap,frida_js game.json -o out/

And out of your machine

A dump is more useful where other people can read it

Zdex indexes Zircon dumps and makes them browsable, searchable and diffable in a browser. zircon publish puts one there without leaving the terminal. Entirely optional — nothing leaves your machine unless you ask it to.

zircon login                                       paste an API key, once
zircon publish game.json --label "1.4.2 (Steam)"   gzip, upload, index, URL

It is a key, not an account

No browser flow, no OAuth, no sign-in window. The key lives in one file and zircon logout deletes it. It never reaches a dump, a log line, or anything in your project.

Survives a bad connection

Chunked and resumable: kill it halfway and the next run carries on from what the server already has. A 59 MB dump compresses to 3.4 MB — 17×, with a DEFLATE encoder written for this and checked byte-for-byte against zlib.

Or from the browser window

Publish… sits next to Dump… in the GUI. It fills in the file and the game for you, shows the same progress, and ends on a link. Same key, same config file.

zircon fetch 42 --usmap pulls a published dump's mappings back down, and --sdk its generated headers.

How it is checked

Against something other than itself

A tool agreeing with itself proves very little. These are the checks that involve a second opinion — a different implementation, a different way of reading memory, or a compiler.

Cross-checked against Dumper-7

Both tools were pointed at the same UE 5.6 game and their SDKs parsed for the offset every member carries. Of the 26,625 members present in both, all 26,625 agree exactly. Two independently written dumpers, no disagreement anywhere — which is evidence for Dumper-7 every bit as much as for this.

Three ways of reading, one answer

The same game read from outside the process, from inside it, and from a 7 GB full-memory minidump gives 10,850 of 10,850 types byte-identical, three ways. Two real bugs were found by that check disagreeing with itself — bugs no amount of “the SDK compiles” would catch.

Every offset is a compiler assertion

The generated SDK carries a static_assert on every member. 56,396 of them on one game, compiling clean with windows.h included first, as any injected DLL would. A wrong offset upstream is a build error here, not a silent misread at runtime.

And the tool audits its own output. zircon validate --strict reads a finished dump back and checks it agrees with itself — members that overlap, offsets past the end of their class, two bools claiming the same bit, an enum too narrow for its own values. Pointed at a shipped game for the first time it found two real defects that all three ways of reading memory had been reproducing identically, so no amount of cross-checking them would ever have caught it.

Why it survives a patch

Nothing is keyed off a version number

Every offset is derived from a property that is structural to the engine rather than incidental to a build. This is the whole design, and it is why the tool works on games patched yesterday and on engine versions that did not exist when it was written.

FactHow it is established
GObjectsthe object in slot i stores i in its own InternalIndex — nothing else in the address space does that across a whole array
the name poolblock 0 begins with None, always the first name interned
ClassPrivate vs OuterPrivatefollow each repeatedly: class converges to a fixed point, outer terminates at null
Offset_Internala struct's own properties must tile it — distinct offsets, ending inside it
the class default objectit points at an object whose class is this class, named Default__*
what an object iswalk its meta-class chain, never its class name — a Blueprint class reports BlueprintGeneratedClass
PropertiesSizethe root class reports the size of a UObject, and nothing that derives from it is smaller
UField::Nextit lands on sizeof(UObject), and its chains have to link — not merely terminate
The rule this was learned the hard way. A field that scores perfectly on a weak constraint is not the right field. Every derivation needs a positive distinguishing property and an independent anchor whose correct value is known ahead of time. Internal consistency alone was wrong every single time it was the only test — eight times, each from real game data.
When a fork changes the engine itself. Most licensee builds rename things. One shipped game appends to UObject and carries a UE5 structure inside a 4.27 UStruct, so sizeof(UObject) is 48 where its members imply 40. Anything anchored on “OuterPrivate is the last member” is simply wrong there. The rules above ask nothing about where UObject ends, and a build that extends it is reported rather than smoothed over.

Get it

Three binaries, no installer

zircon.exe does everything from outside a game, zircon-gui.exe is the live browser, and zircon.dll is the injectable payload. You almost certainly only need the first — injection buys exactly one capability, the ability to call the game's own functions.

4.2 MB · the three binaries, the Lua plugin host and the docs · MSVC runtime linked statically, so it runs on a clean Windows install

Installer, if you would rather not keep a folder: same files into Program Files\Zircon, and it can put zircon on your PATH. The zip needs no installing at all and is the one to take if you are unsure.

Or build it

Visual Studio 2022 and CMake. Nothing to install — Dear ImGui and Lua are vendored in the repo.

cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release

Then point it at a game

Find it, check the layout derives, dump it, render it. Four commands, and the last two collapse into one.

zircon detect
zircon fingerprint --pid 12345
zircon dump --pid 12345 --script --defaults \
       -o game.json --emit cpp_sdk,usmap

Prefer clicking?

zircon-gui.exe does all of it from one dialog — attach, browse, and dump any combination of formats on a background thread.

Full walkthrough →

What it will not do. Anti-cheat evasion — zircon inject refuses outright when the target has anti-cheat loaded. Patching game code: it edits reflected property values, which is data the engine already describes and changes itself, but it never writes over instructions, installs hooks or patches functions. The injected browser opens its own window rather than overlaying the game, because an overlay means writing a trampoline over code the game owns. And non-Unreal engines.