Minimal framework to inject the .NET Runtime into a process.
Supports Windows and Linux. macOS support is complicated due to SIP, and will not be covered here.
Building sabinokaku requires the nightly toolchain. The workspace Cargo.toml is preconfigured to strip debug symbols,
otherwise the Linux kaku.so
binary will bloat from ~500KB to around 3MB large.
$ cargo build --release
The sabinokaku-common
module contains platform independent logic such as configuration parsing and bootstrapping hostfxr
and nethost
.
The sabinokaku-loader
module contains the code for the actual injected assembly; this will be built as cdylib
(.dll
or .so
), and is the
actual assembly that has to be injected into the address space of the target process.
injector-example
is a simple dummy program that injects a DLL into a running process, or simply runs Hello World
and
dumps environment variables for testing injection on Linux.
- Create a class library project for use as your entry point, and add
<EnableDynamicLoading>True</EnableDynamicLoading>
to the csproj to properly generate the runtime configuration. - The entry point in .NET must always have the signature
public static int Main(IntPtr args, int sizeBytes)
. - Create a
kaku.co
file, see Configuration for syntax, and add it to your project. - Add a prebuilt binary of
kaku.dll
on Windows, orlibkaku.so
on Linux.
Your csproj should have the following entries.
<PropertyGroup>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<None Update="kaku.co">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="kaku.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<!-- Linux only -->
<None Update="libkaku.so">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
- On Windows, inject
kaku.dll
into a running process with a DLL injection tool such as Reloaded.Injector. On Linux,libkaku.so
hooks__libc_start_main
and can be injected withLD_PRELOAD
. On load, the CLR will be bootstrapped on a separate thread and your entry point function will be called.
Note that the lifetime of the host process always outlives the lifetime of the .NET Runtime thread. If the host process does not live long enough for the .NET Runtime to bootstrap and finish execution, it will be killed along with the host process.
Waiting for the thread to join before the process quits is doable, but not implemented since this leads to some unexpected behaviour such as
zombie processes. If there seems to be such a need, please file an issue; I will consider making it configurable via kaku.co
.
To determine the .NET bootstrap point, sabinokaku requires a kaku.co
file either in the same directory as kaku.dll
/libkaku.so
,
or in the host process directory. kaku.co
contains the preamble necessary for sabinokaku to bootstrap the .NET runtime. There are 2
preamble formats that sabinokaku understands. The long format (kaku_l
) allows for the most flexibility, for example if you
store the .NET entry point assembly in a child folder. The short form may be preferred for its shorter syntax.
The long format always begins with kaku_l
, followed by the path to runtimeconfig.json
, the path to the .NET assembly DLL,
the qualified name of the entry-point class, and the name of the entry-point function. All paths are relative to the location of
kaku.co
.
kaku_l
TestInject.runtimeconfig.json
TestInject.dll
TestInject.EntryPoint, TestInject
Main
The short format always begins with kaku_s
. Information is then inferred with the syntax AssemblyName::QualifiedClassName$EntryFunction
. The
short format preamble requires that your assembly and runtime configuration file is in the same folder as kaku.co
.
kaku_s
TestInject::TestInject.EntryPoint!Main
After the preamble, you may provide optional environment variables to be set before hostfxr is invoked and the .NET runtime is bootstrapped.
The format is env KEY=VAR
, be sure there are no spaces between the equals symbol or it will be taken as part of the environment string.
For example, to set DOTNET_MULTILEVEL_LOOKUP=0
, an example kaku.co
may be
kaku_s
TestInject::TestInject.EntryPoint!Main
env DOTNET_MULTILEVEL_LOOKUP=0
Multiple environment variables can be set.
kaku_s
TestInject::TestInject.EntryPoint!Main
env DOTNET_MULTILEVEL_LOOKUP=0
env COMPLUS_ForceENC=1
The order of environment variables set is not guaranteed.
Any set variables will also take effect against the hosting process after initialization, and sabinokaku will not restore the prior values. See Platform Differences for more details.
After the preamble, you may optionally provide the path to your own hostfxr.dll
, which is resolved
relative to kaku.co
.
kaku_s
TestInject::TestInject.EntryPoint!Main
hostfxr runtime/host/hostfxr.dll
env DOTNET_MULTILEVEL_LOOKUP=0
sabinokaku will then try to load using your custom hostfxr.dll
. If it does not exist, the runtime will fail to bootstrap.
If you specify hostfxr
multiple times, only the first entry is taken.
After the preamble, you may optionally provide the path to a dotnet root folder containing a dotnet.exe
and a .NET
runtime, relative to kaku.co
. Be sure to set DOTNET_MULTILEVEL_LOOKUP=0
as well.
kaku_s
TestInject::TestInject.EntryPoint!Main
dotnetroot runtime
env DOTNET_MULTILEVEL_LOOKUP=0
sabinokaku will then try to load the runtime specified.
If you specify dotnetroot
multiple times, only the first entry is taken.
sabinokaku provides specialized functionality for initializing the CLR for Vulkan hooking purposes as a layer. Your Vulkan driver must support layer interface 2 (Vulkan 1.1).
To load the CLR during Vulkan instantiation, you must enable it by setting the vulkan
option in your kaku.co
. You
must specify a loader layer library version (must be greater than 2, or the layer will not load), and an entry point for the
runtime, either CreateDevice
(called at first vkCreateDevice
), or CreateInstance
(called at first vkCreateInstance
).
You must also set the environment variable ENABLE_SABINOKAKU_VULKAN=1
, this will also enable the layer with the included layer.json
manifest. This will disable injection via DllMain
or _libc_start_main
.
kaku_s
TestInject::TestInject.EntryPoint!Main
vulkan 2 CreateDevice
env SABINOKAKU_VULKAN_BOOTED=1
You may then configure kaku.dll
or libkaku.so
as a Vulkan layer. See the Vulkan documentation
for more information.
On the first load of the layer, sabinokaku will pass a Vulkan handle of the initialized VkInstance
or VkDevice
as the arguments
to the .NET entry point. VkInstance*
will always be the first handle passed. If CreateDevice
is the entrypoint, VkDevice*
will
be the second pointer passed. The memory ownership of the handles passed will become the CLRs, but since the allocator
is unknown in the managed context, it should be considered leaked memory.
sabinokaku will initialize the CLR only on the first calls to the layer function. To hook subsequent calls to vkCreateInstance
or
vkCreateDevice
, you must do so manually in managed code and hook the calls at the loader level. The returned pointers to VkInstance
and
VkDevice
can be used for hooking the instance or device call chain but must be updated if the Vulkan instance or device is recreated. If
an application creates multiple instances or devices in a short timeframe before managed code can hook, you will not be able to hook into
the subsequent call chains.
Because the Vulkan loader will reinitialize all layers on device recreation, you must also include env SABINOKAKU_VULKAN_BOOTED=1
in your kaku.co
to prevent the layer from being reinitialized (and thus CLR) on device recreation.
Particularly when using the environment variables feature, note the differences in load order between Windows and Linux.
Windows uses DllMain
and thus the host process is already executing when the runtime is injected. Changed environment
variables will thus be visible to the host process only if they are read after initialization.
Linux hooks __libc_start_main
and bootstraps the .NET runtime before main
, thus any environment variables set may be visible
to the target process main
.
On both platforms, .NET initialization and execution happens on a separate thread. However, if an uncaught exception occurs it is not allowed to cross the FFI boundary. To avoid undefined behaviour, the host application will be aborted.