From 5ae4b4083aa3b481f29b9c171bdafcdbb50b192d Mon Sep 17 00:00:00 2001 From: praydog Date: Mon, 25 Mar 2024 01:54:29 -0700 Subject: [PATCH] .NET: Initial working method hooking --- csharp-api/CMakeLists.txt | 2 + csharp-api/REFrameworkNET/Method.cpp | 5 ++ csharp-api/REFrameworkNET/Method.hpp | 26 ++++--- .../REFrameworkNET/MethodHookWrapper.cpp | 47 +++++++++++ .../REFrameworkNET/MethodHookWrapper.hpp | 78 +++++++++++++++++++ csharp-api/test/Test/Test.cs | 17 ++++ 6 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 csharp-api/REFrameworkNET/MethodHookWrapper.cpp create mode 100644 csharp-api/REFrameworkNET/MethodHookWrapper.hpp diff --git a/csharp-api/CMakeLists.txt b/csharp-api/CMakeLists.txt index 13c7cad01..3b7136801 100644 --- a/csharp-api/CMakeLists.txt +++ b/csharp-api/CMakeLists.txt @@ -107,6 +107,7 @@ set(csharp-api_SOURCES "REFrameworkNET/Callbacks.cpp" "REFrameworkNET/ManagedObject.cpp" "REFrameworkNET/Method.cpp" + "REFrameworkNET/MethodHookWrapper.cpp" "REFrameworkNET/NativeObject.cpp" "REFrameworkNET/Plugin.cpp" "REFrameworkNET/PluginManager.cpp" @@ -119,6 +120,7 @@ set(csharp-api_SOURCES "REFrameworkNET/ManagedObject.hpp" "REFrameworkNET/ManagedSingleton.hpp" "REFrameworkNET/Method.hpp" + "REFrameworkNET/MethodHookWrapper.hpp" "REFrameworkNET/MethodParameter.hpp" "REFrameworkNET/NativeObject.hpp" "REFrameworkNET/Plugin.hpp" diff --git a/csharp-api/REFrameworkNET/Method.cpp b/csharp-api/REFrameworkNET/Method.cpp index 6f795b19c..f16d769be 100644 --- a/csharp-api/REFrameworkNET/Method.cpp +++ b/csharp-api/REFrameworkNET/Method.cpp @@ -3,6 +3,7 @@ #include "ManagedObject.hpp" #include "NativeObject.hpp" +#include "MethodHookWrapper.hpp" #include "Method.hpp" #include "Field.hpp" @@ -11,6 +12,10 @@ #include "Utility.hpp" namespace REFrameworkNET { +MethodHookWrapper^ Method::AddHook(bool ignore_jmp) { + return MethodHookWrapper::Create(this, ignore_jmp); +} + REFrameworkNET::InvokeRet^ Method::Invoke(System::Object^ obj, array^ args) { if (obj == nullptr && !this->IsStatic()) { System::String^ declaringName = this->GetDeclaringType() != nullptr ? this->GetDeclaringType()->GetFullName() : gcnew System::String("UnknownType"); diff --git a/csharp-api/REFrameworkNET/Method.hpp b/csharp-api/REFrameworkNET/Method.hpp index 971257775..c6e0f54b3 100644 --- a/csharp-api/REFrameworkNET/Method.hpp +++ b/csharp-api/REFrameworkNET/Method.hpp @@ -8,17 +8,28 @@ #include "MethodParameter.hpp" namespace REFrameworkNET { +ref class MethodHookWrapper; + +public enum class PreHookResult : int32_t { + Continue = 0, + Skip = 1, +}; + public ref class Method : public System::IEquatable { public: Method(reframework::API::Method* method) : m_method(method) {} + void* GetRaw() { + return m_method; + } + REFrameworkNET::InvokeRet^ Invoke(System::Object^ obj, array^ args); bool HandleInvokeMember_Internal(System::Object^ obj, System::String^ methodName, array^ args, System::Object^% result); - /*Void* GetFunctionRaw() { + void* GetFunctionPtr() { return m_method->get_function_raw(); - }*/ + } System::String^ GetName() { return gcnew System::String(m_method->get_name()); @@ -144,14 +155,11 @@ public ref class Method : public System::IEquatable } } - // hmm... - /*UInt32 AddHook(pre_fn, post_fn, Boolean ignore_jmp) { - return m_method->add_hook(pre_fn, post_fn, ignore_jmp); - }*/ + // More palatable C# versions + delegate PreHookResult REFPreHookDelegate(System::Collections::Generic::List^ args); + delegate void REFPostHookDelegate(); - void RemoveHook(uint32_t hook_id) { - m_method->remove_hook(hook_id); - } + MethodHookWrapper^ AddHook(bool ignore_jmp); public: virtual bool Equals(System::Object^ other) override { diff --git a/csharp-api/REFrameworkNET/MethodHookWrapper.cpp b/csharp-api/REFrameworkNET/MethodHookWrapper.cpp new file mode 100644 index 000000000..c8a2fb6df --- /dev/null +++ b/csharp-api/REFrameworkNET/MethodHookWrapper.cpp @@ -0,0 +1,47 @@ +#include "./API.hpp" +#include "MethodHookWrapper.hpp" + +using namespace System::Runtime::InteropServices; +using namespace System::Collections::Generic; + +namespace REFrameworkNET { + void MethodHookWrapper::InstallHooks(bool ignore_jmp) + { + if (m_hooks_installed) { + return; + } + + REFrameworkNET::API::LogInfo("Creating .NET hook for method: " + m_method->GetName()); + + m_hooks_installed = true; + + reframework::API::Method* raw = (reframework::API::Method*)m_method->GetRaw(); + + IntPtr preHookPtr = Marshal::GetFunctionPointerForDelegate(m_preHookLambda); + IntPtr postHookPtr = Marshal::GetFunctionPointerForDelegate(m_postHookLambda); + m_hook_id = raw->add_hook((REFPreHookFn)preHookPtr.ToPointer(), (REFPostHookFn)postHookPtr.ToPointer(), ignore_jmp); + } + + void MethodHookWrapper::UninstallHooks() { + if (!m_hooks_installed) { + return; + } + + reframework::API::Method* raw = (reframework::API::Method*)m_method->GetRaw(); + + m_hooks_installed = false; + raw->remove_hook(m_hook_id); + } + + int32_t MethodHookWrapper::OnPreStart_Raw(int argc, void** argv, REFrameworkTypeDefinitionHandle* arg_tys, unsigned long long ret_addr) + { + OnPreStart(gcnew List()); // todo: pass the arguments + System::Console::WriteLine("Hello from" + m_method->GetName() + " pre-hook!"); + return 0; // Or another appropriate value + } + + void MethodHookWrapper::OnPostStart_Raw(void** ret_val, REFrameworkTypeDefinitionHandle ret_ty, unsigned long long ret_addr) { + //OnPostStart(/* arguments */); + System::Console::WriteLine("Hello from" + m_method->GetName() + " post-hook!"); + } +} \ No newline at end of file diff --git a/csharp-api/REFrameworkNET/MethodHookWrapper.hpp b/csharp-api/REFrameworkNET/MethodHookWrapper.hpp new file mode 100644 index 000000000..978e1bcec --- /dev/null +++ b/csharp-api/REFrameworkNET/MethodHookWrapper.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include "./API.hpp" +#include "./Method.hpp" + +namespace REFrameworkNET { +// Wrapper class we create when we create an initial hook +// Additional hook calls on the same method will return the instance we already made +// and additional callbacks will be appended to the existing events +public ref class MethodHookWrapper +{ +public: + // Public factory method to create a new hook + static MethodHookWrapper^ Create(Method^ method, bool ignore_jmp) + { + if (s_hooked_methods->ContainsKey(method)) { + return s_hooked_methods[method]; + } + + auto wrapper = gcnew MethodHookWrapper(method, ignore_jmp); + s_hooked_methods->Add(method, wrapper); + return wrapper; + } + + MethodHookWrapper^ AddPre(Method::REFPreHookDelegate^ callback) + { + OnPreStart += callback; + return this; + } + + MethodHookWrapper^ AddPost(Method::REFPostHookDelegate^ callback) + { + OnPostStart += callback; + return this; + } + +private: + event Method::REFPreHookDelegate^ OnPreStart; + event Method::REFPostHookDelegate^ OnPostStart; + + + // This is never meant to publicly be called + MethodHookWrapper(Method^ method, bool ignore_jmp) + { + m_method = method; + m_preHookLambda = gcnew REFPreHookDelegateRaw(this, &MethodHookWrapper::OnPreStart_Raw); + m_postHookLambda = gcnew REFPostHookDelegateRaw(this, &MethodHookWrapper::OnPostStart_Raw); + InstallHooks(ignore_jmp); + } + + ~MethodHookWrapper() + { + if (m_hooks_installed) { + UninstallHooks(); + } + } + + static System::Collections::Generic::Dictionary^ s_hooked_methods = gcnew System::Collections::Generic::Dictionary(); + + delegate int32_t REFPreHookDelegateRaw(int argc, void** argv, REFrameworkTypeDefinitionHandle* arg_tys, unsigned long long ret_addr); + delegate void REFPostHookDelegateRaw(void** ret_val, REFrameworkTypeDefinitionHandle ret_ty, unsigned long long ret_addr); + + void InstallHooks(bool ignore_jmp); + void UninstallHooks(); + + int32_t OnPreStart_Raw(int argc, void** argv, REFrameworkTypeDefinitionHandle* arg_tys, unsigned long long ret_addr); + void OnPostStart_Raw(void** ret_val, REFrameworkTypeDefinitionHandle ret_ty, unsigned long long ret_addr); + + Method^ m_method{}; + uint32_t m_hook_id{}; + bool m_hooks_installed{false}; + + REFPreHookDelegateRaw^ m_preHookLambda{}; + REFPostHookDelegateRaw^ m_postHookLambda{}; +}; +} \ No newline at end of file diff --git a/csharp-api/test/Test/Test.cs b/csharp-api/test/Test/Test.cs index b81c20096..2f70559b3 100644 --- a/csharp-api/test/Test/Test.cs +++ b/csharp-api/test/Test/Test.cs @@ -87,7 +87,24 @@ public class NativeProxy : Proxy { } public class DangerousFunctions { + public static REFrameworkNET.PreHookResult isInsidePreHook(System.Collections.Generic.List args) { + Console.WriteLine("Inside pre hook (From C#)"); + REFrameworkNET.API.LogInfo("isInsidePreHook"); + return REFrameworkNET.PreHookResult.Continue; + } + + public static void isInsidePostHook() { + Console.WriteLine("Inside post hook (From C#)"); + } + public static void Entry() { + var tdb = REFrameworkNET.API.GetTDB(); + tdb.GetType("app.CameraManager")?. + GetMethod("isInside")?. + AddHook(false). + AddPre(isInsidePreHook). + AddPost(isInsidePostHook); + // These via.SceneManager and via.Scene are // loaded from an external reference assembly // the classes are all interfaces that correspond to real in-game classes