When to use / When not to use
- Use: game logic reverse engineering (Unity mono/IL2CPP, Unreal); memory search and modification (CE value/pointer scan); function location and modification ([[re-patching]]); anti-cheat (EAC/BE) detection evasion (within an analysis environment)
- Don't use: pure static reading of Unity mono logic (just use [[re-dotnet]] decompilation
Assembly-CSharp.dll, no need for this skill) - Don't use: mobile games ([[re-mobile]] / [[re-apk]] / [[re-ios]] / [[re-frida]])
- Don't use: if you only want a finished trainer/cheat—this skill is an analysis path, it does not produce distribution tools, online multiplayer anti-cheat risk is your own
- Don't use: V8 bytecode .jsc of Electron apps (use [[re-electron]])
Tool preparation
Anti-cheat game analysis defaults to sandbox + isolation ([[re-analyze/platform-tips]] highest principle; [[re-sandbox]] mandatory prerequisite); dynamic debugging branches by OS.
Cheat Engine(CE,Windows)
- Install: download and install from the official site cheatengine.org (
choco install cheatengineoptional) - Verify: open CE, be able to attach to any process and perform a value scan (First Scan has results)
Unity tools
- Il2CppDumper (IL2CPP restoration): GitHub
Perfare/Il2CppDumperreleases(Windows exe / dotnet tool) - mono scenario: no extra tools,
Game_Data/Managed/*.dlldirectly hand over to [[re-dotnet]] (dnSpy/ILSpy) - Verify: Il2CppDumper against the target
global-metadata.datcan producedump.cs(class/method signatures) andscript.json(function addresses)
Unreal tools (UnrealFinder approach)
- UnrealFinder(GitHub
povlhp/UnrealFinderor similarDumper-7): locateGObjects/GNamesand other global symbols, export the UObject class tree and function addresses - Verify: against the target UE game exe, can list
GWorld/GNamesaddresses and export the object list
Debugger (by OS)
- Windows: [[re-x64dbg]] (lightweight breakpoint debugging) or [[re-windbg]] (exception analysis)
- Linux: [[re-gdb]] (when running Windows games under Wine, read the Wine process memory, see the [[re-analyze/platform-tips]] Linux branch Wine direct-read approach)
Operation steps
Execute in order, archive each step (engine identification evidence, address and offset records, sha256 of dump artifacts, [[re-triage]] evidence preservation).
-
Runtime/engine identification:
- Directory features:
Game_Data/Managed/Assembly-CSharp.dll→ Unity mono;Game_Data/il2cpp_data/+global-metadata.dat→ Unity IL2CPP - Unreal features:
Engine/Binaries/、*.pak、<游戏名>-Win64-Shipping.exe; search in strings forGWorld/GNames - Other/self-developed engines → regular [[re-binary-core]] process, do not use this skill
- Archive: engine type + version (determines later tool selection)
- Directory features:
-
Unity mono vs IL2CPP export:
- mono:
Game_Data/Managed/*.dlldirectly decompile ([[re-dotnet]], opening Assembly-CSharp.dll in dnSpy/ILSpy gives all C# logic, no need for the dynamic part of this skill) - IL2CPP (no managed DLL): Il2CppDumper export:
Il2CppDumper.exe <Game.exe> <Game_Data>/il2cpp_data/Metadata/global-metadata.dat → dump.cs(类/方法/字段签名) + script.json(方法地址) + il2cpp.h(结构体)Use
dump.csto locate the target method name,script.jsonget the corresponding function address → debug/patch at the corresponding address in the exe ([[re-x64dbg]] / [[re-windbg]])- Archive: dump.cs/script.json + target method address table
- mono:
-
Memory Search (CE):
- Value Scan: attach to process → enter current value
First Scan→ change the value in-game →Next Scannarrow down the address list - Pointer Scan (addresses change under ASLR): for a located address
What accesses this address→ record the accessing instruction offset →Pointer scangenerate a pointer chain (save.ptfile), still usable after restart - Multi-level pointers: struct base address in the form
Game.exe+0x2A3F4C → +0x10 → +0x28—offsets are determined by the engine object structure (for Unity, look at MonoBehaviour field order; for Unreal, look at UPROPERTY offsets) - Modification verification: freeze the value via memory editing or a CE Lua script, observe behavioral changes in-game
- Save: address expressions (module+offset), pointer chain files, scan parameters
- Value Scan: attach to process → enter current value
-
Function Location and Modification:
- Code↔Memory: for a key address
Find out what writes to this address→ breakpoint hit writes the code location → note the in-module offset → analyze that function at the corresponding address in a decompiler ([[re-ghidra]] / [[re-binaryninja]]) - Or from
dump.cs/script.jsondirectly get the IL2CPP function address → decompile → modify ([[re-patching]]: change jumps/parameters/return values) - Test after modification: run in a sandbox to verify the function takes effect ([[re-sandbox]]), record behavioral differences before and after modification
- Code↔Memory: for a key address
-
Anti-Cheat Countermeasures (detection evasion, in sandbox):
- Cognition: EAC/BattleEye detect debugger presence, memory modification traces, CE/cheat traces, multi-instance/virtualization characteristics—the analysis environment is completely isolated from the real online environment (dedicated VM + snapshot, [[re-sandbox]] + [[re-analyze/platform-tips]] top principle)
- Methods: first statically analyze detection points ([[re-anti-analysis]] anti-debugging approach), hook/bypass detection APIs (NtQuerySystemInformation etc.), minimize modifications (restore after use), avoid long-term persistence traces
- Cleanup: restore snapshot after analysis; do not use for online matches
Game Script Engines (Lua / LuaJIT)
Game logic is often carried by the script layer (character behavior, skill values, level configuration). When the analysis target shifts from the engine C++ side to script bytecode: first identify the script VM and exact version, then follow the bytecode parsing path for that version.
Bytecode Identification (header signature / version)
- Standard Lua bytecode header:
0x1B 4C 75 61(\x1bLua) + version byte—5.0=0x50, 5.1=0x51, 5.2=0x52, 5.3=0x53, 5.4=0x54; from 5.3 onward the header contains format/check data and width fields for int, size_t, instruction, lua_Integer, lua_Number, with a check value at the end - LuaJIT bytecode header:
0x1B 4C 4A(\x1bLJ) + version byte—2.0=0x01, 2.1=0x02, followed by endianness and type width flags - Memory-side identification: scan for the magic number ([[re-memdump]]), or locate the interpreter by references to the interpreter version string (
Lua 5.x/LuaJIT 2.x) and error messages (attempt to call a nil valuetype) - Save: VM type + exact version + endianness/width parameters (determines the parsing path)
Instruction Structure
- Lua 5.1~5.3: 32-bit instruction = opcode(6) + A(8) + B(9) + C(9); Bx is B|C concatenated (18 bits), sBx is signed
- Lua 5.4: opcode expanded to 7 bits, A is 8 bits, B/C narrowed to 8 bits and a new k flag bit is added, Bx is 17 bits—parsing tables for 5.3 and earlier cannot be reused
- LuaJIT: 32-bit instructions have two formats—ABC format opcode(8) + A(8) + C(8) + B(8) (in little-endian memory byte order it is OP/A/C/B), AD format A(8) + D(16); there is no Bx field, and the D field carries both constant indices and jump offsets (jump offsets are biased, D − 0x8000)
- Parsing approach: select the instruction layout table by version and split the 32-bit word into fields; the symptom of selecting the wrong layout table version is jump targets/register indices going out of bounds
- Jump offsets are measured in instruction indices (not bytes), and the base differs slightly by version, so confirm by version when parsing
Constant table / upvalue table location
- Function prototype (Proto) serialization order is fixed: source string → line numbers/parameters/vararg/maxstacksize → code section → constant table → upvalue table → nested Proto → debug information; strings are length-prefix encoded, advance in order
- Value of the constant table: numbers/strings in the script (skill values, level configuration, keys used for validation) are directly readable, which is faster than restoring the full semantics
- Memory side: obtain the Proto layout at the load/execute API, or search the heap by known constant value characteristics (format strings, specific numeric values)
Disassembly approach (luac-like tools)
- Parse layer by layer in the order "header → instructions → constants/upvalues → nested Proto", and output pseudo-assembly (
GETGLOBAL/CALLclass) - When an existing parser does not support the target version, write your own parser according to the above field order (small in scale); the core is a correct instruction layout table for the version
- Output used for: finding the target function by cross-referencing constant table strings/numeric values; modifying instruction operands or constant values to achieve modifications ([[re-patching]] approach)
LuaJIT bytecode differences
- LuaJIT bytecode is incompatible with standard Lua 5.1 bytecode (compatible at the language level, but the instruction sets differ); LuaJIT 2.0 and 2.1 are also incompatible with each other (opcode numbering/semantics have changed)
- Bytecode is bound to the runtime: endianness, pointer width, and type width are all written into the file header, so a file dumped on a different platform will fail to parse if moved directly to another environment
- Countermeasure: strictly follow
\x1bLJ+ version byte along the LuaJIT parsing path; make modifications preferentially at the source/bytecode level, and do not carry them across versions
Cocos-family script resource extraction (.jsc / .luac obfuscated variants)
- Cocos2d-x Lua scenario:
.luacis standard Lua/LuaJIT bytecode, and common obfuscated variants are header truncation (deleting/modifying the\x1bLuamagic number) and single-byte XOR over the whole file; for encrypted variants (XXTEA scheme, when the ciphertext has thexxteastart marker, decrypt first and then follow the bytecode path) - Cocos Creator scenario:
.jscis JS engine bytecode, and header truncation/XOR variants are likewise common - Countermeasure (identify first, then solve): recover using a known magic number as a crib—mask = magic number's corresponding byte XOR the file's first byte; after recovering the magic number, parse by version; if truncated, first fill in the header bytes; the method is isomorphic to the XOR section of [[re-fw-extract]] (block-start mask and block-periodic transformation ideas can be reused directly)
Script function hooking ([[re-frida]] / memory side)
- Load side:
luaL_loadbuffer/luaL_loadbufferx/luaL_loadfile/lua_load—hooks intercept the original bytecode/source code, suitable for changing script logic (replacing bytecode, changing constants) - Execution side:
lua_pcall/lua_pcallk/lua_call—hooks intercept arguments/return values, suitable for changing runtime behavior; LuaJIT also has an assembly call path that does not go through the C API wrapper - Memory side: when not hooking, directly patch the already-loaded bytecode (change constant values/jumps), or modify the resource file and then replace the load path
- Unsigned scenario: locate interpreter function addresses by version string / error message references
Common pitfalls
- Bytecode is incompatible across Lua versions: Symptom — dumped bytecode parsed with a different tool/runtime comes out garbled or fails outright; Cause — Lua 5.1–5.4 and LuaJIT all differ in instruction layout, header fields, and checksum data, and even the same version splits by endianness/width, with 5.4 even changing the opcode bit width; Countermeasure — first determine the version and endianness/width precisely from the magic number + version byte, then choose the corresponding parsing path; for cross-version scenarios, first find same-version source/interpreter and recompile for comparison, do not hard-decode
- .jsc/.luac variant magic numbers are truncated: Symptom — the file header is not
\x1bLua/\x1bLJ, and parsing directly as bytecode fails; Cause — obfuscated variants cut off or alter the magic number (truncated header, single-byte XOR over the whole file); Countermeasure — identify before decoding: use the known magic number as a crib to recover the mask (mask = magic number bytes XOR the corresponding file position bytes) or pad out the truncated header, then parse by version once the magic number is restored; for XXTEA-style encryption, first decrypt by its encryption characteristics and then follow the bytecode path - Hook point selection: lua_pcall is not the only entry: Symptom — when only hooking
lua_pcall, some script behavior is not triggered or is triggered repeatedly, and hooking load-type APIs does not get the target script; Cause — the interpreter has multiple call surfaces (lua_call/lua_pcallk/load-type APIs each form their own surface), and LuaJIT also has assembly call paths that bypass the C API; Countermeasure — first define the goal: to modify script logic, hook the load surface to get bytecode; to modify runtime behavior, hook the execution surface or patch memory; when hooking fails, locate the interpreter's internal call points by version string / error message references - Jump target conversion errors: Symptom — disassembled jump target addresses are absurd/out of bounds; Cause — Lua bytecode jump offsets are in units of instruction index (not bytes), and the Bx bit width and base vary by version (5.1–5.3 use current instruction index + 1 + sBx); Countermeasure — confirm the bit width and base by version, and after conversion check whether the target falls within the code section
Graphics and Shaders
The goal of graphics-side reverse engineering is usually: change visual effects (color/parameters/culling), locate resource binding and constant update points, and recover rendering pipeline logic. Entry points split into the file side (shader resources) and the memory side (runtime load points).
SPIR-V disassembly (spirv-dis style approach)
- Magic number 0x07230203 (file byte order 03 02 23 07), word stream structure: header (magic/version/generator/bound/schema) + instruction sequence (each: word count + opcode + operands)
- Disassembly approach: split the instruction stream on word boundaries, recover semantics such as OpFunction/OpEntryPoint/OpConstant/OpType*/OpDecorate
- Key locating points: OpEntryPoint defines the entry (stage type + entry function), OpDecorate's DescriptorSet/Binding/Location define resource bindings and vertex attributes, OpConstant defines constant initial values (which can be modified directly)
- Generalization: the binary IR parsing routine (magic number → structure header → instruction stream → semantic table) is common across many GPU IRs; switching formats only switches the magic number and instruction table
DXIL / DXBC disassembly (dxc style approach)
- DXBC: DX10/11 container format ("DXBC" magic number + chunk table), with shader code in the SHDR/SHEX chunks and input/output signatures and resource reflection in chunks such as ISGN/OSGN/RDEF
- DXIL: DX12 SM6+ bytecode, the outer layer is still a DXBC container, and the inner "DXIL" sub-object is in LLVM bitcode style — the parsing path is completely different from DXBC
- Disassembly approach: first determine the container/sub-object type (DXBC chunk vs DXIL), then follow the corresponding format; pay attention to the SM version (which determines the instruction set) and cbuffer reflection (names/member offsets)
- Memory side: scan for the "DXBC"/"DXIL" magic numbers to locate already-compiled shaders ([[re-memdump]]), or hook creation-type APIs to intercept the bytes
GLSL restoration (spirv-cross-style approach, generalized)
- Restoration path: IR → target language (GLSL/HLSL/MSL); the approach is type reconstruction (OpType* → language types) → semantic restoration (OpDecorate → layout/qualifiers) → expression reconstruction
- Restoration is lossy: reconstruction of loops/switches/optimized structures is incomplete, and the restored result is for reading and analysis; it may not necessarily recompile as-is
- Generalization: organize the IR→source restorer in three passes of "type → declaration → function body"; to change the target language, only swap the syntax emission layer
Finding the rendering entry point and locating the constant table
- File side: for shader resources (.spv/.dxbc/.hlsl/.cso or in-package resources), first unpack them ([[re-fw-extract]]) and then disassemble according to format; in the binary, locate the load point by references to loading API strings (glShaderSource/glCompileShader, CreateVertexShader/CreatePixelShader/CreateComputeShader, vkCreateShaderModule, etc.)
- Memory side: hook loading-type APIs to intercept the original bytes (source code or compiled bytecode), or scan for magic numbers and dump ([[re-memdump]])
- Constant table location: CPU side update points—the glUniform* series (single-value uniforms indexed by location), VSSetConstantBuffers/UpdateSubresource types, Vulkan's vkCmdPushConstants/descriptor update types; GPU side layout—cbuffers are packed by struct member offsets + 16-byte alignment (there is padding between float4 arrays), while single-value uniforms do not have this rule
- Parameter modification entry points: change values at runtime at constant update points, statically patch constant initial values in bytecode, or modify constant table data
Common pitfalls
- Misjudging DXIL vs DXBC (the SM6 vs SM5 paths differ): Symptom—parsing fails as DXBC's SHDR/SHEX, or disassembly along the DXIL path produces garbled output; Cause—SM5.x and below use DXBC bytecode, while SM6+ uses DXIL (LLVM bitcode style); the parsing paths are completely different, and the outer container of DXIL also uses the "DXBC" magic number, so judging only by the container name leads to misidentification; Countermeasure—first confirm the SM version (reflection/RDEF or build configuration); for SM6+, find the "DXIL" sub-object inside the container and follow the DXIL path, while SM5- follows the DXBC chunk path; the two paths are not interchangeable
- SPIR-V has no function names (locate by entry point/resource binding): Symptom—the disassembly result is all OpFunction/OpLabel numbers, and the target logic cannot be found; Cause—release builds usually strip OpName/OpMemberName debug information; Countermeasure—determine the entry point via OpEntryPoint (stage type + function reference), determine resources via OpDecorate's Binding/Set/Location, and locate the target fragment by known value characteristics of OpConstant (matrix initial values/threshold constants)
- Confusing shader constant tables with uniform buffers: Symptom—the uniform update points found on the CPU side do not match the constant layout disassembled on the GPU side; Cause—the same logic may follow two different paths: single-value uniforms (passed directly by location index) and uniform/constant buffers (packed by struct member offsets + 16-byte alignment, including padding); Countermeasure—first confirm the path via disassembly/reflection (for SPIR-V, look at OpDecorate Uniform/Block; for DXBC, look at RDEF's cbuffer member table); convert single values by location number and cbuffers by member offset; do not mix the two packing rules
Cross-domain combination
- [[re-binary-core]]: static analysis foundation for game exe files ([[re-format-pe]], etc.)
- [[re-dotnet]]: decompiling Unity mono managed logic (the main tool on the static side of this skill)
- [[re-patching]]: implementing function modifications (jumps/byte-level patches)
- [[re-anti-analysis]]: countering anti-cheat detection (reusing anti-debugging ideas)
- [[re-sandbox]]: mandatory isolation for anti-cheat game analysis (the highest principle of [[re-analyze/platform-tips]])
- [[re-mobile]]: mobile games (Unity/Unreal mobile versions) go to [[re-apk]] / [[re-ios]] / [[re-frida]]
- [[re-cracking]]: bypassing single-player game license/activation checks
- [[re-console]]: reverse engineering modern consoles (Switch/PS4/PS5) and retro platforms (ROM headers/cartridge formats/Cheat codes) games
Common Pitfalls and Traps
- IL2CPP has no managed DLL but you are still looking for DLLs: Symptom—
Game_Data/Managed/has only a few/no DLLs, and decompilation cannot find the target logic; Cause—IL2CPP compiles C# into native binaries, and managed DLLs do not exist; Countermeasure—directly use Il2CppDumper (the version must support the target Unity version; if too old, parsingglobal-metadata.datwill fail), and follow function addresses from dump.cs - Pointer chain scanning is cumbersome and prone to failure: Symptom—addresses found by value scanning change on every launch, and directly modifying them has no effect; Cause—the game uses dynamic memory allocation/ASLR, and the base address differs each time; Countermeasure—CE
Pointer scansave the.ptpointer chain, or locate by module base address + offset (Game.exe+0x...), recalculate the base address after launch and then apply the offset; confirm multi-level offsets layer by layer - Strong anti-cheat (EAC/BE) detection: Symptom—attach is rejected, the debugger is detected, and the account is banned; Cause—anti-cheat kernel drivers monitor debuggers and memory traces; Countermeasure—fully isolate the analysis environment (dedicated VM/offline, [[re-sandbox]] + [[re-analyze/platform-tips]] highest principle), analyze detection points statically and then bypass them ([[re-anti-analysis]]), and never attach a debugger in an online environment
- Frequent game updates invalidate patches: Symptom—after an update, all original addresses are invalid and patches no longer take effect; Cause—each update recompiles and addresses/offsets change; Countermeasure—record patches using "module + offset + byte pattern" rather than absolute addresses; after an update, diff the old and new exe to locate changed points, or relocate by string/function name (IL2CPP dump)
- Anti-cheat detects virtualization/multi-boxing: Symptom—the game exits directly or behaves abnormally in a VM; Cause—some anti-cheat software detects VBS/VMP characteristics and virtualized environments; Countermeasure—adjust the VM configuration according to the anti-cheat's known evasion checklist (options to hide hypervisor characteristics); if it still does not work, prioritize static analysis and move the dynamic portion to an older version/offline mode without anti-cheat
- UE5 Dx12 rendering hook (vtable replacement): Symptom—the Dx11 Present hook (copied from ImGui) fails on UE5 Dx12, and after Present breaks, the main module code cannot be found in the stack; Cause—UE5 ships its own separately compiled
D3D12Core.dll(The Release version is called D3D12Core.dll, and the Debug version is called d3d12SDKLayers.dll, it does not use the system directory DLL, and signatures cannot be found in the system DLL), and when GPU acceleration/super resolution is enabled, the upper stack of Present is occupied by other DLLs; Countermeasure—use the open-source UE5 source code to compile a D3D12Core.dll yourself and drag it into IDA as a reference (ExecuteCommandListsis at CommandQueue vtable index=10); after Present breaks, locate it directlyReturn address under the main module(Super-resolution is identified by dll name; changing the mode changes the dll name, the approach stays the same); vtable replacement point:mov rax,[rcx]At the call site, take rcx (object pointer) to replace the vtable - In-game 5-byte hook detection (anti-hook): Symptom — the head of the located target function has already been modified (classic 5-byte hook stub), and the game's own calls pass while external hook calls fail; Cause — the game/engine has built-in call-origin detection (checks the stack return), and non-self calls restore the first 5 bytes; Countermeasure — do not hook the protected function itself, instead hook atthe location that calls it(the stack return site) to get the object pointer (rcx/r12, etc.) and replace the vtable to achieve the same effect; Identification features: the first 5 bytes of the function head are modified + there is logic inside the function to restore the head (key functions like FName::ToString/StaticFindObject are handled the same way in multiple places)
- IL2CPP metadata restoration: Symptom — the dump tool reports an unsupported version/incomplete output; Cause — the Unity version changed the metadata format; Countermeasure — upgrade the dump tool (Il2CppDumper or Il2CppInspectorRedux); you must use the artifact pair from the same dump (script.json matches the so, and clear the IDA cache when switching)
- Encrypted global-metadata.dat: Symptom — the metadata file is ciphertext; Cause — anti-cheat/custom encryption; Countermeasure — find the initialization decryption function (around il2cpp_init), and with Frida dump the decrypted metadata from memory after mmap/read and feed it to the dump tool
- IL2CPP method hook: Symptom — Frida bare hook errors out; Cause — IL2CPP methods are not standard Java/ObjC and require computing offsets from metadata; Countermeasure — use the frida-il2cpp-bridge library, do not hardcode Interceptor.attach
- Crash after patch: Symptom — startup crash after static modification; Cause — file hash verification/anti-tamper; Countermeasure — prefer hooking (do not modify files); when static patch is required, handle the verification logic at the same time; after repackaging, delete META-INF and re-sign (apksigner) (Source: reverse-skill field-journal, MIT)
- IL2CPP dual-file structure pairing (metadata skeleton + binary flesh): Symptom — with only
global-metadata.dator only the binary, the dump result lacks method implementations/field offsets/type graphs, and when the two files have mismatched versions parsing becomes corrupted; Cause — IL2CPP output is split in two halves: metadata (magic 0xFAB11BAF) stores only the declarative skeleton (type/method/field definitions, parameter default values, string literals, generic containers, interface offsets, vtable method indices), while the binary (GameAssembly.dll / libil2cpp.so, etc.) stores the flesh (Il2CppType type graph, field offset table, method pointer table, generic instantiation table); type references are cross-linked across both sides, and if either is missing or version-mismatched, full restoration is impossible; moreover, information ownership drifts with versions (v27 removed metadata usage, and newer versions moved method specs, etc. back into metadata — the exact migration versions are subject to actual testing, do not hardcode based on remembered version numbers); Countermeasure — use the two files as a same-version pair; handle information ownership in a version-forked manner; binary-side parsing depends only on the two primitives 'virtual address → file offset mapping + pointer size', uniformly abstracted across PE/ELF/Mach-O/WASM/NSO (Source: Cpp2IL (SamboyCoding), MIT) - metadata version determination must be jointly decided by the version number + Unity version: Symptom—the dump tool works inconsistently for the same version number across different games, with structural parsing misaligned (fields read incorrectly, index out of bounds); Cause—the header version field is a coarse-grained signal, and the same version number corresponds to multiple generations of structures (24 subdivided into 24.1~24.5, 27 split into 27.1/27.2, etc.), so the Unity version number must be used for a second arbitration to correctly read the structure; different product lines may also have the same number with different meanings (reskinning trap), requiring branching by version; starting from Unity 6 (v38+), type/method/field indices are no longer fixed at 4 bytes, but have dynamic widths of 8/16/32 bits, with the width inferred from the section's Size/Count, and the type index width even needs to be derived from the binary-side interfaceOffsets structure (the type table is not in the metadata); Countermeasure—joint arbitration of the version field + Unity version, build a version mapping table based on the "Unity version that introduced the change" and record each generation's change points (field additions/removals, bit-width changes, structural moves), the hotfix version number is sometimes the key dividing line; after indices widen, first infer the element width before reading the table, do not hardcode 4 bytes (Source: Cpp2IL (SamboyCoding), MIT)
- Registration structure location: count-mode search + candidate arbitration: Symptom—searching the binary for the MetadataRegistration/CodeRegistration structures yields a bunch of false hits, and parsing by the first one gives all garbage; Cause—these two registration structures (MetadataRegistration: pointer and count tables for types/field offsets/metadata usage; CodeRegistration: method pointer table, generic method pointer table, codegen module pointer table) are the first step of dumping, have no name to rely on, and can only be searched across the entire file as byte patterns using known metadata counts (method count/type definition count), resulting in many false hits; Countermeasure—after parsing hits as structures, exclude false hits using a validation rule set: count fields do not exceed upper limits (method count/type count in the hundreds of thousands), pointer fields must map back into the file, and key counts cross-check consistently with the metadata side (fieldOffsetsCount == type definition count, numTypes not less than type definition count); upper limit thresholds are continuously updated with the sample library (real samples push some counts up to the hundreds of thousands); v24.2+ can use the xref reference chain (mscorlib string → codegen module pointer pointing to it → module list → fall back to CodeRegistration base address); when all fail, degrade to manual address input (Source: Cpp2IL (SamboyCoding), MIT)
- Static countermeasures against tampered/encrypted metadata: Symptom—metadata parsing fails (magic mismatch, section count mismatch, layout scrambled), and only the runtime dump route comes to mind; Cause—anti-cheat/custom protection may encrypt or scramble the metadata header; Countermeasure—first determine the Unity version before parsing, with version sources having priority and cross-validation (globalgamemanagers preferred → data.unity3d package file → main program version, automatically switch to a backup source and retry when suspicious values appear); encryption keys are inferred using "known plaintext characteristics" rather than brute force—the high bytes of header offset/length fields must be 0 (file smaller than 32MB), the string area starts with two 0 bytes, and header fields are a sequential byte stream (after XOR, the difference between adjacent bytes is fixed); after constraints converge, verify segment by segment to confirm; when the layout is scrambled, treat all candidate offset/lengths in the header as a pool for DFS splicing to rebuild the section chain (offset differs from the current position within a reasonable range, length advances the position without exceeding bounds, alignment constraints); when parsing fails, a fixup callback can be attached (raw bytes + Unity version → return repaired bytes for re-reading), which is the mounting point for countering encrypted/obfuscated metadata (Source: Cpp2IL (SamboyCoding), MIT)
- Incomplete AOT generic instances + shared implementations: Phenomenon — in decompilation, some generic methods have no implementation or their addresses are placeholder values; different generic call sites resolve to the same implementation, and types do not match. Cause — AOT generates only the generic instantiations the game actually uses; instantiations that are not used have pointer placeholder values. Value-type parameters and reference-type parameters are grouped by size and share implementations (List<some reference type> and List share the same implementation pointer, while List is implemented separately). The compiler deduplicates/shares method bodies, making the "address → method" mapping many-to-one. Countermeasure — trace back from the call address to ConcreteGenericImplementationsByAddress to list all generic instantiations corresponding to that address; this is the key channel for restoring generic calls. For shared-body restoration, use conservative arbitration (only decide when exactly one non-static candidate's declaring type matches the receiver type; prefer leaving it unresolved rather than guessing). When calling a shared generic body, an extra hidden MethodInfo* trailing parameter is passed; during restoration, be careful not to treat the hidden parameter as an ordinary parameter. (Source: Cpp2IL (SamboyCoding), MIT)