Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,4 @@ src/RemoteNET/publish.ps1
src/detours_build/

src/ConsoleApp/
src/RemoteNET/Resources/
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,46 @@ 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.

**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 (this creates a RemoteRttiType)
Type remoteType = unmanagedApp.GetRemoteType("MyNamespace::MyClass", "MyModule.dll");

// 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",
offset: 0x1234, // Offset from module base address
returnType: typeof(int),
parameterTypes: new[] { typeof(int), typeof(float) }
);

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:**
- 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 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
2. Document "Reflection API" (RemoteType, RemoteMethodInfo, ... )
Expand Down
6 changes: 3 additions & 3 deletions src/RemoteNET.Tests/RttiTypesFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions src/RemoteNET/Internal/Reflection/Rtti/RttiTypesFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,18 +164,16 @@ private static void AddGroupOfFunctions(RemoteApp app, TypeDump typeDump, List<T
{
foreach (TypeDump.TypeMethod func in functions)
{
AddFunctionImpl(app, typeDump, func, declaringType, areConstructors);
AddFunctionImpl(app, typeDump.Assembly, func, declaringType, areConstructors);
}
}

public static void AddFunctionImpl(RemoteApp app, TypeDump typeDump, TypeDump.TypeMethod func, RemoteRttiType declaringType, bool areConstructors)
public static MethodBase AddFunctionImpl(RemoteApp app, string moduleName, TypeDump.TypeMethod func, RemoteRttiType declaringType, bool areConstructors)
{
string mangledName = func.DecoratedName;
if (string.IsNullOrEmpty(mangledName))
mangledName = func.Name;

string moduleName = typeDump.Assembly;

List<LazyRemoteParameterResolver> parameters = new List<LazyRemoteParameterResolver>(func.Parameters.Count);
int i = 1;
foreach (TypeDump.TypeMethod.MethodParameter restarizedParameter in func.Parameters)
Expand Down Expand Up @@ -211,6 +209,7 @@ public static void AddFunctionImpl(RemoteApp app, TypeDump typeDump, TypeDump.Ty
RemoteRttiConstructorInfo ctorInfo =
new RemoteRttiConstructorInfo(declaringTypeResolver, parameters.ToArray());
declaringType.AddConstructor(ctorInfo);
return ctorInfo;
}
else
{
Expand All @@ -232,6 +231,7 @@ public static void AddFunctionImpl(RemoteApp app, TypeDump typeDump, TypeDump.Ty
new RemoteRttiMethodInfo(declaringTypeResolver, returnTypeResolver, func.Name, mangledName,
parameters.ToArray(), (MethodAttributes)func.Attributes);
declaringType.AddMethod(methodInfo);
return methodInfo;
}

Lazy<Type> CreateTypeFactory(string namespaceAndTypeName, string moduleName)
Expand All @@ -253,7 +253,7 @@ Lazy<Type> 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;

Expand Down Expand Up @@ -282,7 +282,7 @@ Lazy<Type> 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)
Expand Down
104 changes: 104 additions & 0 deletions src/RemoteNET/UnmanagedRemoteApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using RemoteNET.Common;
using RemoteNET.Internal;
using RemoteNET.RttiReflection;
using ScubaDiver.API;
using ScubaDiver.API.Interactions;
using ScubaDiver.API.Interactions.Dumps;
using ScubaDiver.API.Utils;

Expand Down Expand Up @@ -215,6 +217,108 @@ public override bool InjectDll(string path)
return res;
}

//
// Custom Functions
//

/// <summary>
/// Registers a custom function on a remote type for unmanaged targets
/// </summary>
/// <param name="parentType">The type to add the function to (must be a RemoteRttiType)</param>
/// <param name="functionName">Name of the function</param>
/// <param name="moduleName">Module name where the function is located (e.g., "MyModule.dll")</param>
/// <param name="offset">Offset within the module where the function is located</param>
/// <param name="returnType">Return type of the function</param>
/// <param name="parameterTypes">Parameter types of the function</param>
/// <returns>The MethodInfo for the registered function, or null if registration failed</returns>
public MethodInfo 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));

// 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));
}

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
ParentAssembly = rttiType.Assembly?.GetName()?.Name,
FunctionName = functionName,
ModuleName = moduleName,
Offset = offset,
ReturnTypeFullName = retTypeName,
ReturnTypeAssembly = retTypeAssembly,
Parameters = parameterTypes?.Select(CreateParamInfo).ToList() ?? new List<RegisterCustomFunctionRequest.ParameterTypeInfo>()
};

bool success = _unmanagedCommunicator.RegisterCustomFunction(request, out var methodDump);

if (!success || methodDump == null)
{
return 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
// AddFunctionImpl returns the newly created MethodInfo
return RttiTypesFactory.AddFunctionImpl(this, parentTypeDump.Assembly, methodDump, rttiType, areConstructors: false) as MethodInfo;
}

//
// IDisposable
//
Expand Down
94 changes: 94 additions & 0 deletions src/ScubaDiver.API.Tests/RegisterCustomFunctionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using ScubaDiver.API.Interactions;
using ScubaDiver.API.Interactions.Dumps;
using System.Collections.Generic;

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<RegisterCustomFunctionRequest.ParameterTypeInfo>
{
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 methodDump = new TypeDump.TypeMethod
{
Name = "TestMethod",
ReturnTypeFullName = "int"
};
var response = new RegisterCustomFunctionResponse
{
Success = true,
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]
public void RegisterCustomFunctionResponse_Failure_SetsPropertiesCorrectly()
{
// Arrange
var response = new RegisterCustomFunctionResponse
{
Success = false,
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);
}
}
}
26 changes: 26 additions & 0 deletions src/ScubaDiver.API/DiverCommunicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,32 @@ public void UnhookMethod(LocalHookCallback callback)

public delegate (bool voidReturnType, ObjectOrRemoteAddress res) LocalEventCallback(ObjectOrRemoteAddress[] args, ObjectOrRemoteAddress retVal);

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<RegisterCustomFunctionResponse>(resJson, _withErrors);
if (response?.Success == true)
{
methodDump = response.RegisteredMethod;
}
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)
Expand Down
Loading
Loading