Skip to content

Repository files navigation

RDCManHotkeyFix

Fixes RDCMan's broken Focus Release hotkeys (Ctrl+Alt+Left/Right) on Windows 11 22H2+.

Quick Start

Place RDCManHotkeyFix.Injector.exe in the same directory as RDCMan.exe and run it. It will automatically find and launch RDCMan with the fix applied.

Or you can run the command:

RDCManHotkeyFix.Injector.exe [path\to\RDCMan.exe] [-- RDCMan args...]

Demo

Before (broken) After running with fix (fixed)
not fixed fixed

The Problem

Windows 11 22H2 changed the RDP ActiveX default behavior: AllowAxToContainerEvents was changed from true to false, causing Ctrl+Alt+Arrow to be sent directly into the remote session instead of triggering the OnFocusReleased event.

How It Works

Injector.exe (Launcher)
  +-- Sets DOTNET_STARTUP_HOOKS environment variable
        +-- Launches RDCMan.exe
              +-- CoreCLR loads StartupHook.Initialize()
                    +-- Native inline hook: patches Server.ConnectAs()
                          +-- Prefix: InitClient() -> set AllowAxToContainerEvents = true
                                +-- Original ConnectAs (including Connect)
Technical Details

DOTNET_STARTUP_HOOKS

A .NET Core 3.0+ mechanism that loads custom managed assemblies before the app's Main() runs.

  • CoreCLR looks for internal static class StartupHook (no namespace)
  • Calls static void Initialize()
  • The hook assembly's target framework must be compatible with the host app

Native Inline Hook (x64)

Why not Harmony?

Harmony 2.3.3's underlying MonoMod does not work in .NET 8 single-file apps:

  1. MonoMod needs to load clrjit.dll from the filesystem for JIT operations
  2. .NET 8 single-file apps bundle clrjit.dll inside the exe, loaded from memory
  3. MonoMod can't find clrjit.dll and throws PlatformNotSupportedException: Could not locate clrjit library
  4. Even preloading a system-installed clrjit.dll causes NullReferenceException due to version mismatch

Related issues:

Alternative: x64 Native JMP Hook

Writes a 14-byte absolute jump at the beginning of the target method's JIT'd machine code:

FF 25 00 00 00 00              ; JMP [RIP+0]
XX XX XX XX XX XX XX XX        ; 8-byte target address

How is the JIT'd code address found?

GetFunctionPointer() may return a precode stub, not the actual JIT'd code. Virtual method dispatch bypasses the precode and jumps directly to the JIT'd code, so patching the precode alone won't intercept virtual calls.

Inspired by MonoMod's approach: starting from GetFunctionPointer(), the code detects x64 JMP instruction patterns (E9 rel32, FF 25 [RIP+disp32] abs64) and follows the JMP chain until it reaches a non-JMP instruction — the actual method body. This approach depends only on CPU instruction encoding, not on .NET runtime internal struct layouts.

The original method is called via a restore-invoke-repatch pattern (with a lock to prevent race conditions).

Known limitations:

  • Tiered compilation: .NET Core's tiered compilation may re-JIT methods, potentially overwriting the hook
  • RIP-relative instructions: If using a trampoline, copied instructions may contain RIP-relative addressing that needs offset adjustment after relocation

Build from Source

dotnet build Payload.csproj -c Release
dotnet publish Injector.csproj -c Release

Payload must be built first — the Injector embeds it as a resource. Output: bin/Release/net8.0-windows/win-x64/publish/RDCManHotkeyFix.Injector.exe


Tested On

  • RDCMan 2.93+ (.NET 8 single-file)
  • Windows 11 22H2 / 23H2 / 24H2

License

MIT

About

Fixes RDCMan's hotkeys (Ctrl+Alt+Left/Right)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages