Signatures are dead on mutating binaries
Some games ship every build through a code mutation engine. Not obfuscation of a few hot functions: the entire image is rewritten, so that no instruction sequence from build N exists in build N+1. I measured it once, properly. A 24-byte sequence taken from the old .text, one I had used as a signature for a year, had zero hits in the new image. Not a near miss, not a wildcard problem. Zero.
That kills the whole toolbox at once. Disassembler signatures, RIP-relative resolution from a known instruction, YARA rules, all of it is worth exactly nothing on such a title. So the question is what does survive a rebuild, and the answer is: the data.
1. .data keeps its layout and only shifts
The mutation engine rewrites code. It does not reorder the globals, because the code it rewrites still has to find them. So the layout of .data is stable between builds, and what changes is one offset.
Dump both full images. On a driver-backed reader that takes about a second for 311 MiB. Then find a block of constants that sits next to the global you want, and align the two dumps on that block. In my case the whole section had moved by exactly -0x36FC0. Apply that to the old address and the world global was correct on the first try. The pointer chain behind it had not changed at all.
Two details matter. The section delta from the PE header is not enough: the content of .data moves differently from the section’s RVA, so you align on content, not on headers. And the block you align on must be recognisable by its values, so pick something with structure, a table of floats or a run of small integers, not a stretch of zeros.
2. Class offsets from the object graph, not from names
The second problem after a rebuild is that field offsets inside classes may have moved, and on this title the name pool is encrypted, so you cannot ask an object what it is.
You can, however, ask objects how they point at each other. Cache the first couple of kilobytes of every actor, about 1150 of them in 40 seconds through the driver, and search for mutual pointer pairs: an object A that holds a pointer to B at offset x, where B holds a pointer back to A at offset y.
A + 0x2B0 -> B
B + 0x2F8 -> A
found 12 pairs with the same (x, y)
Twelve pairs with identical offsets name two classes and two fields at once, without a single string: the pawn holds its player state, the player state holds its pawn. The one pawn that additionally points back to a controller is the local player. The actor that holds a pointer to that controller is the camera manager. Every class in the chain falls out of the graph’s shape.
The built-in ring scan in my tooling does the same search, but it needs the offsets it is searching for. After a patch that moved exactly those, it finds nothing. It is a verification tool, not a discovery tool, and confusing the two is how you lose a day.
3. Inherited offsets do not shift uniformly
Once the graph gave me the new offsets, I checked them against the old ones expecting one delta per hierarchy. There was no such thing:
class shift
AActor -0x18
APawn -0x10
APlayerState -0x10
USceneComponent -0x20
APlayerCameraManager -0x38
attribute set 0
Fields in a derived class move by whatever the base classes above it gained or lost, and different bases changed by different amounts. Extrapolating from one measured class to its siblings would have produced offsets that read plausible garbage. Each class gets measured on its own.
What the toolbox looks like now
For a title that mutates, the workflow after every patch is: dump, align .data on a constant block, take the globals from the alignment, rebuild the class offsets from the pointer graph, verify with the ring scan, and only then touch code. Nothing in that list reads an instruction.
The general point is older than mutation engines. Code is what the compiler and the protector control. Data is what the program needs to stay the same in order to keep working. When you need something stable, look where the program itself needs stability.