mono-injector is a Rust workspace for injecting managed .NET assemblies into Windows processes that host Mono, especially Unity games and tools.
The workspace is split into four crates:
| Crate | Purpose |
|---|---|
mono-injector |
Low-level Windows remote Mono API caller. |
mono-injector-core |
Reusable orchestration layer for profiles, process discovery, injection/ejection planning, metadata inspection, remembered handles, Steam launch, and readiness waits. |
mono-injector-cli |
Command-line frontend exposed as the mono-injector binary. |
mono-injector-gui |
Iced-based graphical frontend using the same core layer. |
The default managed entry point is Loader::Init. The default eject entry point is Loader::Unload. If no namespace is supplied, the core inspects the assembly metadata and prefers the namespace of the selected class before falling back to the most common non-empty namespace.
Use this on software you own, control, or are explicitly allowed to modify. Injection into protected or third-party processes can violate terms of service and can crash the target process.
Injection and ejection:
- Inject managed assemblies into Windows processes hosting Mono.
- Eject managed assemblies by invoking a cleanup method and unloading the remembered Mono assembly handle.
- Support x86 and x64 targets through architecture-specific remote stubs.
- Execute Mono API calls from outside the target process without injecting a native loader DLL.
- Configure namespace, class, inject method, and eject method per operation.
- Use practical defaults for common loaders: class
Loader, inject methodInit, eject methodUnload. - Run inject and eject dry-runs that resolve the plan without calling Mono in the target process.
- Use raw handle ejection for advanced/manual recovery, gated behind
--force.
Process and runtime resolution:
- Resolve targets by PID or exact process name.
- List running processes and filter by process name, module name, Mono runtime modules, or Unity modules.
- Include matching or loaded module names in process listings.
- Select the Mono runtime module with a case-insensitive
--mono-modulehint. - Configure remote-thread wait timeout with
--timeout-ms. - Configure the base directory passed to
mono_assembly_load_from_fullwith--base-dir. - Validate managed assemblies by reading .NET metadata.
- Infer the namespace from .NET metadata when it is omitted.
Profiles and remembered state:
- Store reusable TOML profiles for target, assembly, entry point, runtime, wait, settle, and Steam settings.
- Select profiles by positional name or
--profileflag. - Remember successful injection handles locally for later safe ejection.
- Protect against PID reuse by recording process start time.
- Guard ejection by matching remembered handles against the same process instance, namespace, and class.
- Require
--latestor an explicit handle when several remembered injections match. - Clean stale remembered records or clear all records manually.
Launch and readiness:
- Wait for a process before injecting.
- Wait for a module before injecting.
- Configure process/module wait timeout and poll interval.
- Configure a settle delay after readiness before injection.
- Launch Steam apps with
steam://rungameid/<APP_ID>. - Skip Steam launch when the target process is already running.
- Use Steam defaults of
d3d11.dllreadiness and8000mssettle delay unless overridden.
Frontends and tooling:
- CLI supports JSON output where useful through the global
--jsonflag. - CLI can run a post-injection command with target process, PID, handle, and assembly path environment variables.
- GUI supports inject, eject, active-injection status, process browsing, profile management, and logs.
- CLI and GUI share the same
mono-injector-coreplanning, profile, state, and runtime behavior. - Workspace uses Rust 2024 with
warnings = denyand Clippy pedantic enabled.
The CLI binary is named mono-injector. From source, run it with:
cargo run -p mono-injector-cli -- <command> [options]After building, run the binary directly:
target\debug\mono-injector.exe <command> [options]Top-level usage:
Usage: mono-injector.exe [OPTIONS] <COMMAND>
Commands:
inject Inject a managed assembly into a target process
eject Eject a previously injected assembly from a target process
list List running processes
status Show remembered injections
clean Remove stale remembered injections
profile Inspect profile configuration
help Print this message or the help of the given subcommand(s)
Options:
--json Emit machine-readable JSON output where supported
-h, --help Print help
-V, --version Print version
Inject a managed assembly into a target process.
Usage: mono-injector.exe inject [OPTIONS] [PROFILE] [-- <POST_COMMAND>...]
Arguments:
[PROFILE] Optional profile name
[POST_COMMAND]... Command to run after successful injection. Pass after `--`
Options:
--json Emit machine-readable JSON output where supported
--profile <PROFILE_ALIAS> Profile name alias for scripts that prefer flags
-p, --process <PROCESS> Target process id or exact process name
-a, --assembly <ASSEMBLY> Managed assembly to load into the target process
-n, --namespace <NAMESPACE> Namespace containing the loader class
-c, --class <CLASS_NAME> Loader class name
-m, --method <METHOD_NAME> Loader method to invoke after loading the assembly
--eject-method <EJECT_METHOD> Cleanup method recorded for later default ejection
--wait Wait for the target process before injecting
--wait-timeout <WAIT_TIMEOUT> Time to wait for process/module readiness, for example 120s or 2m [default: 120s]
--poll-interval <POLL_INTERVAL> Time between process/module readiness checks, for example 1000ms or 1s [default: 1000ms]
--wait-module <WAIT_MODULE> Wait for a loaded module before injecting, for example UnityPlayer.dll
--no-wait-module Disable the default readiness-module wait used with --steam-app
--settle-ms <SETTLE> Extra time to wait after readiness before injecting. Use 0ms to disable
--steam-app <STEAM_APP> Launch a Steam app before waiting for the process
--dry-run Resolve inputs without calling Mono in the target process
--timeout-ms <TIMEOUT_MS> Remote-thread wait timeout in milliseconds [default: 5000]
--mono-module <MONO_MODULE_HINT> Case-insensitive fragment used to find the target Mono module
--base-dir <BASE_DIR> Base directory passed to `mono_assembly_load_from_full`
-h, --help Print help
-V, --version Print version
Basic inject using defaults:
mono-injector inject --process Game.exe --assembly C:\mods\MyMod.dllExplicit entry point:
mono-injector inject -p Game.exe -a C:\mods\MyMod.dll -n MyMod -c Loader -m Init --eject-method UnloadDry-run an inject plan:
mono-injector inject --dry-run -p Game.exe -a C:\mods\MyMod.dllWait for the process and a readiness module:
mono-injector inject -p Game.exe -a C:\mods\MyMod.dll --wait --wait-module UnityPlayer.dll --settle-ms 3000msLaunch a Steam app and inject after default Steam readiness behavior:
mono-injector inject -p Game.exe -a C:\mods\MyMod.dll --steam-app 480Run a command after successful injection:
mono-injector inject -p Game.exe -a C:\mods\MyMod.dll -- powershell -NoProfile -Command "Write-Output $env:MONO_INJECTOR_HANDLE"The post-command receives these environment variables:
| Variable | Value |
|---|---|
MONO_INJECTOR_PROCESS |
Target process name. |
MONO_INJECTOR_PID |
Target PID. |
MONO_INJECTOR_HANDLE |
Assembly handle returned by inject, or an empty string if absent. |
MONO_INJECTOR_ASSEMBLY |
Managed assembly path. |
Eject a previously injected assembly from a target process.
Usage: mono-injector.exe eject [OPTIONS] [PROFILE]
Arguments:
[PROFILE] Optional profile name
Options:
--json Emit machine-readable JSON output where supported
--profile <PROFILE_ALIAS> Profile name alias for scripts that prefer flags
-p, --process <PROCESS> Target process id or exact process name
-a, --assembly <HANDLE> Assembly handle returned by inject. Defaults to a matching remembered injection
--raw-handle <RAW_HANDLE> Explicit unsafe handle mode; requires --force
-n, --namespace <NAMESPACE> Namespace containing the loader class
-c, --class <CLASS_NAME> Loader class name
-m, --method <METHOD_NAME> Cleanup method to invoke before closing the assembly
--latest Use the latest matching remembered injection when several match
--force Bypass the local injection-record guard for advanced/manual ejection
--dry-run Resolve inputs without calling Mono in the target process
--timeout-ms <TIMEOUT_MS> Remote-thread wait timeout in milliseconds [default: 5000]
--mono-module <MONO_MODULE_HINT> Case-insensitive fragment used to find the target Mono module
--base-dir <BASE_DIR> Base directory passed to `mono_assembly_load_from_full`
-h, --help Print help
-V, --version Print version
Eject using the remembered matching handle:
mono-injector eject --process Game.exeEject a specific remembered handle:
mono-injector eject -p Game.exe -a 0x12345678Resolve an eject plan without touching the process:
mono-injector eject --dry-run -p Game.exe --latestUse a raw handle manually:
mono-injector eject -p Game.exe --raw-handle 0x12345678 --force -n MyMod -c Loader -m UnloadSafe eject behavior:
- Without
--force,ejectrequires the handle to be remembered for the same PID, process start time, namespace, and class. - If one remembered injection matches, the handle is selected automatically.
- If several remembered injections match, pass
--latestor provide a specific--assembly <HANDLE>. --raw-handleis intentionally unsafe and requires--force.
List running processes.
Usage: mono-injector.exe list [OPTIONS]
Options:
-f, --filter <FILTER> Case-insensitive substring used to filter process and module names
--json Emit machine-readable JSON output where supported
--mono Show only processes with a Mono runtime module loaded
--unity Show only Unity processes
--modules Include matching or loaded module names in the output
-h, --help Print help
-V, --version Print version
Examples:
mono-injector list
mono-injector list --mono
mono-injector list --unity --modules
mono-injector list --filter Game --modules
mono-injector --json list --monoShow remembered injections.
Usage: mono-injector.exe status [OPTIONS] [PROFILE]
Arguments:
[PROFILE] Optional profile name to resolve the target process
Options:
--json Emit machine-readable JSON output where supported
--profile <PROFILE_ALIAS> Profile name alias for scripts that prefer flags
-p, --process <PROCESS> Target process id or exact process name
-h, --help Print help
-V, --version Print version
Examples:
mono-injector status
mono-injector status --process Game.exe
mono-injector status my-profile
mono-injector --json statusRemove remembered injection records.
Usage: mono-injector.exe clean [OPTIONS]
Options:
--all Remove all remembered injections, including live ones
--json Emit machine-readable JSON output where supported
-h, --help Print help
-V, --version Print version
Examples:
mono-injector clean
mono-injector clean --all
mono-injector --json cleanDefault clean removes stale records only. clean --all removes every remembered record, including records for live process instances.
Inspect profile configuration.
Usage: mono-injector.exe profile [OPTIONS] <COMMAND>
Commands:
list List configured profiles
show Show one configured profile
path Print the profiles file path
help Print this message or the help of the given subcommand(s)
Options:
--json Emit machine-readable JSON output where supported
-h, --help Print help
-V, --version Print version
Profile subcommands:
Usage: mono-injector.exe profile list [OPTIONS]
Usage: mono-injector.exe profile show [OPTIONS] <NAME>
Usage: mono-injector.exe profile path [OPTIONS]
Examples:
mono-injector profile path
mono-injector profile list
mono-injector profile show my-profile
mono-injector --json profile show my-profileProfiles are TOML and live at the path printed by mono-injector profile path. A typical profile file looks like this:
[profiles.my-profile]
process = "Game.exe"
assembly = "C:\\mods\\MyMod.dll"
namespace = "MyMod"
class = "Loader"
inject_method = "Init"
eject_method = "Unload"
mono_module = "mono-2.0-bdwgc"
base_dir = "C:\\mods"
timeout_ms = 5000
wait_module = "UnityPlayer.dll"
settle_ms = 3000
steam_app = 480All profile fields are optional, but an inject operation still needs enough data after combining CLI flags and profile values to resolve a target process and assembly. CLI flags override profile values. Remembered injection records are stored separately in the user local data directory as mono-injector/injections.json.
Requirements:
- Windows target environment.
- Rust toolchain compatible with workspace
rust-version = "1.95.0"and edition 2024. justfor the documented task runner commands.- A target process with an embedded Mono runtime for real injection tests.
Build the whole workspace:
just buildBuild release artifacts:
just build releaseBuild only the CLI:
just build dev mono-injector-cliBuild only the GUI:
just build dev mono-injector-guiEquivalent Cargo commands:
cargo build --workspace --all-targets
cargo build --release --workspace --all-targets
cargo build -p mono-injector-cli
cargo build -p mono-injector-guiRun from source:
just cli --help
just gui
cargo run -p mono-injector-cli -- --help
cargo run -p mono-injector-guiFormat the workspace:
just formatRun the required final check:
just checkjust check aliases just lint, which runs:
cargo clippy --all-targets --all-features --allRun all tests:
just testRun tests for one crate:
just test mono-injector-core
cargo test -p mono-injector-coreUseful CLI smoke tests:
cargo run -p mono-injector-cli -- --help
cargo run -p mono-injector-cli -- inject --help
cargo run -p mono-injector-cli -- eject --help
cargo run -p mono-injector-cli -- profile pathReal injection tests require a live Windows process hosting Mono and a managed assembly with compatible static entry points:
public static class Loader
{
public static void Init()
{
}
public static void Unload()
{
}
}At a high level, the CLI and GUI collect user intent and pass it to mono-injector-core. The core resolves profiles, process identity, assembly metadata, runtime options, wait behavior, and remembered injection state. The low-level mono-injector crate performs the actual remote Mono calls.
Injection flow:
- Resolve a target process from a PID or exact process name.
- Resolve the managed assembly path from CLI flags, a profile, or a remembered prior injection.
- Read the assembly bytes from disk.
- Validate .NET metadata.
- Resolve namespace, class, inject method, and eject method.
- Optionally launch a Steam app if configured and the target is not already running.
- Optionally wait for the target process.
- Optionally wait for a readiness module.
- Optionally sleep for the settle delay.
- Find the target Mono module in the remote process.
- Call Mono APIs in the remote process to open the image, load the assembly, resolve the class and method, and invoke the static inject method.
- Read the resulting assembly handle.
- Store a remembered injection record for safe later ejection.
Ejection flow:
- Resolve the target process.
- Resolve the assembly handle from CLI flags or remembered records.
- Resolve namespace, class, and eject method from CLI flags, profile values, or remembered records.
- Enforce the local record guard unless
--forceis used. - Call the configured static cleanup method in the target process.
- Close/unload the assembly through Mono.
- Remove the remembered injection record.
The low-level crate works by allocating memory in the target process, writing a small architecture-specific stub, and running that stub with a remote thread. The stub calls functions exported by the embedded Mono runtime. Arguments and return values are marshaled through remote process memory with Windows APIs such as ReadProcessMemory, WriteProcessMemory, VirtualAllocEx, VirtualFreeEx, CreateRemoteThread, and module enumeration APIs.
The managed inject and eject methods should be static and parameterless. They should return quickly. The eject method is responsible for cleaning up objects, hooks, threads, event handlers, and other resources created by the injected assembly.
- Techniques: https://github.com/wh0am15533/SharpMonoInjector
- Original SharpMonoInjector project: https://github.com/warbler/SharpMonoInjector
This project is licensed under GPL-3.0-only. See LICENSE for the full license text.
