Only this pageAll pages
Powered by GitBook
1 of 7

Ruptura

Loading...

Loading...

Tasks

Loading...

Loading...

Loading...

Loading...

Usage

Broadly speaking, Ruptura's packages can be split into three different categories of functionality:

You can pick and choose which of these packages you would like to use. For example, if you already have a working solution for injection, you may want to just use Vezel.Ruptura.Memory for its function hooking APIs, or Vezel.Ruptura.System for convenient Win32 API access. You could also use Vezel.Ruptura.Injection and Vezel.Ruptura.Hosting to inject your managed assembly, but choose to implement your own function hooking method.

Either way, simply use dotnet add package <name> to add the relevant package(s) to your project and start writing code. No further configuration is required.

and provide the ability to inject the CoreCLR runtime and managed assemblies into a target process.

provides function hooking, memory manipulation, and call tracing capabilities.

provides managed wrappers around operating system APIs and kernel objects.

Vezel.Ruptura.Injection
Vezel.Ruptura.Hosting
Vezel.Ruptura.Memory
Vezel.Ruptura.System

Function Hooking

The Vezel.Ruptura.Memory package provides an easy-to-use FunctionHook class that takes care of some common requirements such as placing the trampoline near the target function, atomically toggling an installed hook, associating state with the hook function, and avoiding certain cases of deadlock or stack overflow.

Usage looks like this:

var kernel32 = NativeLibrary.Load("kernel32.dll");

