NOTE: Nearly complete, but still a WIP
Windows-specific command line tool and library for injecting assemblies into Unity games at runtime. (possibly any Mono application, but haven't tested) Useful when you want to load an assembly into a game but don't want to patch the game's files.
Usage information specific to the library or command line tool found further down. This section describes the options and expectations.
Required:
The following parameters require a value:
pid(-p,--pid) - The targeted process ID.assembly_path(positional argument) - The assembly you'd like to load. (not required, but highly recommended that you use the absolute filepath)
You can use whatever assembly/class/method name you'd like, but the signature of the entrypoint method for your assembly is expected to match:
public static void MethodName()
{
// ...
}Optional:
The following parameters are optional:
mono_path(-M,--mono) (Default: auto-detected) - Filepath of the target's Mono DLL. By default, the injector scans the target process's loaded modules for one exportingmono_initand uses whichever it finds; only override this if that detection picks the wrong module.class_name(-c,--class) (Default: Loader) - The fully qualified name of the class that contains your assembly's entrypoint method.method_name(-m,--method) (Default: Initialize) - The name of your entrypoint method, which is immediately called after the assembly has been loaded.thread_id(-t,--tid) (No Default) - Should only be specified if you require your assembly's entrypoint be invoked on a specific existing thread in the target process (via thread hijacking) instead of a new thread attached to the root app domain.debugging(-g,--debug) (Default: FALSE) - Allows debugging the game in Visual Studio Tools for Unity. See below for more info.log_path(Default: none) - Accepted by the library API (unij_set_log_path), but not currently exposed as a CLI flag, and not yet wired up to an actual logging backend (LogDebug/LogInfo/LogWarning/LogErrorare all no-ops right now) - setting it currently has no effect.
Unsupported:
The following parameters were either started and scrapped or never made it past "idea".
no_wait(Default: FALSE) - Tells the injector not to wait for confirmation that the the loader has completed.
Debugging
I copied the logic for this option from dynity, which documents the setup process. I've never tried setting up the tools and testing this.
uniject.exe -l
uniject.exe [OPTION]... -p PID ASSEMBLY
| Flag | Description |
|---|---|
-h, --help |
Display usage and exit. |
-l, --list |
List running Unity/Mono processes (PID, executable, detected Mono module). |
-p, --pid |
Required (unless -l). Process ID of the target game. |
ASSEMBLY |
Required (unless -l). Filepath of the assembly to inject, given as a positional argument. |
-g, --debug |
Enable Mono soft-debugger support in the target. See Debugging. |
-t, --tid |
Thread ID to hijack for the entrypoint call, instead of attaching a new thread. |
-c, --class |
Fully qualified class name containing the entrypoint (default: Loader). |
-m, --method |
Entrypoint method name (default: Initialize). |
-M, --mono |
Explicit path to the target's Mono DLL (default: auto-detected). |
Examples:
uniject.exe -l
uniject.exe -p 12345 C:\path\to\MyMod.dll
uniject.exe -p 12345 -c MyMod.Loader -m Start C:\path\to\MyMod.dll
TODO: Library documentation. In the meantime, include/uniject/*.h is the API surface - injector.h/process.h
for driving an injection, params.h for the parameters struct, base.h for the uniject_t context lifecycle
(unij_injector_open*/unij_loader_open/unij_inject/unij_close).
Probably best to just break this down into the step-by-step:
- Injector is fed and processes a set of parameters for the current injection.
- Injector acquires necessary token privileges and opens the process specified by the PID parameter.
- Injector auto-detects the target's Mono DLL (unless
--monowas given), initializes shared memory, and populates it with all the parameters needed for the injection. - Injector creates a named event that will be used to signal the completion of the loader process.
- Injector loads the loader DLL into the target process:
uniject-loader-32.dllvsuniject-loader-64.dll- Picked based on whether the target process is 32-bit or 64-bit.- Always done via
CreateRemoteThread, running a small hand-written shellcode stub that walks the target's own PEB/loader data to resolveLoadLibraryWand calls it - no assumptions about anything already being injected.
- Loader (DLL) maps the shared memory and reads in the injection instructions.
- Loader finds the currently loaded Mono DLL and looks up all the API it'll be using.
- Loader invokes the entrypoint one of two ways, depending on whether a thread ID (
--tid) was specified:- No thread ID: the loader attaches a brand-new thread to the root app domain, calls the entrypoint on it, then detaches it from the app domain when done.
- Thread ID specified: the loader hijacks the existing thread via
SuspendThread/GetThreadContext/SetThreadContext, running the entrypoint call on it directly (it's already attached to the app domain), then restores the thread's original context.
- Loader finds the specified assembly and loads it into the current app domain, then searches it for the specified class and method name and calls it, waiting for it to complete.
- Loader cleans up, signals the named event to notify the injector of its completion, and then unloads itself from the process. (assembly still loaded)
- Injector cleans up shared memory and event and exits.