private void LogAllSections(string from, IntPtr src, uint size) { MMDbgLog.Log($"{from} failed for 0x{(long) src:X16} + {size} - logging all memory sections"); Exception ex = new Win32Exception(); MMDbgLog.Log($"reason: {ex.Message}"); try { IntPtr proc = GetCurrentProcess(); IntPtr addr = (IntPtr)0x00000000000010000; int i = 0; while (true) { if (VirtualQueryEx(proc, addr, out MEMORY_BASIC_INFORMATION infoBasic, sizeof(MEMORY_BASIC_INFORMATION)) == 0) { break; } ulong srcL = (ulong)src; ulong srcR = srcL + size; ulong infoL = (ulong)infoBasic.BaseAddress; ulong infoR = infoL + (ulong)infoBasic.RegionSize; bool overlap = infoL <= srcR && srcL <= infoR; MMDbgLog.Log($"{(overlap ? "*" : "-")} #{i++}"); MMDbgLog.Log($"addr: 0x{(long) infoBasic.BaseAddress:X16}"); MMDbgLog.Log($"size: 0x{(long) infoBasic.RegionSize:X16}"); MMDbgLog.Log($"aaddr: 0x{(long) infoBasic.AllocationBase:X16}"); MMDbgLog.Log($"state: {infoBasic.State}"); MMDbgLog.Log($"type: {infoBasic.Type}"); MMDbgLog.Log($"protect: {infoBasic.Protect}"); MMDbgLog.Log($"aprotect: {infoBasic.AllocationProtect}"); long regionSize = (long)infoBasic.RegionSize; if (regionSize <= 0 || (int)regionSize != regionSize) { if (IntPtr.Size == 8) { try { addr = (IntPtr)((ulong)infoBasic.BaseAddress + (ulong)infoBasic.RegionSize); } catch (OverflowException) { MMDbgLog.Log("overflow"); break; } continue; } break; } try { addr = (IntPtr)((uint)infoBasic.BaseAddress + (uint)infoBasic.RegionSize); } catch (OverflowException) { MMDbgLog.Log("overflow"); break; } } } finally { throw ex; } }
private IntPtr NotThePreStub(IntPtr ptrGot, IntPtr ptrParsed) { if (ThePreStub == IntPtr.Zero) { ThePreStub = (IntPtr)(-2); // FIXME: Find a better less likely called NGEN'd candidate that points to ThePreStub. // This was "found" by tModLoader. // Can be missing in .NET 5.0 outside of Windows for some reason. MethodInfo mi = typeof(System.Net.HttpWebRequest).Assembly .GetType("System.Net.Connection") ?.GetMethod("SubmitRequest", BindingFlags.NonPublic | BindingFlags.Instance); if (mi != null) { ThePreStub = GetNativeStart(mi); MMDbgLog.Log($"ThePreStub: 0x{(long) ThePreStub:X16}"); } else if (PlatformHelper.Is(Platform.Windows)) { // FIXME: This should be -1 (always return ptrGot) on all plats, but SubmitRequest is Windows-only? ThePreStub = (IntPtr)(-1); } } return((ptrParsed == ThePreStub || ThePreStub == (IntPtr)(-1)) ? ptrGot : ptrParsed); }
public static DetourRuntimeNETCorePlatform Create() { try { IntPtr jit = GetJitObject(); Guid jitGuid = GetJitGuid(jit); DetourRuntimeNETCorePlatform platform = null; if (jitGuid == DetourRuntimeNET50Platform.JitVersionGuid) { platform = new DetourRuntimeNET50Platform(); } else if (jitGuid == DetourRuntimeNETCore30Platform.JitVersionGuid) { platform = new DetourRuntimeNETCore30Platform(); } // TODO: add more known JIT GUIDs if (platform == null) { return(new DetourRuntimeNETCorePlatform()); } platform?.InstallJitHooks(jit); return(platform); } catch (Exception e) { MMDbgLog.Log("Could not get JIT information for the runtime, falling out to the version without JIT hooks"); MMDbgLog.Log($"Error: {e}"); } return(new DetourRuntimeNETCorePlatform()); }
protected virtual void JitHookCore( RuntimeTypeHandle declaringType, RuntimeMethodHandle methodHandle, IntPtr methodBodyStart, ulong methodBodySize, RuntimeTypeHandle[] genericClassArguments, RuntimeTypeHandle[] genericMethodArguments ) { try { Type declType = Type.GetTypeFromHandle(declaringType); if (genericClassArguments != null && declType.IsGenericTypeDefinition) { declType = declType.MakeGenericType(genericClassArguments.Select(Type.GetTypeFromHandle).ToArray()); } MethodBase method = MethodBase.GetMethodFromHandle(methodHandle, declType.TypeHandle); // method is null for P/Invokes, ComImports and other dynamic interop methods. // Just to be 100% sure that it ISN'T an already known-but-"hidden" pinned method though... if (method == null) { method = GetPin(methodHandle).Method; } try { OnMethodCompiled?.Invoke(method, methodBodyStart, methodBodySize); } catch (Exception e) { MMDbgLog.Log($"Error executing OnMethodCompiled event: {e}"); } } catch (Exception e) { MMDbgLog.Log($"Error in JitHookCore: {e}"); } }
private static MemberInfo _Cache(string cacheKey, MemberInfo value) { if (cacheKey != null && value == null) { MMDbgLog.Log($"ResolveRefl failure: {cacheKey}"); } if (cacheKey != null && value != null) { lock (ResolveReflectionCache) { ResolveReflectionCache[cacheKey] = new WeakReference(value); } } return(value); }
protected override unsafe IntPtr GetFunctionPointer(MethodBase method, RuntimeMethodHandle handle) { MMDbgLog.Log($"mets: {method.GetID()}"); MMDbgLog.Log($"meth: 0x{(long) handle.Value:X16}"); MMDbgLog.Log($"getf: 0x{(long) handle.GetFunctionPointer():X16}"); if (method.IsVirtual && (method.DeclaringType?.IsValueType ?? false)) { /* .NET has got TWO MethodDescs and thus TWO ENTRY POINTS for virtual struct methods (f.e. override ToString). * More info: https://mattwarren.org/2017/08/02/A-look-at-the-internals-of-boxing-in-the-CLR/#unboxing-stub-creation * * Observations made so far: * - GetFunctionPointer ALWAYS returns a pointer to the unboxing stub handle. * - On x86, the "real" entry point is often found 8 bytes after the unboxing stub entry point. * - Methods WILL be called INDIRECTLY using the pointer found in the "real" MethodDesc. * - The "real" MethodDesc will be updated, which isn't an issue except that we can't patch the stub in time. * - The "real" stub will stay untouched. * - LDFTN RETURNS A POINTER TO THE "REAL" ENTRY POINT. */ MMDbgLog.Log($"ldfn: 0x{(long) method.GetLdftnPointer():X16}"); return(method.GetLdftnPointer()); } IntPtr ptr = base.GetFunctionPointer(method, handle); /* Many (if not all) NGEN'd methods (f.e. those from mscorlib.ni.dll) are handled in a special manner. * When debugged using WinDbg, !dumpmd for the handle gives a different CodeAddr than ldftn or GetFunctionPointer. * When using !ip2md on the ldftn / GetFunctionPointer result, no MD is found. * There is only one MD, we're already accessing it, but we still can't access the "real" entry point. * Luckily a jmp to it exists within the stub returned by GetFunctionPointer. * Sadly detecting when to read it is... ugly, to say the least. * This pretty much acts as the reverse of DetourNative*Platform.Apply * Maybe this should be Native*Platform-ified in the future, but for now... */ // IMPORTANT: IN SOME CIRCUMSTANCES, THIS CAN FIND ThePreStub AS THE ENTRY POINT. if (PlatformHelper.Is(Platform.ARM)) { // TODO: Debug detouring NGEN'd methods on ARM. } else if (IntPtr.Size == 4) { int iptr = (int)ptr; // x86 if (*(byte *)(iptr + 0x00) == 0xb8 && // mov ... (mscorlib_ni!???) *(byte *)(iptr + 0x05) == 0x90 && // nop *(byte *)(iptr + 0x06) == 0xe8 && // call ... (clr!PrecodeRemotingThunk) *(byte *)(iptr + 0x0b) == 0xe9 // jmp {DELTA} ) { // delta = to - (from + 1 + sizeof(int)) // to = delta + (from + 1 + sizeof(int)) int from = iptr + 0x0b; int delta = *(int *)(from + 1); int to = delta + (from + 1 + sizeof(int)); ptr = NotThePreStub(ptr, (IntPtr)to); MMDbgLog.Log($"ngen: 0x{(long) ptr:X16}"); return(ptr); } } else { long lptr = (long)ptr; // x64 .NET Framework if (*(uint *)(lptr + 0x00) == 0x74___c9_85_48 && // in reverse order: test rcx, rcx | je ... *(uint *)(lptr + 0x05) == 0x49___01_8b_48 && // in reverse order: rax, qword ptr [rcx] | mov ... *(uint *)(lptr + 0x12) == 0x74___c2_3b_49 && // in reverse order: cmp rax, r10 | je ... *(ushort *)(lptr + 0x17) == 0xb8_48 // in reverse order: mov {TARGET} ) { ptr = NotThePreStub(ptr, (IntPtr)(*(ulong *)(lptr + 0x19))); MMDbgLog.Log($"ngen: 0x{(long) ptr:X16}"); return(ptr); } // x64 .NET Core if (*(byte *)(lptr + 0x00) == 0xe9 && // jmp {DELTA} *(byte *)(lptr + 0x05) == 0x5f // pop rdi ) { // delta = to - (from + 1 + sizeof(int)) // to = delta + (from + 1 + sizeof(int)) long from = lptr; int delta = *(int *)(from + 1); long to = delta + (from + 1 + sizeof(int)); ptr = NotThePreStub(ptr, (IntPtr)to); MMDbgLog.Log($"ngen: 0x{(long) ptr:X16}"); return(ptr); } } return(ptr); }
protected override MethodInfo _Generate(DynamicMethodDefinition dmd, object context) { MethodBase orig = dmd.OriginalMethod; MethodDefinition def = dmd.Definition; Type[] argTypes; if (orig != null) { ParameterInfo[] args = orig.GetParameters(); int offs = 0; if (!orig.IsStatic) { offs++; argTypes = new Type[args.Length + 1]; argTypes[0] = orig.GetThisParamType(); } else { argTypes = new Type[args.Length]; } for (int i = 0; i < args.Length; i++) { argTypes[i + offs] = args[i].ParameterType; } } else { int offs = 0; if (def.HasThis) { offs++; argTypes = new Type[def.Parameters.Count + 1]; Type type = def.DeclaringType.ResolveReflection(); if (type.IsValueType) { type = type.MakeByRefType(); } argTypes[0] = type; } else { argTypes = new Type[def.Parameters.Count]; } for (int i = 0; i < def.Parameters.Count; i++) { argTypes[i + offs] = def.Parameters[i].ParameterType.ResolveReflection(); } } string name = dmd.Name ?? $"DMD<{orig?.GetID(simple: true) ?? def.GetID(simple: true)}>"; Type retType = (orig as MethodInfo)?.ReturnType ?? def.ReturnType?.ResolveReflection(); MMDbgLog.Log($"new DynamicMethod: {retType} {name}({string.Join(",", argTypes.Select(type => type?.ToString()).ToArray())})"); if (orig != null) { MMDbgLog.Log($"orig: {(orig as MethodInfo)?.ReturnType?.ToString() ?? "NULL"} {orig.Name}({string.Join(",", orig.GetParameters().Select(arg => arg?.ParameterType?.ToString() ?? "NULL").ToArray())})"); } MMDbgLog.Log($"mdef: {def.ReturnType?.ToString() ?? "NULL"} {name}({string.Join(",", def.Parameters.Select(arg => arg?.ParameterType?.ToString() ?? "NULL").ToArray())})"); DynamicMethod dm = new DynamicMethod( name, typeof(void), argTypes, orig?.DeclaringType ?? dmd.OwnerType ?? typeof(DynamicMethodDefinition), true // If any random errors pop up, try setting this to false first. ); // DynamicMethods don't officially "support" certain return types, such as ByRef types. _DynamicMethod_returnType.SetValue(dm, retType); ILGenerator il = dm.GetILGenerator(); _DMDEmit.Generate(dmd, dm, il); return(dm); }
protected override unsafe IntPtr GetFunctionPointer(MethodBase method, RuntimeMethodHandle handle) { MMDbgLog.Log($"mets: {method.GetID()}"); MMDbgLog.Log($"meth: 0x{(long) handle.Value:X16}"); MMDbgLog.Log($"getf: 0x{(long) handle.GetFunctionPointer():X16}"); if (method.IsVirtual && (method.DeclaringType?.IsValueType ?? false)) { /* .NET has got TWO MethodDescs and thus TWO ENTRY POINTS for virtual struct methods (f.e. override ToString). * More info: https://mattwarren.org/2017/08/02/A-look-at-the-internals-of-boxing-in-the-CLR/#unboxing-stub-creation * * Observations made so far: * - GetFunctionPointer ALWAYS returns a pointer to the unboxing stub handle. * - On x86, the "real" entry point is often found 8 bytes after the unboxing stub entry point. * - Methods WILL be called INDIRECTLY using the pointer found in the "real" MethodDesc. * - The "real" MethodDesc will be updated, which isn't an issue except that we can't patch the stub in time. * - The "real" stub will stay untouched. * - LDFTN RETURNS A POINTER TO THE "REAL" ENTRY POINT. */ MMDbgLog.Log($"ldfn: 0x{(long) method.GetLdftnPointer():X16}"); return(method.GetLdftnPointer()); } bool regenerated = false; ReloadFuncPtr: IntPtr ptr = base.GetFunctionPointer(method, handle); /* Many (if not all) NGEN'd methods (f.e. those from mscorlib.ni.dll) are handled in a special manner. * When debugged using WinDbg, !dumpmd for the handle gives a different CodeAddr than ldftn or GetFunctionPointer. * When using !ip2md on the ldftn / GetFunctionPointer result, no MD is found. * There is only one MD, we're already accessing it, but we still can't access the "real" entry point. * Luckily a jmp to it exists within the stub returned by GetFunctionPointer. * Sadly detecting when to read it is... ugly, to say the least. * This pretty much acts as the reverse of DetourNative*Platform.Apply * Maybe this should be Native*Platform-ified in the future, but for now... */ // IMPORTANT: IN SOME CIRCUMSTANCES, THIS CAN FIND ThePreStub AS THE ENTRY POINT. if (PlatformHelper.Is(Platform.ARM)) { // TODO: Debug detouring NGEN'd methods on ARM. } else if (IntPtr.Size == 4) { int iptr = (int)ptr; // x86 if (*(byte *)(iptr + 0x00) == 0xb8 && // mov ... (mscorlib_ni!???) *(byte *)(iptr + 0x05) == 0x90 && // nop *(byte *)(iptr + 0x06) == 0xe8 && // call ... (clr!PrecodeRemotingThunk) *(byte *)(iptr + 0x0b) == 0xe9 // jmp {DELTA} ) { // delta = to - (from + 1 + sizeof(int)) // to = delta + (from + 1 + sizeof(int)) int from = iptr + 0x0b; int delta = *(int *)(from + 1); int to = delta + (from + 1 + sizeof(int)); ptr = NotThePreStub(ptr, (IntPtr)to); MMDbgLog.Log($"ngen: 0x{(long) ptr:X8}"); return(ptr); } // .NET Core if (*(byte *)(iptr + 0x00) == 0xe9 && // jmp {DELTA} *(byte *)(iptr + 0x05) == 0x5f // pop rdi ) { // delta = to - (from + 1 + sizeof(int)) // to = delta + (from + 1 + sizeof(int)) int from = iptr; int delta = *(int *)(from + 1); int to = delta + (from + 1 + sizeof(int)); ptr = NotThePreStub(ptr, (IntPtr)to); MMDbgLog.Log($"ngen: 0x{(int) ptr:X8}"); return(ptr); } } else { long lptr = (long)ptr; // x64 .NET Framework if (*(uint *)(lptr + 0x00) == 0x74___c9_85_48 && // in reverse order: test rcx, rcx | je ... *(uint *)(lptr + 0x05) == 0x49___01_8b_48 && // in reverse order: rax, qword ptr [rcx] | mov ... *(uint *)(lptr + 0x12) == 0x74___c2_3b_49 && // in reverse order: cmp rax, r10 | je ... *(ushort *)(lptr + 0x17) == 0xb8_48 // in reverse order: mov {TARGET} ) { ptr = NotThePreStub(ptr, (IntPtr)(*(ulong *)(lptr + 0x19))); MMDbgLog.Log($"ngen: 0x{(long) ptr:X16}"); return(ptr); } // FIXME: on Core, it seems that *every* method has this stub, not just NGEN'd methods // It also seems to correctly find the body, but because ThePreStub is always -1, // it never returns that. // One consequence of this seems to be that re-JITting a method calling a patched // method causes it to use a new stub, except not patched. // It seems that if there is *any* pause between the method being prepared, and this being // called, there is a chance that the JIT will do something funky and reset the thunk for // the method (which is what GetFunctionPointer gives) back to a call to PrecodeFixupThunk. // This can be observed by checking for the first byte being 0xe8 instead of 0xe9. // If this happens at the wrong moment, we won't get the opportunity to patch the actual method // body because our only pointer to it will have been deleted. // In conclusion: *Do we need to disable re-JITing while patching?* // Correction for the above: It seems that .NET Core ALWAYS has one indirection before the method // body, and that indirection is used as an easy way to call into the JIT when necessary. Also, // the JIT never generates a call directly to ThePreStub, but instead generates a call to // PrecodeFixupThunk which then calls ThePreStub. // x64 .NET Core if (*(byte *)(lptr + 0x00) == 0xe9 && // jmp {DELTA} *(byte *)(lptr + 0x05) == 0x5f // pop rdi ) { // delta = to - (from + 1 + sizeof(int)) // to = delta + (from + 1 + sizeof(int)) long from = lptr; int delta = *(int *)(from + 1); long to = delta + (from + 1 + sizeof(int)); ptr = NotThePreStub(ptr, (IntPtr)to); MMDbgLog.Log($"ngen: 0x{(long) ptr:X16}"); return(ptr); } // x64 .NET Core, but the thunk was reset // This can also just be an optimized method immediately calling another method. if (*(byte *)(lptr + 0x00) == 0xe8 && !regenerated) // call { MMDbgLog.Log($"Method thunk reset; regenerating"); regenerated = true; int precodeThunkOffset = *(int *)(lptr + 1); long precodeThunk = precodeThunkOffset + (lptr + 1 + sizeof(int)); MMDbgLog.Log($"PrecodeFixupThunk: 0x{precodeThunk:X16}"); PrepareMethod(method, handle); goto ReloadFuncPtr; } } return(ptr); }