try
{
    using var manager = new PageCodeManager();

    var setThreadDescription = (delegate* unmanaged[Stdcall]<nint, char*, int>)NativeLibrary.GetExport(
        kernel32, "SetThreadDescription");
    var setThreadDescriptionHook = (delegate* unmanaged[Stdcall]<nint, char*, int>)&SetThreadDescriptionHook;

    using var hook = FunctionHook.Create(manager, setThreadDescription, setThreadDescriptionHook, "bar");

    hook.IsActive = true;

    fixed (char* ptr = "foo")
        _ = setThreadDescription(-1, ptr);
}
finally
{
    NativeLibrary.Free(kernel32);
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
static int SetThreadDescriptionHook(nint hThread, char* lpThreadDescription)
{
    Console.WriteLine(new string(lpThreadDescription) + FunctionHook.Current.State);

    return 0;
}

This example will print foobar to the console. There is a lot going on here, so let us go over each part:

  1. A PageCodeManager instance is created to manage code allocations. This is used by FunctionHook to allocate its internal trampoline near the target function.

  2. FunctionHook.Create() is called to hook SetThreadDescription with the SetThreadDescriptionHook method. The string object "bar" is passed as the hook's associated state so that it can be accessed in the hook method.

  3. A created hook is fully functional with all code emission and patching done, but calls will not actually be intercepted until the IsActive property is set to true, so that is then done.

  4. The function pointer for the target function is invoked with some dummy arguments. Since the hook is active, the SetThreadDescriptionHook method will be called.

  5. SetThreadDescriptionHook accesses the FunctionHook.Current property and retrieves its State property (i.e. the "bar" string passed earlier). It concatenates it with the string passed in as lpThreadDescription and prints the result ("foobar") to the console.

This example is fairly contrived, but you can hook almost any function, whether it comes from the operating system, a normal library, or an executable. Of course, you can also associate more useful state objects with the hook so that you can access your application's state.

It is important that the hook method matches the target function exactly in calling convention (cdecl, stdcall, etc), return type, and parameter types. Mismatches can result in unpredictable bugs and crashes. Also, managed exceptions thrown from within a hook method must be handled; native call frames cannot be correctly unwound by CoreCLR.

Hook Gate

Internally, FunctionHook uses a so-called hook gate as part of its trampoline to guard calls to the hook method. The most user-visible effects of this are:

  • The FunctionHook instance for the most recent hook method in the call stack can be accessed through the Current property.

  • Hook recursion does not invoke the hook method a second time, preventing common issues like deadlocks and stack overflows when calling arbitrary code in a hook.

  • Hooks are not invoked while the Windows loader lock is held since managed code is virtually impossible to run reliably when that is the case.

The API is used to retrieve a function pointer to the function.

NativeLibrary
SetThreadDescription

Call Tracing

When trying to understand closed-source binaries or collecting diagnostic data, it can be useful to capture a detailed call stack trace at any given point - especially from a function hook. The Vezel.Ruptura.Memory package provides the CallTrace API to do exactly that. For example:

Console.WriteLine(CallTrace.Capture());

This will print something like:

0x7ffe4cfa0b09: Void Program.<Main>$(String[] args) in tests.dll
0x7ffeacabe823: coreclr_shutdown_2+0x16683 in coreclr.dll+0x16e823
0x7ffeac99a133: <unknown> in coreclr.dll+0x4a133
0x7ffeaca2d710: <unknown> in coreclr.dll+0xdd710
0x7ffeaca2efb6: <unknown> in coreclr.dll+0xdefb6
0x7ffeaca2f749: <unknown> in coreclr.dll+0xdf749
0x7ffeaca2dcb9: <unknown> in coreclr.dll+0xddcb9
0x7ffeaca93bd1: coreclr_execute_assembly+0xe1 in coreclr.dll+0x143bd1
0x7fff31f69148: <unknown> in hostpolicy.dll+0x19148
0x7fff31f6941c: <unknown> in hostpolicy.dll+0x1941c
0x7fff31f69d17: corehost_main+0x107 in hostpolicy.dll+0x19d17
0x7fff3947b459: hostfxr_close+0xfb9 in hostfxr.dll+0xb459
0x7fff3947e4f6: hostfxr_close+0x4056 in hostfxr.dll+0xe4f6
0x7fff394807cf: hostfxr_close+0x632f in hostfxr.dll+0x107cf
0x7fff3947eb52: hostfxr_close+0x46b2 in hostfxr.dll+0xeb52
0x7fff394781cb: hostfxr_main_startupinfo+0xab in hostfxr.dll+0x81cb
0x7ff61aba255b: <unknown> in tests.exe+0x1255b
0x7ff61aba28cb: <unknown> in tests.exe+0x128cb
0x7ff61aba3d78: <unknown> in tests.exe+0x13d78
0x7fff79193fed: BaseThreadInitThunk+0x1d in kernel32.dll+0x13fed
0x7fff7a6142a8: RtlUserThreadStart+0x28 in ntdll.dll+0x42a8

Of course, the captured CallTrace object has plenty of details that you can inspect programmatically. For example, you could print a bit more information about the RIP, RSP, and RBP registers in each CallFrame:

unsafe
{
    foreach (CallFrame frame in CallTrace.Capture().Frames)
    {
        Console.WriteLine(frame);
        Console.WriteLine($"  rip=0x{(nuint)frame.IP:x} rsp=0x{(nuint)frame.SP:x} rbp=0x{(nuint)frame.FP:x}");
    }
}

You will now get:

0x7ffe4cf80b42: Void Program.<Main>$(String[] args) in tests.dll
  rip=0x7ffe4cf80b42 rsp=0xd61858e920 rbp=0xd61858e9f0
0x7ffeacabe823: coreclr_shutdown_2+0x16683 in coreclr.dll+0x16e823
  rip=0x7ffeacabe823 rsp=0xd61858ea00 rbp=0xd61858ea30
0x7ffeac99a133: <unknown> in coreclr.dll+0x4a133
  rip=0x7ffeac99a133 rsp=0xd61858ea40 rbp=0xd61858eb60
0x7ffeaca2d710: <unknown> in coreclr.dll+0xdd710
  rip=0x7ffeaca2d710 rsp=0xd61858eb70 rbp=0xd61858ec80
0x7ffeaca2efb6: <unknown> in coreclr.dll+0xdefb6
  rip=0x7ffeaca2efb6 rsp=0xd61858ec90 rbp=0xd61858ed30
0x7ffeaca2f749: <unknown> in coreclr.dll+0xdf749
  rip=0x7ffeaca2f749 rsp=0xd61858ed40 rbp=0xd61858f0c0
0x7ffeaca2dcb9: <unknown> in coreclr.dll+0xddcb9
  rip=0x7ffeaca2dcb9 rsp=0xd61858f0d0 rbp=0xd61858f200
0x7ffeaca93bd1: coreclr_execute_assembly+0xe1 in coreclr.dll+0x143bd1
  rip=0x7ffeaca93bd1 rsp=0xd61858f210 rbp=0xd61858f290
0x7fff31f69148: <unknown> in hostpolicy.dll+0x19148
  rip=0x7fff31f69148 rsp=0xd61858f2a0 rbp=0xd61858f420
0x7fff31f6941c: <unknown> in hostpolicy.dll+0x1941c
  rip=0x7fff31f6941c rsp=0xd61858f430 rbp=0xd61858f460
0x7fff31f69d17: corehost_main+0x107 in hostpolicy.dll+0x19d17
  rip=0x7fff31f69d17 rsp=0xd61858f470 rbp=0xd61858f610
0x7fff3947b459: hostfxr_close+0xfb9 in hostfxr.dll+0xb459
  rip=0x7fff3947b459 rsp=0xd61858f620 rbp=0xd61858f710
0x7fff3947e4f6: hostfxr_close+0x4056 in hostfxr.dll+0xe4f6
  rip=0x7fff3947e4f6 rsp=0xd61858f720 rbp=0xd61858f810
0x7fff394807cf: hostfxr_close+0x632f in hostfxr.dll+0x107cf
  rip=0x7fff394807cf rsp=0xd61858f820 rbp=0xd61858f8c0
0x7fff3947eb52: hostfxr_close+0x46b2 in hostfxr.dll+0xeb52
  rip=0x7fff3947eb52 rsp=0xd61858f8d0 rbp=0xd61858fa00
0x7fff394781cb: hostfxr_main_startupinfo+0xab in hostfxr.dll+0x81cb
  rip=0x7fff394781cb rsp=0xd61858fa10 rbp=0xd61858fb00
0x7ff7e62f255b: <unknown> in tests.exe+0x1255b
  rip=0x7ff7e62f255b rsp=0xd61858fb10 rbp=0xd61858fcd0
0x7ff7e62f28cb: <unknown> in tests.exe+0x128cb
  rip=0x7ff7e62f28cb rsp=0xd61858fce0 rbp=0xd61858fd00
0x7ff7e62f3d78: <unknown> in tests.exe+0x13d78
  rip=0x7ff7e62f3d78 rsp=0xd61858fd10 rbp=0xd61858fd40
0x7fff79193fed: BaseThreadInitThunk+0x1d in kernel32.dll+0x13fed
  rip=0x7fff79193fed rsp=0xd61858fd50 rbp=0xd61858fd70
0x7fff7a6142a8: RtlUserThreadStart+0x28 in ntdll.dll+0x42a8
  rip=0x7fff7a6142a8 rsp=0xd61858fd80 rbp=0xd61858fdf0

Symbolication

There is a CallTrace.Capture() overload that allows you to specify the CallFrameSymbolicator instances you would like to use in a call trace instead of or in addition to the aforementioned two. This allows you to implement your own symbolicators that can pull on whatever data you would like. For instance, you could symbolicate based on signature matching, or based on a symbol table manually constructed from reverse engineering.

Symbol Servers

For NativeCallFrameSymbolicator, it is worth noting that the DbgHelp library shipped with Windows does not have symbol server support (symsrv.dll). That is why the call traces above had rather poor, export-based symbolication for coreclr.dll and related libraries.

0x7ffe5a5e0b09: Void Program.<Main>$(String[] args) in tests.dll
0x7ffeba10e823: CallDescrWorkerInternal+0x83 in coreclr.dll+0x16e823
0x7ffeb9fea133: CallDescrWorkerWithHandler+0x56 in coreclr.dll+0x4a133 at D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp:67
0x7ffeb9fea133: MethodDescCallSite::CallTargetWorker+0x247 in coreclr.dll+0x4a133 at D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp:570
0x7ffeba07d710: MethodDescCallSite::Call+0xb in coreclr.dll+0xdd710 at D:\a\_work\1\s\src\coreclr\vm\callhelpers.h:458
0x7ffeba07d710: RunMainInternal+0x11c in coreclr.dll+0xdd710 at D:\a\_work\1\s\src\coreclr\vm\assembly.cpp:1354
0x7ffeba07efb6: RunMain+0xd2 in coreclr.dll+0xdefb6 at D:\a\_work\1\s\src\coreclr\vm\assembly.cpp:1425
0x7ffeba07f749: Assembly::ExecuteMainMethod+0x1f1 in coreclr.dll+0xdf749 at D:\a\_work\1\s\src\coreclr\vm\assembly.cpp:1543
0x7ffeba07dcb9: CorHost2::ExecuteAssembly+0x1d9 in coreclr.dll+0xddcb9 at D:\a\_work\1\s\src\coreclr\vm\corhost.cpp:360
0x7ffeba0e3bd1: coreclr_execute_assembly+0xe1 in coreclr.dll+0x143bd1 at D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp:430
0x7ffebaa19148: coreclr_t::execute_assembly+0x2a in hostpolicy.dll+0x19148 at D:\a\_work\1\s\src\native\corehost\hostpolicy\coreclr.cpp:89
0x7ffebaa19148: run_app_for_context+0x4e8 in hostpolicy.dll+0x19148 at D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp:255
0x7ffebaa1941c: run_app+0x3c in hostpolicy.dll+0x1941c at D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp:284
0x7ffebaa19d17: corehost_main+0x107 in hostpolicy.dll+0x19d17 at D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp:430
0x7ffebaa7b459: execute_app+0x2e9 in hostfxr.dll+0xb459 at D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp:146
0x7ffebaa7e4f6: `anonymous namespace'::read_config_and_execute+0xa6 in hostfxr.dll+0xe4f6 at D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp:533
0x7ffebaa807cf: fx_muxer_t::handle_exec_host_command+0x15f in hostfxr.dll+0x107cf at D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp:1018
0x7ffebaa7eb52: fx_muxer_t::execute+0x482 in hostfxr.dll+0xeb52 at D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp:579
0x7ffebaa781cb: hostfxr_main_startupinfo+0xab in hostfxr.dll+0x81cb at D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp:61
0x7ff7e7cb255b: exe_start+0x8eb in tests.exe+0x1255b at D:\a\_work\1\s\src\native\corehost\corehost.cpp:251
0x7ff7e7cb28cb: wmain+0xab in tests.exe+0x128cb at D:\a\_work\1\s\src\native\corehost\corehost.cpp:322
0x7ff7e7cb3d78: invoke_main+0x22 in tests.exe+0x13d78 at D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:90
0x7ff7e7cb3d78: __scrt_common_main_seh+0x10c in tests.exe+0x13d78 at D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
0x7fff79193fed: BaseThreadInitThunk+0x1d in kernel32.dll+0x13fed
0x7fff7a6142a8: RtlUserThreadStart+0x28 in ntdll.dll+0x42a8

Assembly Injection

Before getting started, you must decide whether you wish to implement your injection code and injected code in the same project or in two separate projects. Using a single project is easiest, but separate projects may be desirable if having the injection code in the injected assembly would lead to too many assemblies being pointlessly loaded into the target process.

Single-Project Setup

For a single-project setup, your project file should look something like:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TargetFramework>net8.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Vezel.Ruptura.Hosting"
                          Version="x.y.z" />
        <PackageReference Include="Vezel.Ruptura.Injection"
                          Version="x.y.z" />
    </ItemGroup>
</Project>

(Replace x.y.z with the actual NuGet package version.)

Your entry point can then be implemented using the IInjectedProgram interface:

sealed class InjectedProgram : IInjectedProgram
{
    public static async Task<int> RunAsync(InjectedProgramContext context, ReadOnlyMemory<string> args)
    {
        if (context.InjectorProcessId != null)
            return 42;

        using var target = TargetProcess.Open(int.Parse(args.Span[0]));
        using var injector = new AssemblyInjector(
            target, new AssemblyInjectorOptions(typeof(InjectedProgram).Assembly.Location));

        await injector.InjectAssemblyAsync();

        return await injector.WaitForCompletionAsync() == 42 ? 0 : 1;
    }
}

(Note that, when using Vezel.Ruptura.Hosting, you should not implement a regular Main entry point; the IInjectedProgram implementation will be used instead.)

The above example injects itself into a given target process's ID. The injected code just returns the exit code 42 immediately, which the injection code verifies. In general, checking the InjectedProgramContext.InjectorProcessId property for a non-null value is the way to know if you are running in the target process.

Multi-Project Setup

For a multi-project setup, the project file for the injector program should look like this:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TargetFramework>net8.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Vezel.Ruptura.Injection"
                          Version="x.y.z" />
    </ItemGroup>
</Project>

The injector entry point can be implemented normally:

static class Program
{
    static async Task<int> Main(string[] args)
    {
        using var target = TargetProcess.Open(int.Parse(args[0]));
        using var injector = new AssemblyInjector(target, new AssemblyInjectorOptions(args[1]));

        await injector.InjectAssemblyAsync();

        return await injector.WaitForCompletionAsync() == 42 ? 0 : 1;
    }
}

The project file for the injected program should look like this:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TargetFramework>net8.0</TargetFramework>
        <UseAppHost>false</UseAppHost>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Vezel.Ruptura.Hosting"
                          Version="x.y.z" />
    </ItemGroup>
</Project>

Note that the injected assembly must have OutputType set to Exe too, since the .NET hosting APIs will look for an executable program entry point. Despite this, a native executable is not needed for the injected assembly, so UseAppHost can safely be set to false to cut down on build time and publish size.

The entry point for the injected program is implemented with the IInjectedProgram interface:

sealed class InjectedProgram : IInjectedProgram
{
    public static Task<int> RunAsync(InjectedProgramContext context, ReadOnlyMemory<string> args)
    {
        return Task.FromResult(42);
    }
}

This example does the same thing as the single-project example, but here, you pass the file name of the injected program to the injection program in addition to the target process's ID. There are ways that you can avoid that requirement, e.g. by referencing the injected program project from the injection program project and using typeof(InjectedProgram).Assembly.Location.

Process Creation

In addition to injecting existing processes, you can also create new ones. This is mainly useful because it lets you start a process in a suspended state:

sealed class InjectedProgram : IInjectedProgram
{
    public static async Task<int> RunAsync(InjectedProgramContext context, ReadOnlyMemory<string> args)
    {
        if (context.InjectorProcessId != null)
        {
            context.WakeUp();

            return 42;
        }

        using var target = TargetProcess.Create(args.Span[0], string.Empty, null, suspended: true);
        using var injector = new AssemblyInjector(
            target, new AssemblyInjectorOptions(typeof(InjectedProgram).Assembly.Location));

        await injector.InjectAssemblyAsync();

        return await injector.WaitForCompletionAsync() == 42 ? 0 : 1;
    }
}

The InjectedProgramContext.WakeUp() call resumes the process's main thread. Having control over when the main thread starts executing gives you a window of opportunity to perform setup work such as installing function hooks.

Process Lifetime

It is worth noting that the target process's lifetime is completely unaffected by the lifetime of your injected program. Even if you return from your IInjectedProgram.RunAsync() implementation and you have no background work running, the target process will still continue running. If you need the target process to exit at the same time as your injected program, it is your responsibility to make that happen.

Interprocess Communication

Ruptura does not provide a mechanism for communication between the injector process and the injected process. A rudimentary way of passing arguments to the injected program exists in the form of the AssemblyInjectorOptions.WithArguments() method, however.

System Access

The Vezel.Ruptura.System library provides convenient managed wrappers for native Win32 APIs and kernel objects. The focus is primarily on functionality that is not readily available in .NET, but there is still some overlap.

For example, you could enumerate all threads in a process like so:

Some kernel objects are waitable. This is the case for ProcessObject and ThreadObject, for example. They derive from SynchronizationObject (which derives from KernelObject). You could wait for a thread to exit like this:

(WaitResult.Alerted can only occur if alertable is set to true. Also, WaitResult.Abandoned is only relevant when waiting on a mutex.)

CallTrace tries very hard internally to fill in as much information as it can. For managed frames, information is pulled from CoreCLR internals, while for unmanaged frames, the library will consult export names, symbol files, etc. These are implemented in the ManagedCallFrameSymbolicator and NativeCallFrameSymbolicator singleton classes, respectively, and both derive from the CallFrameSymbolicator class.

If you obtain a of the DbgHelp library consisting of dbghelp.dll and symsrv.dll, you can simply drop them into your application directory and Ruptura will pick them up. Setting the _NT_SYMBOL_PATH environment variable to https://msdl.microsoft.com/download/symbols or similar will then enable symsrv.dll to actually download Microsoft symbol files. Doing that, you will get a much better call trace:

Regardless of the above, the CoreCLR runtime will stay loaded in the process until it exits, due to limitations in the .NET hosting APIs. This means that you cannot inject into the same process more than once for the duration of its lifetime. If you need to reload managed code in the target process, you can use to do so. The library makes this particularly easy.

One simple and type-safe way to achieve IPC between the two processes would be to combine either (one-way) or (one-way or two-way) with the library.

Instances of classes derived from KernelObject (such as the SnapshotObject and ThreadObject instances in the above code) are thin wrappers around objects, with a few niceties on top, such as a settable IsInheritable property and equality operators based on . KernelObject inherits from , so like SafeHandle, you can rely on KernelObject instances being usable in finalizers. On top of that, they expose a bunch of relevant Win32 APIs for the particular object type.

DbgHelp
standalone version
AssemblyLoadContext
McMaster.NETCore.Plugins
anonymous pipes
named pipes
StreamJsonRpc
using var snapshot = SnapshotObject.Create(SnapshotFlags.Threads, ProcessObject.CurrentId);

foreach (ThreadSnapshot threadSnapshot in snapshot.EnumerateThreads())
{
    using var thread = ThreadObject.OpenId(threadSnapshot.Id, ThreadAccess.GetLimitedInfo);

    Console.WriteLine($"{thread.Id}: {thread.Description}");
}
using var thread = ThreadObject.OpenId(id, ThreadAccess.Synchronize);

if (thread.Wait(TimeSpan.FromSeconds(5), alertable: false) == WaitResult.TimedOut)
    throw new TimeoutException();
SafeHandle
CompareObjectHandles
CriticalFinalizerObject

Home

Ruptura provides a set of libraries that make it easy to inject a managed .NET assembly into arbitrary Windows (and Wine) processes for the purposes of function hooking and memory manipulation.

Ruptura injects a bundled native module into the target process, which then locates the .NET runtime in either framework-dependent or self-contained mode and initializes it, after which a user-specified managed assembly is executed - all without the user writing a single line of native code. Additionally, a library facilitating common function hooking and memory manipulation scenarios is available for use by the injected assembly.

Features

Here are some of the Ruptura highlights:

  • Easy injection and hosting: Just point Ruptura to a target process and give it the name of a managed assembly to inject. All the complexity of injecting the CoreCLR runtime libraries and loading your assembly is handled for you.

  • Create or attach to processes: You can inject into existing processes or create them yourself. When creating a target process, it can be started in a suspended state, giving your injected assembly full control over when and how the target process starts running.

  • Native function hooks: Hooking APIs allow you to intercept calls to arbitrary native functions in a process. You can inspect or modify arguments and return values as needed, and it is up to you whether the original function is called at all.

  • Hook state access: A state object can be associated with a hook, giving you easy access to your managed application state from within a hook function, where it is ordinarily difficult to access non-global state.

  • Hook error prevention: A 'hook gate' prevents common programming errors (deadlocks, stack overflows, etc) by avoiding execution of hook functions when the Windows loader lock is held, or when the hook is already executing earlier in the call stack.

  • Call stack tracing: Call stack traces can be collected at any point, giving you insight into how the target process behaves. These traces contain highly detailed information both for managed and unmanaged stack frames.

  • Operating system interop: A set of convenient classes and functions for accessing Win32 APIs and kernel objects are provided, which Ruptura makes heavy use of for its own functionality.

All of these features make Ruptura a great choice for a wide variety of tasks such as closed-source modding, interoperability research, advanced diagnostics, security research, anti-tampering, etc.

Limitations

There are some notable limitations and caveats to be aware of when using Ruptura:

  • Only modern versions of Windows 10 and 11 are supported due to certain native APIs used for function hooking and call tracing.

  • The CoreCLR runtime is never unloaded from the target process due to limitations in the hosting APIs for .NET 7+. This means that you cannot inject into the same process more than once for the duration of its lifetime.

Publishing modes: You can publish your Ruptura-based application in either or mode. There is also partial support for .

There is currently only support for injecting x64 processes. x86 support is .

framework-dependent
self-contained
trimming
in the works