/// <summary> /// Creates the wrapper function that wraps a native function with our own defined or custom calling convention, /// into a regular X64 Microsoft Calling Convention function that is natively supported by the C# programming language. /// /// This allows us to call non-standard "usercall" game functions, such as for example functions that take values /// in registers as parameters instead of using the stack, functions which take paremeters in a mixture of both stack /// and registers as well as functions which varying return parameters, either caller or callee cleaned up. /// </summary> /// <param name="functionAddress">The address of the function to create a wrapper for.</param> /// <param name="reloadedFunction">Structure containing the details of the actual function in question.</param> /// <returns>Pointer to the new X64 Microsoft Calling Convention function address to call from C# code to invoke our game function.</returns> private static IntPtr CreateWrapperFunctionInternal <TFunction>(IntPtr functionAddress, X64ReloadedFunctionAttribute reloadedFunction) { // Retrieve number of parameters. int numberOfParameters = FunctionCommon.GetNumberofParametersWithoutFloats(typeof(TFunction)); int nonRegisterParameters = numberOfParameters - reloadedFunction.SourceRegisters.Length; // List of ASM Instructions to be Compiled List <string> assemblyCode = new List <string> { "use64" }; // Backup Stack Frame assemblyCode.Add("push rbp"); // Backup old call frame assemblyCode.Add("mov rbp, rsp"); // Setup new call frame // Setup Function Parameters if (numberOfParameters > 0) { assemblyCode.AddRange(AssembleFunctionParameters(numberOfParameters, reloadedFunction.SourceRegisters)); } // Make Shadow Space if necessary. if (reloadedFunction.ShadowSpace) { assemblyCode.Add("sub rsp, 32"); // Setup new call frame } // Assemble the call to the game function in question. assemblyCode.AddRange(HookCommon.X64AssembleAbsoluteCallMnemonics(functionAddress, reloadedFunction)); // Move return register back if necessary. if (reloadedFunction.ReturnRegister != X64ReloadedFunctionAttribute.Register.rax) { assemblyCode.Add("mov rax, " + reloadedFunction.ReturnRegister); } // Restore the stack pointer from the Shadow Space // 8 = IntPtr.Size // 32 = Shadow Space if (reloadedFunction.ShadowSpace) { assemblyCode.Add("add rsp, " + ((nonRegisterParameters * 8) + 32)); } else { assemblyCode.Add("add rsp, " + (nonRegisterParameters * 8)); } // Restore Stack Frame and Return assemblyCode.Add("pop rbp"); assemblyCode.Add("ret"); // Write function to buffer and return pointer. byte[] assembledMnemonics = Assembler.Assembler.Assemble(assemblyCode.ToArray()); return(MemoryBufferManager.Add(assembledMnemonics)); }
/// <summary> /// Creates the wrapper function for redirecting program flow to our C# function. /// </summary> /// <param name="reloadedFunction">Structure containing the details of the actual function in question.</param> /// <param name="functionAddress">The address of the function to create a wrapper for.</param> /// <returns></returns> internal static IntPtr CreateX64WrapperFunctionInternal <TFunction>(IntPtr functionAddress, X64ReloadedFunctionAttribute reloadedFunction) { // Retrieve number of parameters. int numberOfParameters = FunctionCommon.GetNumberofParametersWithoutFloats(typeof(TFunction)); int nonRegisterParameters = numberOfParameters - reloadedFunction.SourceRegisters.Length; // List of ASM Instructions to be Compiled List <string> assemblyCode = new List <string> { "use64" }; // If the attribute is Microsoft Call Convention, take fast path! if (reloadedFunction.Equals(new X64ReloadedFunctionAttribute(X64CallingConventions.Microsoft))) { // Backup old call frame assemblyCode.AddRange(HookCommon.X64AssembleAbsoluteJumpMnemonics(functionAddress, reloadedFunction)); } else { // Backup Stack Frame assemblyCode.Add("push rbp"); // Backup old call frame assemblyCode.Add("mov rbp, rsp"); // Setup new call frame // We assume that we are stack aligned in usercall/custom calling conventions, if we are not, game over for now. // Our stack frame alignment is off by 8 here, we must also consider non-register parameters. // Note to self: In the case you forget, dummy. Your stack needs to be 16 byte aligned. // Calculate the bytes our wrapper parameters take on the stack in total. // The stack frame backup, push rbp and CALL negate themselves so it's down to this. // Then our misalignment to the stack. int stackBytesTotal = (nonRegisterParameters * 8); int stackMisalignment = (stackBytesTotal % 16); // Prealign stack assemblyCode.Add($"sub rbp, {stackMisalignment}"); // Setup new call frame // Setup the registers for our C# method. assemblyCode.AddRange(AssembleFunctionParameters(numberOfParameters, reloadedFunction, stackMisalignment)); // And now we add the shadow space for C# method's Microsoft Call Convention. assemblyCode.Add("sub rsp, 32"); // Setup new call frame // Assemble the Call to C# Function Pointer. assemblyCode.AddRange(HookCommon.X64AssembleAbsoluteCallMnemonics(functionAddress, reloadedFunction)); // MOV our own return register, EAX into the register expected by the calling convention assemblyCode.Add($"mov {reloadedFunction.ReturnRegister}, rax"); // Restore stack pointer (this is always the same, our function is Microsoft Call Convention Compliant) assemblyCode.Add("add rsp, " + ((nonRegisterParameters * 8) + 32)); // Restore stack alignment assemblyCode.Add($"add rbp, {stackMisalignment}"); // Restore Stack Frame and Return assemblyCode.Add("pop rbp"); assemblyCode.Add("ret"); } // Assemble and return pointer to code byte[] assembledMnemonics = Assembler.Assembler.Assemble(assemblyCode.ToArray()); return(MemoryBufferManager.Add(assembledMnemonics)); }