Allows injecting .NET code into any Windows process.
Heavily based on Cory Plott's Snoop.
The only requirement is that the injected code must be a public static method on a public static class, such as:
namespace Sample;
public static class Startup
{
public static void Start(string arg1, int arg2, bool debug)
{
if (debug)
Debugger.Launch();
// do stuff with arg1, arg2, etc.
// note args are typed :)
}
}
NOTE: parameter type conversion is supported and happens via the
TypeConverter
associated with the parameter type.
There are two main usages for this package:
- From AnyCPU code: your code is bitness-agnostic and can be injected into the target process whether it's x86 or x64.
- From x86/x64 code: you are injecting into a target process that has the same bitness as the calling code.
This is likely the more common scenario. You have .NET code that is AnyCPU and can
therefore be injected regardless of the target process bitness. When referencing this
package, you will get two (content) folders containing a helper Injector.exe
for each architecture:
These files are automatically copied to the output directory under Injector\[x86|x64]\Injector.exe
(and are also included when doing dotnet publish
). This allows you to run the relevant executable
that matches the target process bitness.
Injector.exe
usage:
> Injector.exe -?
Usage: Injector.exe <mainWindowHandle> <assemblyFile> <typeName> <methodName>
Arguments:
<processMainWindowHandle> IntPtr of the main window handle of the process to inject, i.e. Process.MainWindowHandle.
<assemblyFile> The full path to the .NET assembly to load in the remote process.
<typeName> Full type name of the public static class to invoke in the remote process.
<methodName> Name of the static method in that class to invoke in the remote process. Must be a
static method, which can also receive arguments, such as 'Start:true:42'.
To detect the target process bitness, you can use the following bit of interop:
static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsWow64Process([In] IntPtr process, [Out] out bool wow64Process);
}
And the following code would lookup the target process (in this case, we just get the first instance
of devenv.exe
, the Visual Studio main process, as an example), and invoke the right executable:
var targetProcess = System.Diagnostics.Process.GetProcessesByName("devenv.exe")[0];
NativeMethods.IsWow64Process(targetProcess.Handle, out var isWow);
var platform = isWow ? "x86" : "x64";
Process.Start(Path.Combine("Injector", platform, "Injector.exe"),
// IntPtr of the main window handle of the process to inject
targetProcess.MainWindowHandle + " " +
// The full path to the .NET assembly to load in the remote process
Assembly.GetExecutingAssembly().Location + " " +
// Full type name of the public static class to invoke in the remote process
typeof(Startup).FullName + " " +
// Name of the static method in that class to invoke in the remote process,
// and any parameters.
$"{nameof(Startup.Start)}:hello:42:true");
NOTE: we can pass typed arguments to the
Startup.Start
method (shown as an example at the beginning) and type conversion will be applied automatically.
When building platform-specific code, the project would typically have (for a console app, for example):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Platforms>x64;x86</Platforms>
</PropertyGroup>
</Project>
You would then build for either platform via: dotnet build --arch x64
or dotnet build --arch x86
.
In this case, the bitness of the calling code (that intends to inject itself into a remote process)
must match the target process bitness too. Since the bitness of both is the same, you can use the
automatically referenced assembly from your code, rather than invoking the helper Injector.exe
as shown in the first case.
The code will otherwise look similar to the previous case:
var targetProcess = System.Diagnostics.Process.GetProcessesByName("devenv.exe")[0];
// NOTE: target process bitness must match our own assembly architecture for
// this to succeed.
Devlooped.Injector.Launch(
// IntPtr of the main window handle of the process to inject
targetProcess.MainWindowHandle,
// The full path to the .NET assembly to load in the remote process
Assembly.GetExecutingAssembly().Location,
// Full type name of the public static class to invoke in the remote process
typeof(Startup).FullName,
// Name of the static method in that class to invoke in the remote process,
// and any parameters.
$"{nameof(Startup.Start)}:hello:42:true");
NOTE: the
Devlooped.Injector
type will NOT be available on AnyCPU projects
See Program.cs for a complete example.