fix: hooks can resolve into packer/obfuscator RWX stubs, producing a delayed fatal AccessViolationException - #281
Open
MagicPro1994 wants to merge 2 commits into
Conversation
Il2CppInterop locates its injection hooks by scanning native code. On packed/obfuscated IL2CPP builds that scan can land on an obfuscator trampoline instead of a real function entry, and detouring one is unsound: the stub may rewrite its own arguments and tail-jump elsewhere, so Original(...) no longer honours the delegate signature. The process then survives thousands of ordinary calls and hard-faults later, far from the actual mistake. Observed on Rise of Eros (Unity 2022.3.62f2, metadata v29.1), where MetadataCache::GetTypeInfoFromTypeDefinitionIndex resolved into a .data section marked CODE|EXECUTE|WRITE. The bytes there zero the saved rcx, replace it via `or rcx,rax`, and tail-jump through a push/lea/xchg/ret sequence. Result: a hard AccessViolationException inside the hook, reproducible 100% on one content path and absent everywhere else. Compilers never emit ordinary function entries into writable+executable memory -- PE code sections map read+execute -- so W+X is a reliable marker for packer/obfuscator stub regions. Check the resolved target with VirtualQuery before detouring; when it is W+X, log a warning and skip that hook so the feature degrades instead of corrupting the process. Hooks whose absence is fatal can override AllowUnsafeTarget. On unpacked games no code section is W+X, so this is a no-op there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On packed/obfuscated IL2CPP builds, the native-code scan that locates injection hooks can land on
an obfuscator trampoline rather than a real function entry. Detouring one is unsound — the stub may
rewrite its own arguments and tail-jump elsewhere, so
Original(...)no longer honours the delegatesignature. The process survives thousands of ordinary calls and then hard-faults far from the actual
mistake, which makes this very hard to attribute.
I have a concrete diagnosis and a small general fix; happy to open a PR if the approach looks right.
Environment
1.5.3.0(also reproduced on currentmaster, and with fix: harden hook and signature resolution for optimized IL2CPP builds #262 cherry-picked)6.0.0-be.785GameAssembly.dllis packed;global-metadata.datisencrypted at rest and decrypted natively at startup
Symptom
100% reproducible on one specific content path (a stage using "borrowed" characters), absent
everywhere else, and reproduces with zero plugins loaded — so it is not mod code. Everything up
to that point works: the game runs normally, and the same hook services thousands of successful
calls first.
Root cause
MetadataCache::GetTypeInfoFromTypeDefinitionIndexresolves toGameAssembly.dll+0x51830B0, whichis in the
.datasection. This binary marks that section0xE0000020(
CNT_CODE | MEM_EXECUTE | MEM_READ | MEM_WRITE) — writable and executable. The hooks thatresolve correctly in the same run (
il2cpp_image_get_class,Image::GetType) land in.xpdata,marked
0x60000020— executable, not writable, like a normal PE code section.Disassembling the resolved target shows it is not a function entry:
It replaces its own first argument and tail-jumps via a
rettrick. The hook is declared asIl2CppClass* MethodDelegate(int index)and callsOriginal(index); against this stub thatcontract does not hold.
Proposed fix
Compilers never emit ordinary function entries into writable+executable memory — PE code sections
map read+execute — so W+X is a reliable marker for packer/obfuscator stub regions.
In
Hook<T>.ApplyHook(), check the resolved target withVirtualQuerybefore detouring. If it isW+X, log a warning and skip that hook, so the feature degrades gracefully instead of corrupting the
process. Hooks whose absence would be fatal can opt back in via a virtual
AllowUnsafeTarget.On unpacked games no code section is W+X, so this is a no-op there.
This reuses the
MemoryUtils/TerraFXVirtualQueryinfrastructure already added in #267.There is precedent for skipping this specific hook when the environment does not fit its
assumptions: #260 skips it under HybridCLR.
Result
With the guard in place: no crash, and
ClassInjector.RegisterTypeInIl2Cppstill works — myinjected
MonoBehaviourregisters and functions normally on this build, so at least here theskipped hook costs nothing observable.
Possibly related
#230 (same hook, Unity 2022.3.52f1, AV immediately after the hook springboard), #214, #226, #138 —
all AV reports on this hook family without an identified root cause. I cannot confirm they share
this cause, but the W+X check is cheap to test against them.