Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion Il2CppInterop.Runtime/Injection/Hook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Il2CppInterop.Runtime.Injection
internal abstract class Hook<T> where T : Delegate
{
private bool _isApplied;
private bool _isSkipped;
private T _detour;
private T _method;
private T _original;
Expand All @@ -23,9 +24,19 @@ public virtual void TargetMethodNotFound()
throw new Exception($"Required target method {TargetMethodName} not found");
}

/// <summary>
/// Whether a target that resolved into writable+executable memory should be hooked anyway.
/// Such a target is not a normal function entry: packers/obfuscators emit their stubs into
/// RWX regions, and detouring one installs a hook on a trampoline that may rewrite its own
/// arguments and tail-jump elsewhere, so <c>Original(...)</c> no longer honours the delegate
/// signature. That corrupts state and eventually faults far away from here. Hooks whose
/// absence is fatal can opt back in by overriding this.
/// </summary>
public virtual bool AllowUnsafeTarget => false;

public void ApplyHook()
{
if (_isApplied) return;
if (_isApplied || _isSkipped) return;

var methodPtr = FindTargetMethod();

Expand All @@ -35,6 +46,19 @@ public void ApplyHook()
return;
}

if (!AllowUnsafeTarget && MemoryUtils.IsWritableExecutable(methodPtr))
{
// Degrade gracefully rather than installing a detour that is known to be unsound:
// whatever this hook enables stays unavailable, but the process keeps running.
Logger.Instance.LogWarning(
"{MethodName} resolved to 0x{MethodPtr}, which lies in writable+executable memory - " +
"this is a packer/obfuscator stub rather than a real function entry, so the hook is " +
"being skipped to avoid corrupting the process.",
TargetMethodName, methodPtr.ToInt64().ToString("X2"));
_isSkipped = true;
return;
}

Logger.Instance.LogTrace("{MethodName} found: 0x{MethodPtr}", TargetMethodName, methodPtr.ToInt64().ToString("X2"));

_detour = GetDetour();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Il2CppInterop.Common;
Expand Down Expand Up @@ -75,8 +75,8 @@ private IntPtr FindGetTypeInfoFromTypeDefinitionIndex(bool forceICallMethod = fa
else getTypeInfoFromTypeDefinitionIndex = imageGetTypeXrefs[0];
if ((getTypeInfoFromTypeDefinitionIndex.ToInt64() & 0xF) != 0)
{
Logger.Instance.LogTrace("Image::GetType xref wasn't aligned, attempting to resolve from icall");
return FindGetTypeInfoFromTypeDefinitionIndex(true);
Logger.Instance.LogTrace("Image::GetType xref wasn't aligned, GetTypeInfoFromTypeDefinitionIndex is likely inlined into Image::GetType");
getTypeInfoFromTypeDefinitionIndex = imageGetType;
}
if (imageGetTypeXrefs.Count() > 1 && UnityVersionHandler.IsMetadataV29OrHigher)
{
Expand Down
4 changes: 2 additions & 2 deletions Il2CppInterop.Runtime/Injection/InjectorHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
Expand Down Expand Up @@ -187,7 +187,7 @@ static nint GetClassInitSubstitute()
}
nint pClassInit = s_ClassInitSignatures
.Select(s => MemoryUtils.FindSignatureInModule(Il2CppModule, s))
.FirstOrDefault(p => p != 0);
.FirstOrDefault(p => p != 0 && (long)p >= (long)Il2CppModule.BaseAddress && (long)p < (long)Il2CppModule.BaseAddress + Il2CppModule.ModuleMemorySize);

if (pClassInit == 0)
{
Expand Down
25 changes: 25 additions & 0 deletions Il2CppInterop.Runtime/MemoryUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,31 @@ internal static unsafe List<MEMORY_BASIC_INFORMATION> GetModuleRegions(ProcessMo
return regions;
}

/// <summary>
/// Whether <paramref name="address"/> sits in committed memory that is both writable and executable.
/// Compilers never emit ordinary function entries into such memory - PE code sections map read+execute -
/// so a hit here means the address belongs to a packer/obfuscator stub region instead. Returns false when
/// the answer cannot be established (non-Windows, or <c>VirtualQuery</c> failure), leaving callers on
/// their previous behaviour rather than skipping work on a guess.
/// </summary>
internal static unsafe bool IsWritableExecutable(nint address)
{
if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1))
return false;

MEMORY_BASIC_INFORMATION memoryInfo = default;
if (Windows.VirtualQuery((void*)address, &memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
return false;

if (memoryInfo.State != MEM.MEM_COMMIT)
return false;

// The two protections that grant write and execute at once. PAGE_EXECUTE_WRITECOPY counts: the page
// becomes privately writable on first write while staying executable.
const uint pageWritableExecutable = PAGE.PAGE_EXECUTE_READWRITE | PAGE.PAGE_EXECUTE_WRITECOPY;
return (memoryInfo.Protect & pageWritableExecutable) != 0;
}

public struct SignatureDefinition
{
public string pattern;
Expand Down
Loading