From 55f9641fff5ee6e02513a21c4de752f0b049f359 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 16:12:32 +0000
Subject: [PATCH 01/12] Initial plan
From 04718c9b590baeb87f063a9034de34b04d8bdd71 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 16:23:13 +0000
Subject: [PATCH 02/12] Add custom function registration API and infrastructure
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
src/RemoteNET/UnmanagedRemoteApp.cs | 49 ++++++++++++++
src/ScubaDiver.API/DiverCommunicator.cs | 16 +++++
.../RegisterCustomFunctionRequest.cs | 57 ++++++++++++++++
.../RegisterCustomFunctionResponse.cs | 18 +++++
src/ScubaDiver/DiverBase.cs | 3 +
src/ScubaDiver/DotNetDiver.cs | 12 ++++
src/ScubaDiver/MsvcDiver.cs | 50 ++++++++++++++
.../CustomUndecoratedFunction.cs | 35 ++++++++++
.../MsvcPrimitives/MsvcTypesManager.cs | 67 ++++++++++++++++++-
9 files changed, 306 insertions(+), 1 deletion(-)
create mode 100644 src/ScubaDiver.API/Interactions/RegisterCustomFunctionRequest.cs
create mode 100644 src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs
create mode 100644 src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
diff --git a/src/RemoteNET/UnmanagedRemoteApp.cs b/src/RemoteNET/UnmanagedRemoteApp.cs
index d5020a6..8cea0fc 100644
--- a/src/RemoteNET/UnmanagedRemoteApp.cs
+++ b/src/RemoteNET/UnmanagedRemoteApp.cs
@@ -215,6 +215,55 @@ public override bool InjectDll(string path)
return res;
}
+ //
+ // Custom Functions
+ //
+
+ ///
+ /// Registers a custom function on a remote type for unmanaged targets
+ ///
+ /// The type to add the function to
+ /// Name of the function
+ /// Module name where the function is located (e.g., "MyModule.dll")
+ /// Offset within the module where the function is located
+ /// Return type of the function
+ /// Parameter types of the function
+ /// True if registration was successful, false otherwise
+ public bool RegisterCustomFunction(
+ Type parentType,
+ string functionName,
+ string moduleName,
+ ulong offset,
+ Type returnType,
+ params Type[] parameterTypes)
+ {
+ if (parentType == null)
+ throw new ArgumentNullException(nameof(parentType));
+ if (string.IsNullOrEmpty(functionName))
+ throw new ArgumentException("Function name cannot be null or empty", nameof(functionName));
+ if (string.IsNullOrEmpty(moduleName))
+ throw new ArgumentException("Module name cannot be null or empty", nameof(moduleName));
+
+ var request = new RegisterCustomFunctionRequest
+ {
+ ParentTypeFullName = parentType.FullName,
+ ParentAssembly = parentType.Assembly?.GetName()?.Name,
+ FunctionName = functionName,
+ ModuleName = moduleName,
+ Offset = offset,
+ ReturnTypeFullName = returnType?.FullName ?? "void",
+ ReturnTypeAssembly = returnType?.Assembly?.GetName()?.Name,
+ Parameters = parameterTypes?.Select((pt, idx) => new RegisterCustomFunctionRequest.ParameterTypeInfo
+ {
+ Name = $"param{idx}",
+ TypeFullName = pt.FullName,
+ Assembly = pt.Assembly?.GetName()?.Name
+ }).ToList() ?? new List()
+ };
+
+ return _unmanagedCommunicator.RegisterCustomFunction(request);
+ }
+
//
// IDisposable
//
diff --git a/src/ScubaDiver.API/DiverCommunicator.cs b/src/ScubaDiver.API/DiverCommunicator.cs
index 070773e..a92146e 100644
--- a/src/ScubaDiver.API/DiverCommunicator.cs
+++ b/src/ScubaDiver.API/DiverCommunicator.cs
@@ -537,6 +537,22 @@ public void UnhookMethod(LocalHookCallback callback)
public delegate (bool voidReturnType, ObjectOrRemoteAddress res) LocalEventCallback(ObjectOrRemoteAddress[] args, ObjectOrRemoteAddress retVal);
+ public bool RegisterCustomFunction(RegisterCustomFunctionRequest request)
+ {
+ var requestJsonBody = JsonConvert.SerializeObject(request);
+ var resJson = SendRequest("register_custom_function", null, requestJsonBody);
+
+ try
+ {
+ RegisterCustomFunctionResponse response = JsonConvert.DeserializeObject(resJson, _withErrors);
+ return response?.Success ?? false;
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Failed to register custom function. Error: {ex.Message}", ex);
+ }
+ }
+
public void Dispose()
{
if (_httpClient != null)
diff --git a/src/ScubaDiver.API/Interactions/RegisterCustomFunctionRequest.cs b/src/ScubaDiver.API/Interactions/RegisterCustomFunctionRequest.cs
new file mode 100644
index 0000000..d3d9d8d
--- /dev/null
+++ b/src/ScubaDiver.API/Interactions/RegisterCustomFunctionRequest.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+
+namespace ScubaDiver.API.Interactions
+{
+ ///
+ /// Request to register a custom function on a type (primarily for unmanaged targets)
+ ///
+ public class RegisterCustomFunctionRequest
+ {
+ ///
+ /// Full type name of the parent type that this function belongs to
+ ///
+ public string ParentTypeFullName { get; set; }
+
+ ///
+ /// Assembly name of the parent type
+ ///
+ public string ParentAssembly { get; set; }
+
+ ///
+ /// Name of the function to register
+ ///
+ public string FunctionName { get; set; }
+
+ ///
+ /// Module name where the function is located (e.g., "MyModule.dll")
+ ///
+ public string ModuleName { get; set; }
+
+ ///
+ /// Offset within the module where the function is located
+ ///
+ public ulong Offset { get; set; }
+
+ ///
+ /// Full type name of the return type
+ ///
+ public string ReturnTypeFullName { get; set; }
+
+ ///
+ /// Assembly of the return type
+ ///
+ public string ReturnTypeAssembly { get; set; }
+
+ ///
+ /// List of parameter types (full type names)
+ ///
+ public List Parameters { get; set; }
+
+ public class ParameterTypeInfo
+ {
+ public string Name { get; set; }
+ public string TypeFullName { get; set; }
+ public string Assembly { get; set; }
+ }
+ }
+}
diff --git a/src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs b/src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs
new file mode 100644
index 0000000..6fa0bc0
--- /dev/null
+++ b/src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs
@@ -0,0 +1,18 @@
+namespace ScubaDiver.API.Interactions
+{
+ ///
+ /// Response for a custom function registration request
+ ///
+ public class RegisterCustomFunctionResponse
+ {
+ ///
+ /// Whether the registration was successful
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Error message if registration failed
+ ///
+ public string ErrorMessage { get; set; }
+ }
+}
diff --git a/src/ScubaDiver/DiverBase.cs b/src/ScubaDiver/DiverBase.cs
index c49d465..4c908e9 100644
--- a/src/ScubaDiver/DiverBase.cs
+++ b/src/ScubaDiver/DiverBase.cs
@@ -62,6 +62,8 @@ public DiverBase(IRequestsListener listener)
// Hooking
{"/hook_method", MakeHookMethodResponse},
{"/unhook_method", MakeUnhookMethodResponse},
+ // Custom Functions
+ {"/register_custom_function", MakeRegisterCustomFunctionResponse},
};
_remoteHooks = new ConcurrentDictionary();
}
@@ -370,6 +372,7 @@ private string MakeLaunchDebuggerResponse(ScubaDiverMessage arg)
protected abstract string MakeSetFieldResponse(ScubaDiverMessage arg);
protected abstract string MakeArrayItemResponse(ScubaDiverMessage arg);
protected abstract string MakeUnpinResponse(ScubaDiverMessage arg);
+ protected abstract string MakeRegisterCustomFunctionResponse(ScubaDiverMessage arg);
private string MakeDieResponse(ScubaDiverMessage req)
{
diff --git a/src/ScubaDiver/DotNetDiver.cs b/src/ScubaDiver/DotNetDiver.cs
index eb5842a..3eeda4f 100644
--- a/src/ScubaDiver/DotNetDiver.cs
+++ b/src/ScubaDiver/DotNetDiver.cs
@@ -1531,6 +1531,18 @@ protected override string MakeUnpinResponse(ScubaDiverMessage arg)
}
}
+ protected override string MakeRegisterCustomFunctionResponse(ScubaDiverMessage arg)
+ {
+ // Custom function registration is not supported for managed (.NET) targets
+ // This feature is only available for unmanaged (native C++) targets
+ RegisterCustomFunctionResponse response = new RegisterCustomFunctionResponse
+ {
+ Success = false,
+ ErrorMessage = "Custom function registration is not supported for managed (.NET) targets"
+ };
+ return JsonConvert.SerializeObject(response);
+ }
+
// IDisposable
public override void Dispose()
{
diff --git a/src/ScubaDiver/MsvcDiver.cs b/src/ScubaDiver/MsvcDiver.cs
index 0f02308..227f561 100644
--- a/src/ScubaDiver/MsvcDiver.cs
+++ b/src/ScubaDiver/MsvcDiver.cs
@@ -865,6 +865,56 @@ protected override string MakeUnpinResponse(ScubaDiverMessage arg)
return QuickError("Not Implemented");
}
+ protected override string MakeRegisterCustomFunctionResponse(ScubaDiverMessage arg)
+ {
+ string body = arg.Body;
+ if (string.IsNullOrEmpty(body))
+ {
+ return QuickError("Missing body");
+ }
+
+ var request = JsonConvert.DeserializeObject(body);
+ if (request == null)
+ {
+ return QuickError("Failed to deserialize body");
+ }
+
+ Logger.Debug($"[MsvcDiver][MakeRegisterCustomFunctionResponse] Registering custom function: {request.FunctionName} on type {request.ParentTypeFullName}");
+
+ try
+ {
+ // Extract parameter type names from the request
+ string[] argTypeFullNames = request.Parameters?.Select(p => p.TypeFullName).ToArray() ?? new string[0];
+
+ bool success = _typesManager.RegisterCustomFunction(
+ request.ParentTypeFullName,
+ request.ParentAssembly,
+ request.FunctionName,
+ request.ModuleName,
+ request.Offset,
+ request.ReturnTypeFullName,
+ argTypeFullNames);
+
+ RegisterCustomFunctionResponse response = new RegisterCustomFunctionResponse
+ {
+ Success = success,
+ ErrorMessage = success ? null : "Failed to register custom function"
+ };
+
+ return JsonConvert.SerializeObject(response);
+ }
+ catch (Exception ex)
+ {
+ Logger.Debug($"[MsvcDiver][MakeRegisterCustomFunctionResponse] Exception: {ex}");
+ RegisterCustomFunctionResponse response = new RegisterCustomFunctionResponse
+ {
+ Success = false,
+ ErrorMessage = ex.Message
+ };
+ return JsonConvert.SerializeObject(response);
+ }
+ }
+
public override void Dispose()
{
}
diff --git a/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs b/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
new file mode 100644
index 0000000..95e49e2
--- /dev/null
+++ b/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
@@ -0,0 +1,35 @@
+using ScubaDiver.Rtti;
+
+namespace ScubaDiver
+{
+ ///
+ /// Represents a custom user-defined function that can be registered on a type
+ ///
+ public class CustomUndecoratedFunction : UndecoratedFunction
+ {
+ private readonly ModuleInfo _module;
+ private readonly nuint _address;
+ private readonly string _retType;
+ private readonly string[] _argTypes;
+
+ public CustomUndecoratedFunction(
+ string moduleName,
+ nuint baseAddress,
+ ulong offset,
+ string functionName,
+ string returnType,
+ string[] argTypes)
+ : base(functionName, functionName, functionName, argTypes?.Length)
+ {
+ _module = new ModuleInfo { Name = moduleName, BaseAddress = baseAddress };
+ _address = baseAddress + offset;
+ _retType = returnType;
+ _argTypes = argTypes ?? new string[0];
+ }
+
+ public override ModuleInfo Module => _module;
+ public override nuint Address => _address;
+ public override string RetType => _retType;
+ public override string[] ArgTypes => _argTypes;
+ }
+}
diff --git a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
index 0b4d22b..fd57305 100644
--- a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
+++ b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
@@ -135,6 +135,7 @@ public class MsvcType : Type
private MsvcModule _module;
private MsvcMethod[] _methods;
private VftableInfo[] _vftables;
+ private List _customMethods = new List();
public Rtti.TypeInfo TypeInfo { get; set; }
public MsvcType(MsvcModule module, Rtti.TypeInfo typeInfo)
@@ -152,6 +153,11 @@ public void SetVftables(VftableInfo[] vftables)
_vftables = vftables;
}
+ public void AddCustomMethod(MsvcMethod method)
+ {
+ _customMethods.Add(method);
+ }
+
public override MsvcModule Module => _module;
public override string Name => TypeInfo.Name;
@@ -159,7 +165,12 @@ public void SetVftables(VftableInfo[] vftables)
public override string FullName => TypeInfo.FullTypeName;
- public override MsvcMethod[] GetMethods(BindingFlags bindingAttr) => _methods;
+ public override MsvcMethod[] GetMethods(BindingFlags bindingAttr)
+ {
+ if (_methods == null)
+ return _customMethods.ToArray();
+ return _methods.Concat(_customMethods).ToArray();
+ }
public new MsvcMethod[] GetMethods() => GetMethods(0);
public VftableInfo[] GetVftables() => _vftables;
public override MemberInfo[] GetMembers(BindingFlags bindingAttr) => [.. GetVftables(), .. GetMethods()];
@@ -467,6 +478,60 @@ bool IsNotExport(ulong addr)
return res;
}
}
+
+ ///
+ /// Registers a custom function on a type
+ ///
+ public bool RegisterCustomFunction(
+ string parentTypeFullName,
+ string parentAssembly,
+ string functionName,
+ string moduleName,
+ ulong offset,
+ string returnTypeFullName,
+ string[] argTypeFullNames)
+ {
+ try
+ {
+ // Get the parent type
+ Predicate moduleFilter = Filter.CreatePredicate(parentAssembly);
+ Predicate typeFilter = Filter.CreatePredicate(parentTypeFullName);
+ MsvcTypeStub typeStub = GetType(moduleFilter, typeFilter);
+ if (typeStub == null)
+ return false;
+
+ MsvcType parentType = typeStub.Upgrade();
+ if (parentType == null)
+ return false;
+
+ // Find the module base address
+ List modules = GetUndecoratedModules(Filter.CreatePredicate(moduleName));
+ if (modules.Count == 0)
+ return false;
+
+ UndecoratedModule targetModule = modules.First();
+ nuint moduleBaseAddress = targetModule.ModuleInfo.BaseAddress;
+
+ // Create a custom undecorated function
+ CustomUndecoratedFunction customFunc = new CustomUndecoratedFunction(
+ moduleName,
+ moduleBaseAddress,
+ offset,
+ functionName,
+ returnTypeFullName,
+ argTypeFullNames);
+
+ // Create an MsvcMethod from the custom function and add it to the type
+ MsvcMethod customMethod = new MsvcMethod(parentType, customFunc);
+ parentType.AddCustomMethod(customMethod);
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
}
public class MsvcModuleExports
From ccbbf459202e9433304253558575400f0d9e691a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 16:25:28 +0000
Subject: [PATCH 03/12] Add tests and documentation for custom function
registration
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
README.md | 32 ++++++++
.../RegisterCustomFunctionTests.cs | 82 +++++++++++++++++++
2 files changed, 114 insertions(+)
create mode 100644 src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
diff --git a/README.md b/README.md
index d663817..9df8852 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,38 @@ The limitations:
2. The callback must define the exact number of parameters for that event
3. Lambda expression are not allowed. The callback must be cast to an `Action<...>`.
+### ✳️ Registering Custom Functions (Unmanaged/C++ Targets Only)
+For unmanaged (MSVC C++) targets, you can register custom functions that aren't automatically discovered by the RTTI scanner.
+This is useful when you know the address of a function in the target process and want to call it through RemoteNET.
+
+```C#
+// Connect to an unmanaged target
+UnmanagedRemoteApp unmanagedApp = (UnmanagedRemoteApp)RemoteAppFactory.Connect("MyNativeTarget.exe", RuntimeType.Unmanaged);
+
+// Get a remote type
+Type remoteType = unmanagedApp.GetRemoteType("MyNamespace::MyClass", "MyModule.dll");
+
+// Register a custom function on the type
+bool success = unmanagedApp.RegisterCustomFunction(
+ parentType: remoteType,
+ functionName: "MyCustomFunction",
+ moduleName: "MyModule.dll",
+ offset: 0x1234, // Offset from module base address
+ returnType: typeof(int),
+ parameterTypes: new[] { typeof(int), typeof(float) }
+);
+
+// After registration, the function can be invoked like any other remote method
+dynamic dynamicObj = remoteObject.Dynamify();
+int result = dynamicObj.MyCustomFunction(42, 3.14f);
+```
+
+**Notes:**
+- This feature is only available for unmanaged (C++) targets
+- You need to know the module name and offset where the function is located
+- The function will be added to the type's method list and can be invoked normally
+- Parameter and return types should be specified as .NET types
+
## TODOs
1. Static members
2. Document "Reflection API" (RemoteType, RemoteMethodInfo, ... )
diff --git a/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs b/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
new file mode 100644
index 0000000..010baea
--- /dev/null
+++ b/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
@@ -0,0 +1,82 @@
+using ScubaDiver.API.Interactions;
+
+namespace ScubaDiver.API.Tests
+{
+ [TestFixture]
+ public class RegisterCustomFunctionTests
+ {
+ [Test]
+ public void RegisterCustomFunctionRequest_ValidData_SetsPropertiesCorrectly()
+ {
+ // Arrange
+ var request = new RegisterCustomFunctionRequest
+ {
+ ParentTypeFullName = "MyNamespace::MyClass",
+ ParentAssembly = "MyModule.dll",
+ FunctionName = "MyCustomFunction",
+ ModuleName = "MyModule.dll",
+ Offset = 0x1234,
+ ReturnTypeFullName = "int",
+ ReturnTypeAssembly = "System.Private.CoreLib",
+ Parameters = new List
+ {
+ new RegisterCustomFunctionRequest.ParameterTypeInfo
+ {
+ Name = "param1",
+ TypeFullName = "int",
+ Assembly = "System.Private.CoreLib"
+ },
+ new RegisterCustomFunctionRequest.ParameterTypeInfo
+ {
+ Name = "param2",
+ TypeFullName = "float",
+ Assembly = "System.Private.CoreLib"
+ }
+ }
+ };
+
+ // Assert
+ Assert.That(request.ParentTypeFullName, Is.EqualTo("MyNamespace::MyClass"));
+ Assert.That(request.ParentAssembly, Is.EqualTo("MyModule.dll"));
+ Assert.That(request.FunctionName, Is.EqualTo("MyCustomFunction"));
+ Assert.That(request.ModuleName, Is.EqualTo("MyModule.dll"));
+ Assert.That(request.Offset, Is.EqualTo(0x1234));
+ Assert.That(request.ReturnTypeFullName, Is.EqualTo("int"));
+ Assert.That(request.Parameters.Count, Is.EqualTo(2));
+ Assert.That(request.Parameters[0].Name, Is.EqualTo("param1"));
+ Assert.That(request.Parameters[0].TypeFullName, Is.EqualTo("int"));
+ Assert.That(request.Parameters[1].Name, Is.EqualTo("param2"));
+ Assert.That(request.Parameters[1].TypeFullName, Is.EqualTo("float"));
+ }
+
+ [Test]
+ public void RegisterCustomFunctionResponse_Success_SetsPropertiesCorrectly()
+ {
+ // Arrange
+ var response = new RegisterCustomFunctionResponse
+ {
+ Success = true,
+ ErrorMessage = null
+ };
+
+ // Assert
+ Assert.That(response.Success, Is.True);
+ Assert.That(response.ErrorMessage, Is.Null);
+ }
+
+ [Test]
+ public void RegisterCustomFunctionResponse_Failure_SetsPropertiesCorrectly()
+ {
+ // Arrange
+ var response = new RegisterCustomFunctionResponse
+ {
+ Success = false,
+ ErrorMessage = "Failed to register custom function"
+ };
+
+ // Assert
+ Assert.That(response.Success, Is.False);
+ Assert.That(response.ErrorMessage, Is.EqualTo("Failed to register custom function"));
+ }
+ }
+}
From 444720c278668de50852b0cb76b047fdda8c8415 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 16:26:36 +0000
Subject: [PATCH 04/12] Add missing using statement for Interactions namespace
---
src/RemoteNET/UnmanagedRemoteApp.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/RemoteNET/UnmanagedRemoteApp.cs b/src/RemoteNET/UnmanagedRemoteApp.cs
index 8cea0fc..64e2061 100644
--- a/src/RemoteNET/UnmanagedRemoteApp.cs
+++ b/src/RemoteNET/UnmanagedRemoteApp.cs
@@ -6,6 +6,7 @@
using RemoteNET.Internal;
using RemoteNET.RttiReflection;
using ScubaDiver.API;
+using ScubaDiver.API.Interactions;
using ScubaDiver.API.Interactions.Dumps;
using ScubaDiver.API.Utils;
From 437a0ad755ce8057a2add745379cda37c5afbe4d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 16:27:03 +0000
Subject: [PATCH 05/12] Update gitignore to exclude build artifacts
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index de1d236..2828314 100644
--- a/.gitignore
+++ b/.gitignore
@@ -340,3 +340,4 @@ src/RemoteNET/publish.ps1
src/detours_build/
src/ConsoleApp/
+src/RemoteNET/Resources/
From a78fbbdc86b223595e405e8d9770a4302e176457 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 16:31:06 +0000
Subject: [PATCH 06/12] Address code review feedback - fix exception handling,
overflow check, and missing using
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
.../RegisterCustomFunctionTests.cs | 1 +
.../MsvcPrimitives/CustomUndecoratedFunction.cs | 10 ++++++++--
src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs | 9 +++++++--
3 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs b/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
index 010baea..a16a116 100644
--- a/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
+++ b/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
@@ -1,4 +1,5 @@
using ScubaDiver.API.Interactions;
+using System.Collections.Generic;
namespace ScubaDiver.API.Tests
{
diff --git a/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs b/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
index 95e49e2..22ab954 100644
--- a/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
+++ b/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
@@ -22,9 +22,15 @@ public CustomUndecoratedFunction(
: base(functionName, functionName, functionName, argTypes?.Length)
{
_module = new ModuleInfo { Name = moduleName, BaseAddress = baseAddress };
- _address = baseAddress + offset;
+
+ // Check for potential overflow when adding baseAddress and offset
+ checked
+ {
+ _address = baseAddress + offset;
+ }
+
_retType = returnType;
- _argTypes = argTypes ?? new string[0];
+ _argTypes = argTypes ?? Array.Empty();
}
public override ModuleInfo Module => _module;
diff --git a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
index fd57305..dcbebee 100644
--- a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
+++ b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
@@ -509,7 +509,10 @@ public bool RegisterCustomFunction(
if (modules.Count == 0)
return false;
- UndecoratedModule targetModule = modules.First();
+ UndecoratedModule targetModule = modules.FirstOrDefault();
+ if (targetModule == null)
+ return false;
+
nuint moduleBaseAddress = targetModule.ModuleInfo.BaseAddress;
// Create a custom undecorated function
@@ -527,8 +530,10 @@ public bool RegisterCustomFunction(
return true;
}
- catch
+ catch (Exception ex)
{
+ // Log the exception for debugging purposes
+ Logger.Debug($"[MsvcTypesManager][RegisterCustomFunction] Failed to register custom function. Exception: {ex}");
return false;
}
}
From 44745b7140b95a843c0a5e4990c1166931956b27 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 16:33:04 +0000
Subject: [PATCH 07/12] Final code review fixes - use Array.Empty and direct
indexing
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
src/ScubaDiver/MsvcDiver.cs | 2 +-
src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs | 6 ++----
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/ScubaDiver/MsvcDiver.cs b/src/ScubaDiver/MsvcDiver.cs
index 227f561..9b83c09 100644
--- a/src/ScubaDiver/MsvcDiver.cs
+++ b/src/ScubaDiver/MsvcDiver.cs
@@ -884,7 +884,7 @@ protected override string MakeRegisterCustomFunctionResponse(ScubaDiverMessage a
try
{
// Extract parameter type names from the request
- string[] argTypeFullNames = request.Parameters?.Select(p => p.TypeFullName).ToArray() ?? new string[0];
+ string[] argTypeFullNames = request.Parameters?.Select(p => p.TypeFullName).ToArray() ?? Array.Empty();
bool success = _typesManager.RegisterCustomFunction(
request.ParentTypeFullName,
diff --git a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
index dcbebee..68038f7 100644
--- a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
+++ b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
@@ -509,10 +509,8 @@ public bool RegisterCustomFunction(
if (modules.Count == 0)
return false;
- UndecoratedModule targetModule = modules.FirstOrDefault();
- if (targetModule == null)
- return false;
-
+ // Since we've verified the list is not empty, we can safely access the first element
+ UndecoratedModule targetModule = modules[0];
nuint moduleBaseAddress = targetModule.ModuleInfo.BaseAddress;
// Create a custom undecorated function
From cd3cef289259d6d244c2b13f509d01e98ba6711c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 17:43:54 +0000
Subject: [PATCH 08/12] Update custom function registration to modify existing
RemoteRttiType instances
- Modified response to return TypeDump.TypeMethod instead of just success/failure
- Updated UnmanagedRemoteApp to verify Type is RemoteRttiType and add method to it
- Use RttiTypesFactory.AddFunctionImpl to ensure consistent method creation
- Updated DiverCommunicator with overload to return method dump
- Updated tests to validate RegisteredMethod property
- Updated documentation to clarify that existing type instances are updated
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
README.md | 8 +++--
src/RemoteNET/UnmanagedRemoteApp.cs | 29 ++++++++++++---
.../RegisterCustomFunctionTests.cs | 15 ++++++--
src/ScubaDiver.API/DiverCommunicator.cs | 10 ++++++
.../RegisterCustomFunctionResponse.cs | 7 ++++
src/ScubaDiver/MsvcDiver.cs | 35 +++++++++++++++++--
6 files changed, 94 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 9df8852..ecaf67d 100644
--- a/README.md
+++ b/README.md
@@ -147,11 +147,13 @@ The limitations:
For unmanaged (MSVC C++) targets, you can register custom functions that aren't automatically discovered by the RTTI scanner.
This is useful when you know the address of a function in the target process and want to call it through RemoteNET.
+**Important:** The type must be a `RemoteRttiType` instance that was fetched from the remote process. Any existing instances of that type will be automatically updated with the new method.
+
```C#
// Connect to an unmanaged target
UnmanagedRemoteApp unmanagedApp = (UnmanagedRemoteApp)RemoteAppFactory.Connect("MyNativeTarget.exe", RuntimeType.Unmanaged);
-// Get a remote type
+// Get a remote type (this creates a RemoteRttiType)
Type remoteType = unmanagedApp.GetRemoteType("MyNamespace::MyClass", "MyModule.dll");
// Register a custom function on the type
@@ -165,14 +167,16 @@ bool success = unmanagedApp.RegisterCustomFunction(
);
// After registration, the function can be invoked like any other remote method
+// All existing instances of this type will have the new method available
dynamic dynamicObj = remoteObject.Dynamify();
int result = dynamicObj.MyCustomFunction(42, 3.14f);
```
**Notes:**
- This feature is only available for unmanaged (C++) targets
+- The parent type must be a RemoteRttiType (obtained via `UnmanagedRemoteApp.GetRemoteType()`)
- You need to know the module name and offset where the function is located
-- The function will be added to the type's method list and can be invoked normally
+- The function will be added to the type's method list and available on all instances
- Parameter and return types should be specified as .NET types
## TODOs
diff --git a/src/RemoteNET/UnmanagedRemoteApp.cs b/src/RemoteNET/UnmanagedRemoteApp.cs
index 64e2061..eea78e1 100644
--- a/src/RemoteNET/UnmanagedRemoteApp.cs
+++ b/src/RemoteNET/UnmanagedRemoteApp.cs
@@ -223,7 +223,7 @@ public override bool InjectDll(string path)
///
/// Registers a custom function on a remote type for unmanaged targets
///
- /// The type to add the function to
+ /// The type to add the function to (must be a RemoteRttiType)
/// Name of the function
/// Module name where the function is located (e.g., "MyModule.dll")
/// Offset within the module where the function is located
@@ -245,10 +245,16 @@ public bool RegisterCustomFunction(
if (string.IsNullOrEmpty(moduleName))
throw new ArgumentException("Module name cannot be null or empty", nameof(moduleName));
+ // Verify the type is a RemoteRttiType
+ if (!(parentType is RemoteRttiType rttiType))
+ {
+ throw new ArgumentException("The parent type must be a RemoteRttiType. Only remote RTTI types created by UnmanagedRemoteApp can have custom functions registered.", nameof(parentType));
+ }
+
var request = new RegisterCustomFunctionRequest
{
- ParentTypeFullName = parentType.FullName,
- ParentAssembly = parentType.Assembly?.GetName()?.Name,
+ ParentTypeFullName = rttiType.Namespace + "::" + rttiType.Name, // Use namespace::name format for RTTI types
+ ParentAssembly = rttiType.Assembly?.GetName()?.Name,
FunctionName = functionName,
ModuleName = moduleName,
Offset = offset,
@@ -262,7 +268,22 @@ public bool RegisterCustomFunction(
}).ToList() ?? new List()
};
- return _unmanagedCommunicator.RegisterCustomFunction(request);
+ bool success = _unmanagedCommunicator.RegisterCustomFunction(request, out var methodDump);
+
+ if (success && methodDump != null)
+ {
+ // Create a TypeDump for the parent type to pass to AddFunctionImpl
+ TypeDump parentTypeDump = new TypeDump
+ {
+ Assembly = rttiType.Assembly?.GetName()?.Name,
+ FullTypeName = rttiType.Namespace + "::" + rttiType.Name
+ };
+
+ // Use the existing factory method to add the function to the RemoteRttiType
+ RttiTypesFactory.AddFunctionImpl(this, parentTypeDump, methodDump, rttiType, areConstructors: false);
+ }
+
+ return success;
}
//
diff --git a/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs b/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
index a16a116..47a6803 100644
--- a/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
+++ b/src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
@@ -1,4 +1,5 @@
using ScubaDiver.API.Interactions;
+using ScubaDiver.API.Interactions.Dumps;
using System.Collections.Generic;
namespace ScubaDiver.API.Tests
@@ -54,15 +55,23 @@ public void RegisterCustomFunctionRequest_ValidData_SetsPropertiesCorrectly()
public void RegisterCustomFunctionResponse_Success_SetsPropertiesCorrectly()
{
// Arrange
+ var methodDump = new TypeDump.TypeMethod
+ {
+ Name = "TestMethod",
+ ReturnTypeFullName = "int"
+ };
var response = new RegisterCustomFunctionResponse
{
Success = true,
- ErrorMessage = null
+ ErrorMessage = null,
+ RegisteredMethod = methodDump
};
// Assert
Assert.That(response.Success, Is.True);
Assert.That(response.ErrorMessage, Is.Null);
+ Assert.That(response.RegisteredMethod, Is.Not.Null);
+ Assert.That(response.RegisteredMethod.Name, Is.EqualTo("TestMethod"));
}
[Test]
@@ -72,12 +81,14 @@ public void RegisterCustomFunctionResponse_Failure_SetsPropertiesCorrectly()
var response = new RegisterCustomFunctionResponse
{
Success = false,
- ErrorMessage = "Failed to register custom function"
+ ErrorMessage = "Failed to register custom function",
+ RegisteredMethod = null
};
// Assert
Assert.That(response.Success, Is.False);
Assert.That(response.ErrorMessage, Is.EqualTo("Failed to register custom function"));
+ Assert.That(response.RegisteredMethod, Is.Null);
}
}
}
diff --git a/src/ScubaDiver.API/DiverCommunicator.cs b/src/ScubaDiver.API/DiverCommunicator.cs
index a92146e..df244c8 100644
--- a/src/ScubaDiver.API/DiverCommunicator.cs
+++ b/src/ScubaDiver.API/DiverCommunicator.cs
@@ -539,12 +539,22 @@ public void UnhookMethod(LocalHookCallback callback)
public bool RegisterCustomFunction(RegisterCustomFunctionRequest request)
{
+ return RegisterCustomFunction(request, out _);
+ }
+
+ public bool RegisterCustomFunction(RegisterCustomFunctionRequest request, out TypeDump.TypeMethod methodDump)
+ {
+ methodDump = null;
var requestJsonBody = JsonConvert.SerializeObject(request);
var resJson = SendRequest("register_custom_function", null, requestJsonBody);
try
{
RegisterCustomFunctionResponse response = JsonConvert.DeserializeObject(resJson, _withErrors);
+ if (response?.Success == true)
+ {
+ methodDump = response.RegisteredMethod;
+ }
return response?.Success ?? false;
}
catch (Exception ex)
diff --git a/src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs b/src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs
index 6fa0bc0..2c7ebca 100644
--- a/src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs
+++ b/src/ScubaDiver.API/Interactions/RegisterCustomFunctionResponse.cs
@@ -1,3 +1,5 @@
+using ScubaDiver.API.Interactions.Dumps;
+
namespace ScubaDiver.API.Interactions
{
///
@@ -14,5 +16,10 @@ public class RegisterCustomFunctionResponse
/// Error message if registration failed
///
public string ErrorMessage { get; set; }
+
+ ///
+ /// The method dump for the registered function (when successful)
+ ///
+ public TypeDump.TypeMethod RegisteredMethod { get; set; }
}
}
diff --git a/src/ScubaDiver/MsvcDiver.cs b/src/ScubaDiver/MsvcDiver.cs
index 9b83c09..f94f71c 100644
--- a/src/ScubaDiver/MsvcDiver.cs
+++ b/src/ScubaDiver/MsvcDiver.cs
@@ -895,10 +895,41 @@ protected override string MakeRegisterCustomFunctionResponse(ScubaDiverMessage a
request.ReturnTypeFullName,
argTypeFullNames);
+ if (!success)
+ {
+ RegisterCustomFunctionResponse failResponse = new RegisterCustomFunctionResponse
+ {
+ Success = false,
+ ErrorMessage = "Failed to register custom function"
+ };
+ return JsonConvert.SerializeObject(failResponse);
+ }
+
+ // Create a TypeMethod dump from the registered function
+ TypeDump.TypeMethod methodDump = new TypeDump.TypeMethod
+ {
+ Name = request.FunctionName,
+ DecoratedName = request.FunctionName,
+ UndecoratedFullName = request.FunctionName,
+ ReturnTypeFullName = request.ReturnTypeFullName,
+ ReturnTypeAssembly = request.ReturnTypeAssembly,
+ ReturnTypeName = request.ReturnTypeFullName,
+ Visibility = "Public",
+ Attributes = 0,
+ Parameters = request.Parameters?.Select((p, idx) => new TypeDump.TypeMethod.MethodParameter
+ {
+ Name = p.Name ?? $"a{idx}",
+ FullTypeName = p.TypeFullName,
+ TypeName = p.TypeFullName,
+ Assembly = p.Assembly
+ }).ToList() ?? new List()
+ };
+
RegisterCustomFunctionResponse response = new RegisterCustomFunctionResponse
{
- Success = success,
- ErrorMessage = success ? null : "Failed to register custom function"
+ Success = true,
+ ErrorMessage = null,
+ RegisteredMethod = methodDump
};
return JsonConvert.SerializeObject(response);
From 6f0bcf3f02eb5c04246de0d869e65a43a3e0c512 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 18:29:59 +0000
Subject: [PATCH 09/12] Change RegisterCustomFunction to return MethodInfo and
fix compilation errors
- Changed return type from bool to MethodInfo (returns null on error)
- Added using ScubaDiver.API.Utils to MsvcTypesManager for Filter class
- Added using System.Reflection to UnmanagedRemoteApp
- Method now returns the newly registered MethodInfo as a convenience
- Fixed compilation errors: Filter and CustomUndecoratedFunction now accessible
- Updated README with new return type and null-check example
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
README.md | 16 ++++++----
src/RemoteNET/UnmanagedRemoteApp.cs | 32 +++++++++++--------
.../MsvcPrimitives/MsvcTypesManager.cs | 1 +
3 files changed, 30 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index ecaf67d..9d1d83f 100644
--- a/README.md
+++ b/README.md
@@ -156,8 +156,8 @@ UnmanagedRemoteApp unmanagedApp = (UnmanagedRemoteApp)RemoteAppFactory.Connect("
// Get a remote type (this creates a RemoteRttiType)
Type remoteType = unmanagedApp.GetRemoteType("MyNamespace::MyClass", "MyModule.dll");
-// Register a custom function on the type
-bool success = unmanagedApp.RegisterCustomFunction(
+// Register a custom function on the type - returns the MethodInfo or null on error
+MethodInfo customMethod = unmanagedApp.RegisterCustomFunction(
parentType: remoteType,
functionName: "MyCustomFunction",
moduleName: "MyModule.dll",
@@ -166,10 +166,13 @@ bool success = unmanagedApp.RegisterCustomFunction(
parameterTypes: new[] { typeof(int), typeof(float) }
);
-// After registration, the function can be invoked like any other remote method
-// All existing instances of this type will have the new method available
-dynamic dynamicObj = remoteObject.Dynamify();
-int result = dynamicObj.MyCustomFunction(42, 3.14f);
+if (customMethod != null)
+{
+ // After registration, the function can be invoked like any other remote method
+ // All existing instances of this type will have the new method available
+ dynamic dynamicObj = remoteObject.Dynamify();
+ int result = dynamicObj.MyCustomFunction(42, 3.14f);
+}
```
**Notes:**
@@ -178,6 +181,7 @@ int result = dynamicObj.MyCustomFunction(42, 3.14f);
- You need to know the module name and offset where the function is located
- The function will be added to the type's method list and available on all instances
- Parameter and return types should be specified as .NET types
+- Returns the `MethodInfo` for the registered method, or `null` if registration fails
## TODOs
1. Static members
diff --git a/src/RemoteNET/UnmanagedRemoteApp.cs b/src/RemoteNET/UnmanagedRemoteApp.cs
index eea78e1..3bdf717 100644
--- a/src/RemoteNET/UnmanagedRemoteApp.cs
+++ b/src/RemoteNET/UnmanagedRemoteApp.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Reflection;
using RemoteNET.Common;
using RemoteNET.Internal;
using RemoteNET.RttiReflection;
@@ -229,8 +230,8 @@ public override bool InjectDll(string path)
/// Offset within the module where the function is located
/// Return type of the function
/// Parameter types of the function
- /// True if registration was successful, false otherwise
- public bool RegisterCustomFunction(
+ /// The MethodInfo for the registered function, or null if registration failed
+ public MethodInfo RegisterCustomFunction(
Type parentType,
string functionName,
string moduleName,
@@ -270,20 +271,25 @@ public bool RegisterCustomFunction(
bool success = _unmanagedCommunicator.RegisterCustomFunction(request, out var methodDump);
- if (success && methodDump != null)
+ if (!success || methodDump == null)
{
- // Create a TypeDump for the parent type to pass to AddFunctionImpl
- TypeDump parentTypeDump = new TypeDump
- {
- Assembly = rttiType.Assembly?.GetName()?.Name,
- FullTypeName = rttiType.Namespace + "::" + rttiType.Name
- };
-
- // Use the existing factory method to add the function to the RemoteRttiType
- RttiTypesFactory.AddFunctionImpl(this, parentTypeDump, methodDump, rttiType, areConstructors: false);
+ return null;
}
- return success;
+ // Create a TypeDump for the parent type to pass to AddFunctionImpl
+ TypeDump parentTypeDump = new TypeDump
+ {
+ Assembly = rttiType.Assembly?.GetName()?.Name,
+ FullTypeName = rttiType.Namespace + "::" + rttiType.Name
+ };
+
+ // Use the existing factory method to add the function to the RemoteRttiType
+ // AddFunctionImpl will add the method to rttiType's internal methods list
+ RttiTypesFactory.AddFunctionImpl(this, parentTypeDump, methodDump, rttiType, areConstructors: false);
+
+ // Find and return the newly added method
+ var methods = rttiType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
+ return methods.FirstOrDefault(m => m.Name == functionName);
}
//
diff --git a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
index 68038f7..9d7296f 100644
--- a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
+++ b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
@@ -1,4 +1,5 @@
using NtApiDotNet.Win32;
+using ScubaDiver.API.Utils;
using ScubaDiver.Rtti;
using System;
using System.Collections.Generic;
From caa5782d55bf5faf0e70f8a434222ac15e3b060a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 18:53:39 +0000
Subject: [PATCH 10/12] Make AddFunctionImpl return MethodInfo to avoid O(n)
search
- Changed AddFunctionImpl signature to return MethodInfo instead of void
- Returns the newly created RemoteRttiMethodInfo or RemoteRttiConstructorInfo
- UnmanagedRemoteApp now directly returns the MethodInfo from AddFunctionImpl
- Removed inefficient O(n) search through all methods by name
- More efficient and safer since it returns the exact method that was created
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
.../Internal/Reflection/Rtti/RttiTypesFactory.cs | 4 +++-
src/RemoteNET/UnmanagedRemoteApp.cs | 8 ++------
2 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs b/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs
index 72abf21..6ad3458 100644
--- a/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs
+++ b/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs
@@ -168,7 +168,7 @@ private static void AddGroupOfFunctions(RemoteApp app, TypeDump typeDump, List CreateTypeFactory(string namespaceAndTypeName, string moduleName)
diff --git a/src/RemoteNET/UnmanagedRemoteApp.cs b/src/RemoteNET/UnmanagedRemoteApp.cs
index 3bdf717..b67c3da 100644
--- a/src/RemoteNET/UnmanagedRemoteApp.cs
+++ b/src/RemoteNET/UnmanagedRemoteApp.cs
@@ -284,12 +284,8 @@ public MethodInfo RegisterCustomFunction(
};
// Use the existing factory method to add the function to the RemoteRttiType
- // AddFunctionImpl will add the method to rttiType's internal methods list
- RttiTypesFactory.AddFunctionImpl(this, parentTypeDump, methodDump, rttiType, areConstructors: false);
-
- // Find and return the newly added method
- var methods = rttiType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
- return methods.FirstOrDefault(m => m.Name == functionName);
+ // AddFunctionImpl returns the newly created MethodInfo
+ return RttiTypesFactory.AddFunctionImpl(this, parentTypeDump, methodDump, rttiType, areConstructors: false);
}
//
From 4aa39db8e69a3c27eadbaaab0781a6a1759d64ba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 2 Nov 2025 19:07:27 +0000
Subject: [PATCH 11/12] Add CustomUndecoratedFunction.cs reference to
ScubaDiver project files
- Added Compile reference to project_net6_x64/ScubaDiver_Net6_x64.csproj
- Added Compile reference to project_net6_x86/ScubaDiver_Net6_x86.csproj
- Fixes build error: CustomUndecoratedFunction type not found
- Projects use explicit file references, so new files must be added manually
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
src/ScubaDiver/project_net6_x64/ScubaDiver_Net6_x64.csproj | 1 +
src/ScubaDiver/project_net6_x86/ScubaDiver_Net6_x86.csproj | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/ScubaDiver/project_net6_x64/ScubaDiver_Net6_x64.csproj b/src/ScubaDiver/project_net6_x64/ScubaDiver_Net6_x64.csproj
index cb1445b..877fc3b 100644
--- a/src/ScubaDiver/project_net6_x64/ScubaDiver_Net6_x64.csproj
+++ b/src/ScubaDiver/project_net6_x64/ScubaDiver_Net6_x64.csproj
@@ -49,6 +49,7 @@
+
diff --git a/src/ScubaDiver/project_net6_x86/ScubaDiver_Net6_x86.csproj b/src/ScubaDiver/project_net6_x86/ScubaDiver_Net6_x86.csproj
index c99e5cd..fc1a424 100644
--- a/src/ScubaDiver/project_net6_x86/ScubaDiver_Net6_x86.csproj
+++ b/src/ScubaDiver/project_net6_x86/ScubaDiver_Net6_x86.csproj
@@ -49,6 +49,7 @@
+
From 2832563169f3a75b2a0ab5e3657633e928452fe4 Mon Sep 17 00:00:00 2001
From: RemoteNet
Date: Fri, 28 Nov 2025 17:32:18 +0200
Subject: [PATCH 12/12] Changes to MSVC/RTTI Type system
---
src/RemoteNET.Tests/RttiTypesFactoryTests.cs | 6 +--
.../Reflection/Rtti/RttiTypesFactory.cs | 10 ++--
src/RemoteNET/UnmanagedRemoteApp.cs | 49 +++++++++++++++----
src/ScubaDiver/DiverBase.cs | 7 +++
src/ScubaDiver/MsvcDiver.cs | 6 +--
.../CustomUndecoratedFunction.cs | 9 ++--
.../MsvcPrimitives/MsvcTypesManager.cs | 10 ++--
7 files changed, 66 insertions(+), 31 deletions(-)
diff --git a/src/RemoteNET.Tests/RttiTypesFactoryTests.cs b/src/RemoteNET.Tests/RttiTypesFactoryTests.cs
index a2bce320..26b3527 100644
--- a/src/RemoteNET.Tests/RttiTypesFactoryTests.cs
+++ b/src/RemoteNET.Tests/RttiTypesFactoryTests.cs
@@ -260,7 +260,7 @@ public void AddFunctionImpl_DifferentDeclaringClassOnFunc_DifferentDeclaringType
RemoteApp? fakeApp = new FakeRemoteApp();
// Act
- RttiTypesFactory.AddFunctionImpl(fakeApp, typeDump, func, childType, false);
+ RttiTypesFactory.AddFunctionImpl(fakeApp, typeDump.Assembly, func, childType, false);
// Assert
MethodInfo? method = childType.GetMethods().Single();
@@ -291,7 +291,7 @@ public void AddFunctionImpl_SameDeclaringClassOnFunc_SameDeclaringType()
RemoteApp? fakeApp = new FakeRemoteApp();
// Act
- RttiTypesFactory.AddFunctionImpl(fakeApp, typeDump, func, childType, false);
+ RttiTypesFactory.AddFunctionImpl(fakeApp, typeDump.Assembly, func, childType, false);
// Assert
MethodInfo? method = childType.GetMethods().Single();
@@ -345,7 +345,7 @@ public void UndecoratingConstRef_ParseType_NoMethod()
RemoteApp? fakeApp = new FakeRemoteApp();
// Act
- RttiTypesFactory.AddFunctionImpl(fakeApp, typeDump, func, childType, false);
+ RttiTypesFactory.AddFunctionImpl(fakeApp, typeDump.Assembly, func, childType, false);
// Assert
// Expecting `AddFunctionImpl` to NOT add that function (not supported yet)
diff --git a/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs b/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs
index 6ad3458..d383e5c 100644
--- a/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs
+++ b/src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs
@@ -164,18 +164,16 @@ private static void AddGroupOfFunctions(RemoteApp app, TypeDump typeDump, List parameters = new List(func.Parameters.Count);
int i = 1;
foreach (TypeDump.TypeMethod.MethodParameter restarizedParameter in func.Parameters)
@@ -255,7 +253,7 @@ Lazy CreateTypeFactory(string namespaceAndTypeName, string moduleName)
if (_shittyCache.TryGetValue(namespaceAndTypeName, out resultType))
return resultType;
- resultType = RttiTypesResolver.Instance.Resolve(typeDump.Assembly, $"{typeDump.Assembly}!{namespaceAndTypeName}");
+ resultType = RttiTypesResolver.Instance.Resolve(moduleName, $"{moduleName}!{namespaceAndTypeName}");
if (resultType != null)
return resultType;
@@ -284,7 +282,7 @@ Lazy CreateTypeFactory(string namespaceAndTypeName, string moduleName)
// Prefer any matches in the existing assembly
var paramTypeInSameAssembly =
- possibleParamTypes.Where(t => t.Assembly == typeDump.Assembly).ToArray();
+ possibleParamTypes.Where(t => t.Assembly == moduleName).ToArray();
if (paramTypeInSameAssembly.Length > 0)
{
if (paramTypeInSameAssembly.Length > 1)
diff --git a/src/RemoteNET/UnmanagedRemoteApp.cs b/src/RemoteNET/UnmanagedRemoteApp.cs
index b67c3da..f9c2296 100644
--- a/src/RemoteNET/UnmanagedRemoteApp.cs
+++ b/src/RemoteNET/UnmanagedRemoteApp.cs
@@ -252,6 +252,42 @@ public MethodInfo RegisterCustomFunction(
throw new ArgumentException("The parent type must be a RemoteRttiType. Only remote RTTI types created by UnmanagedRemoteApp can have custom functions registered.", nameof(parentType));
}
+ static RegisterCustomFunctionRequest.ParameterTypeInfo CreateParamInfo(Type pt, int idx)
+ {
+ var typeName = pt?.FullName ?? "void";
+ var assembly = pt.Assembly?.GetName()?.Name;
+
+ if (pt.IsPrimitive)
+ {
+ typeName = pt.Name;
+ assembly = "mscorlib";
+
+ if (pt == typeof(ulong))
+ {
+ typeName = "ulong";
+ }
+ }
+
+ return new RegisterCustomFunctionRequest.ParameterTypeInfo
+ {
+ Name = $"param{idx}",
+ TypeFullName = typeName,
+ Assembly = assembly
+ };
+ }
+
+ var retTypeName = returnType?.FullName ?? "void";
+ var retTypeAssembly = returnType?.Assembly?.GetName()?.Name;
+ if (returnType != null && returnType.IsPrimitive)
+ {
+ retTypeName = returnType.Name;
+ retTypeAssembly = "mscorlib";
+ if (returnType == typeof(ulong))
+ {
+ retTypeName = "ulong";
+ }
+ }
+
var request = new RegisterCustomFunctionRequest
{
ParentTypeFullName = rttiType.Namespace + "::" + rttiType.Name, // Use namespace::name format for RTTI types
@@ -259,14 +295,9 @@ public MethodInfo RegisterCustomFunction(
FunctionName = functionName,
ModuleName = moduleName,
Offset = offset,
- ReturnTypeFullName = returnType?.FullName ?? "void",
- ReturnTypeAssembly = returnType?.Assembly?.GetName()?.Name,
- Parameters = parameterTypes?.Select((pt, idx) => new RegisterCustomFunctionRequest.ParameterTypeInfo
- {
- Name = $"param{idx}",
- TypeFullName = pt.FullName,
- Assembly = pt.Assembly?.GetName()?.Name
- }).ToList() ?? new List()
+ ReturnTypeFullName = retTypeName,
+ ReturnTypeAssembly = retTypeAssembly,
+ Parameters = parameterTypes?.Select(CreateParamInfo).ToList() ?? new List()
};
bool success = _unmanagedCommunicator.RegisterCustomFunction(request, out var methodDump);
@@ -285,7 +316,7 @@ public MethodInfo RegisterCustomFunction(
// Use the existing factory method to add the function to the RemoteRttiType
// AddFunctionImpl returns the newly created MethodInfo
- return RttiTypesFactory.AddFunctionImpl(this, parentTypeDump, methodDump, rttiType, areConstructors: false);
+ return RttiTypesFactory.AddFunctionImpl(this, parentTypeDump.Assembly, methodDump, rttiType, areConstructors: false) as MethodInfo;
}
//
diff --git a/src/ScubaDiver/DiverBase.cs b/src/ScubaDiver/DiverBase.cs
index 4c908e9..6bc0b1e 100644
--- a/src/ScubaDiver/DiverBase.cs
+++ b/src/ScubaDiver/DiverBase.cs
@@ -130,6 +130,13 @@ public string QuickError(string error, string stackTrace = null)
private void HandleDispatchedRequest(object obj, ScubaDiverMessage request)
{
+ // Check if the "debug" query parameter is set. If so, launch the debugger
+ if (request.QueryString.Get("debug") == "1")
+ {
+ Logger.Debug("[DiverBase] Debugging enabled");
+ Debugger.Launch();
+ }
+
Stopwatch sw = Stopwatch.StartNew();
string body;
if (_responseBodyCreators.TryGetValue(request.UrlAbsolutePath, out var respBodyGenerator))
diff --git a/src/ScubaDiver/MsvcDiver.cs b/src/ScubaDiver/MsvcDiver.cs
index f94f71c..f39325f 100644
--- a/src/ScubaDiver/MsvcDiver.cs
+++ b/src/ScubaDiver/MsvcDiver.cs
@@ -212,7 +212,6 @@ protected override ObjectOrRemoteAddress InvokeEventCallback(IPEndPoint callback
protected override HookResponse InvokeHookCallback(IPEndPoint callbacksEndpoint, int token, string stackTrace, object retValue, params object[] parameters)
{
- Logger.Debug($"[{nameof(MsvcDiver)}] InvokeHookCallback Entered. EndPoint: {callbacksEndpoint} Token: {token}");
ReverseCommunicator reverseCommunicator = new(callbacksEndpoint);
ObjectOrRemoteAddress[] remoteParams = new ObjectOrRemoteAddress[parameters.Length];
@@ -296,7 +295,7 @@ protected override string MakeTypesResponse(ScubaDiverMessage req)
string typeFilter = req.QueryString.Get("type_filter");
if (string.IsNullOrWhiteSpace(typeFilter))
- return QuickError("Missing parameter 'type_filter'");
+ return QuickError("Missing parameter 'type_filter'. Try this: /types?type_filter=*");
ParseFullTypeName(typeFilter, out var assemblyFilter, out typeFilter);
Predicate typeFilterPredicate = Filter.CreatePredicate(typeFilter);
@@ -339,7 +338,6 @@ protected override string MakeTypeResponse(ScubaDiverMessage req)
{
return QuickError("Failed to deserialize body");
}
- Logger.Debug($"[MsvcDiver][MakeTypeResponse] Resolving type Name: {request.Assembly} {request.TypeFullName} vftable: 0x{request.MethodTableAddress:x16}");
TypeDump dump;
if (request.MethodTableAddress != 0)
@@ -499,7 +497,6 @@ protected override string MakeObjectResponse(ScubaDiverMessage arg)
// Check if the object is already frozen
if (_freezer.IsFrozen(objAddr))
{
- Logger.Debug($"[MsvcDiver][MakeObjectResponse] Object at 0x{objAddr:X16} is already frozen.");
ObjectDump alreadyFrozenObjDump = new ObjectDump()
{
Type = fullTypeName,
@@ -556,7 +553,6 @@ protected override string MakeCreateObjectResponse(ScubaDiverMessage arg)
protected override string MakeInvokeResponse(ScubaDiverMessage arg)
{
- Console.WriteLine($"[{nameof(MsvcDiver)}] MakeInvokeResponse Entered (!)");
if (string.IsNullOrEmpty(arg.Body))
return QuickError("Missing body");
diff --git a/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs b/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
index 22ab954..00edad8 100644
--- a/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
+++ b/src/ScubaDiver/MsvcPrimitives/CustomUndecoratedFunction.cs
@@ -1,4 +1,6 @@
+using Microsoft.Diagnostics.Runtime.AbstractDac;
using ScubaDiver.Rtti;
+using System;
namespace ScubaDiver
{
@@ -13,20 +15,19 @@ public class CustomUndecoratedFunction : UndecoratedFunction
private readonly string[] _argTypes;
public CustomUndecoratedFunction(
- string moduleName,
- nuint baseAddress,
+ ModuleInfo module,
ulong offset,
string functionName,
string returnType,
string[] argTypes)
: base(functionName, functionName, functionName, argTypes?.Length)
{
- _module = new ModuleInfo { Name = moduleName, BaseAddress = baseAddress };
+ _module = module;
// Check for potential overflow when adding baseAddress and offset
checked
{
- _address = baseAddress + offset;
+ _address = module.BaseAddress + (nuint)offset;
}
_retType = returnType;
diff --git a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
index 9d7296f..8a27c02 100644
--- a/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
+++ b/src/ScubaDiver/MsvcPrimitives/MsvcTypesManager.cs
@@ -508,16 +508,18 @@ public bool RegisterCustomFunction(
// Find the module base address
List modules = GetUndecoratedModules(Filter.CreatePredicate(moduleName));
if (modules.Count == 0)
- return false;
+ {
+ modules = GetUndecoratedModules(Filter.CreatePredicate(moduleName + ".dll"));
+ if (modules.Count == 0)
+ return false;
+ }
// Since we've verified the list is not empty, we can safely access the first element
UndecoratedModule targetModule = modules[0];
- nuint moduleBaseAddress = targetModule.ModuleInfo.BaseAddress;
// Create a custom undecorated function
CustomUndecoratedFunction customFunc = new CustomUndecoratedFunction(
- moduleName,
- moduleBaseAddress,
+ targetModule.ModuleInfo,
offset,
functionName,
returnTypeFullName,