/// <summary> /// Checks whether an instance of <see cref="X64ReloadedFunctionAttribute"/> has the same logical meaning /// as the current instance of <see cref="X64ReloadedFunctionAttribute"/>. /// </summary> /// <param name="obj">The <see cref="X64ReloadedFunctionAttribute"/> to compare to the current attribute.</param> /// <returns>True if both of the ReloadedFunctions are logically equivalent.</returns> public override bool Equals(Object obj) { // Check for type. X64ReloadedFunctionAttribute functionAttribute = obj as X64ReloadedFunctionAttribute; // Return false if null if (functionAttribute == null) { return(false); } // Check by value. return(functionAttribute.ShadowSpace == ShadowSpace && functionAttribute.ReturnRegister == ReturnRegister && functionAttribute.SourceRegisters.SequenceEqual(SourceRegisters)); }
/// <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)); }
/// <summary> /// Generates the assembly code to assemble for the passing of the /// function parameters for to our own C# CDECL compliant function. /// </summary> /// <param name="parameterCount">The total amount of parameters that the target function accepts.</param> /// <param name="reloadedFunction">Structure containing the details of the actual function in question.</param> /// <param name="stackMisalignment">The amount of extra bytes that the stack pointer is decremented by/grown in order to 16-byte align the stack.</param> /// <returns>A string array of compatible x64 mnemonics to be assembled.</returns> internal static string[] AssembleFunctionParameters(int parameterCount, X64ReloadedFunctionAttribute reloadedFunction, int stackMisalignment) { // Store our JIT Assembly Code List <string> assemblyCode = new List <string>(); // At the current moment in time, the base address of old call stack (EBP) is at [ebp + 0] // the return address of the calling function is at [ebp + 8], last parameter is therefore at [ebp + 16]. // Reminder: The stack grows by DECREMENTING THE STACK POINTER. // There exists 32 bits of Shadow Space depending on the function, we must move it to a convention supporting shadow space (Microsoft). // The initial offset from EBP (Stack Base Pointer) for the rightmost parameter (right to left passing): int nonRegisterParameters = parameterCount - reloadedFunction.SourceRegisters.Length; int currentBaseStackOffset = 0; // Set the base stack offset depending on whether the method has "Shadow Space" or not. if (reloadedFunction.ShadowSpace) { // Including parameter count will compensate for the "Shadow Space" currentBaseStackOffset = ((parameterCount + 1) * 8) + stackMisalignment; } else { // If there is no "Shadow Space", it's directly on the stack below. currentBaseStackOffset = ((nonRegisterParameters + 1) * 8) + stackMisalignment; } /* * Re-push register parameters to be used for calling the method. */ // Re-push our non-register parameters passed onto the method onto the stack. for (int x = 0; x < nonRegisterParameters; x++) { // Push parameter onto stack. assemblyCode.Add($"push qword [rbp + {currentBaseStackOffset}]"); // Go to next parameter. currentBaseStackOffset -= 8; } // Push our register parameters onto the stack. // We reverse the order of the register parameters such that they are ultimately pushed in right to left order, matching // our individual parameter order as if they were pushed onto the stack in left to right order. X64ReloadedFunctionAttribute.Register[] newRegisters = reloadedFunction.SourceRegisters.Reverse().ToArray(); foreach (X64ReloadedFunctionAttribute.Register registerParameter in newRegisters) { assemblyCode.Add($"push {registerParameter.ToString()}"); } // Now we pop our individual register parameters from the stack back into registers expected by the Microsoft // X64 Calling Convention. // This looks stupid, I know. X64ReloadedFunctionAttribute microsoftX64CallingConvention = new X64ReloadedFunctionAttribute(X64CallingConventions.Microsoft); // Loop for all registers in Microsoft Convention for (int x = 0; x < microsoftX64CallingConvention.SourceRegisters.Length; x++) { // Reduce this until the end of our parameter list, in the case we have e.g. only 3 parameters // and the convention can take 4. if (x < parameterCount) { assemblyCode.Add($"pop {microsoftX64CallingConvention.SourceRegisters[x].ToString()}"); } else { break; } } return(assemblyCode.ToArray()); }
/// <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)); }