ClrOxide
is a rust library that allows you to host the CLR and dynamically execute dotnet binaries.
I wanted to call it Kepler
for no particular reason, but there's already a package named kepler
in cargo. :(
I have been working on hosting CLR with rust on and off for 2 years now, and finally something clicked two weeks ago!
This library wouldn't be possible without the following projects:
- NimPlant and its execute assembly implementation
- The elegance with which
winim/clr
allows overwriting the output buffer forConsole.Write
and gets the output! Striving for the same elegance is the only reason this library took two years. How can I convince Cas to dabble with rust if he can't replicate this!? My work for a rust implant forNimPlant
is also how I got into this rabbit hole in the first place.
- The elegance with which
- go-clr by ropnop
- A very special thank you to ropnop here! This whole library is the result of 3 days of work thanks to something in
go-clr
that just made everything click for me!
- A very special thank you to ropnop here! This whole library is the result of 3 days of work thanks to something in
- dinvoke_rs by Kudaes
- Similar to
go-clr
, Kurosh'sdinvoke_rs
project also made some rust/win32 intricacies clearer and allowed the project to move forward.
- Similar to
- Various CLR-related rust libraries
- https://github.com/ZerothLaw/mscorlib-rs-sys
- https://github.com/ZerothLaw/mscoree-rs
- and likely a few more...
ClrOxide
only works if compiled for x86_64-pc-windows-gnu
or x86_64-pc-windows-msvc
.
Compiling for i686-pc-windows-gnu
fails due to known issues with rust panic unwinding. It might work with i686-pc-windows-msvc
, but I haven't tried it myself.
Although I haven't run into this issue myself, there might be cases where you need to specifically compile your assembly as x64
instead of Any CPU
.
windows
crate had no type definitions for mscoree.dll
until a few weeks ago. It looks like the definitions for mscoree.dll
have made their way into the windows
crate in version 0.48.0
. However, these definitions don't appear to be working correctly. Just as an example; a vtable entry that should point to a function within the CLR thread (let's say at address 0x7ffef16821a0
), somehow returns an address widely out of range (0x750003cac9053b48
).
The windows
crate does a lot of fancy stuff with vtables for safety, but ironically, these are likely causing the access violation above. Or something else is happening... I intended to use the official definitions for V2 to offload the maintenance burden, but this is a dealbreaker.
You can find more examples in the examples/
folder.
ClrOxide
will load the CLR in the current process, resolve mscorlib
and redirect the output for System.Console
, finally loading and running your executable and returning its output as a string.
Streaming the output is not currently supported, although I'm sure the CLR wrangling magic used for redirecting the output could be a good guide for anyone willing to implement it.
use clroxide::clr::Clr;
use std::{env, fs, process::exit};
fn main() -> Result<(), String> {
let (path, args) = prepare_args();
let contents = fs::read(path).expect("Unable to read file");
let mut clr = Clr::new(contents, args)?;
let results = clr.run()?;
println!("[*] Results:\n\n{}", results);
Ok(())
}
fn prepare_args() -> (String, Vec<String>) {
let mut args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Please provide a path to a dotnet executable");
exit(1)
}
let mut command_args: Vec<String> = vec![];
if args.len() > 2 {
command_args = args.split_off(2)
}
let path = args[1].clone();
println!("[+] Running `{}` with given args: {:?}", path, command_args);
return (path, command_args);
}
You can update the context to use a custom app domain. This can be useful if you want to avoid DefaultDomain
. Check out examples/custom_app_domain.rs
for more details.
...
let app_domain = clr.using_runtime_host(|host| {
let app_domain = unsafe { (*host).create_domain("CustomDomain")? };
Ok(app_domain)
})?;
clr.use_app_domain(app_domain)?;
...
We need to load the CreateInterface
function from mscoree.dll
to kickstart the CLR. You can provide a custom loader by disabling default features.
First, add default-features = false
to your dependency declaration.
clroxide = { version = "1.0.6", default-features = false }
And then provide a function with the signature fn() -> Result<isize, String>
that returns a pointer to the CreateInterface
function when creating the Clr instance.
litcrypt::use_litcrypt!();
fn load_function() -> Result<isize, String> {
let library = custom_load_library_a(lc!("mscoree.dll\0"));
if library == 0 {
return Err("Failed".into());
}
let function = custom_get_process_address(library, lc!("CreateInterface\0"));
if function == 0 {
return Err("Failed".into());
}
Ok(function)
}
fn main() -> Result<(), String> {
// ...
let mut context = Clr::new(contents, args, load_function)?;
// ...
}
You can use the building blocks provided by ClrOxide
to patch System.Environment.Exit
as described in Massaging your CLR: Preventing Environment.Exit in In-Process .NET Assemblies by MDSec.
You can check the reference implementation at examples/patch_exit.rs
. Since this requires using VirtualProtect
or NtProtectVirtualMemory
, I don't intend to add this as a feature to ClrOxide
.