ClrOxide 是一个 Rust 库,允许你托管 CLR 并动态执行 dotnet 二进制文件。
我想叫它 Kepler,没什么特别的原因,但 cargo 里已经有一个叫 kepler 的包了。:(
我断断续续花了两年时间用 Rust 托管 CLR,直到两周前终于有了突破!
如果没有以下项目,这个库是不可能完成的:
winim/clr 用优雅的方式覆盖了 Console.Write 的输出缓冲区并获取输出!正是因为追求同样的优雅,这个库才花了两年时间。
如果 Cas 不能复现这个,我怎么说服他去尝试 Rust 呢?我为 NimPlant 编写 Rust 植入体的工作,也正是我最初进入这个兔子洞的原因。go-clr 中的某个东西让我豁然开朗!go-clr 类似,Kurosh 的 dinvoke_rs 项目也让我更清楚地理解了 rust/win32 的一些复杂之处,并推动了项目向前发展。ClrOxide 只有在针对 x86_64-pc-windows-gnu 或 x86_64-pc-windows-msvc 编译时才有效。
针对 i686-pc-windows-gnu 编译会因 Rust panic 展开的已知问题而失败。它可能可以在 i686-pc-windows-msvc 上工作,但我自己没有尝试过。
虽然我自己没有遇到过这个问题,但在某些情况下,你可能需要将程序集专门编译为 x64 而不是 Any CPU。
windows crate 直到几周前都没有 mscoree.dll 的类型定义。看起来 mscoree.dll 的定义已经进入了 0.48.0 版本的 windows crate。然而,这些定义似乎不能正常工作。举个例子:一个应该指向 CLR 线程内某个函数的 vtable 条目(假设地址为 0x7ffef16821a0),不知何故返回了一个远超出范围的地址(0x750003cac9053b48)。
windows crate 为了安全性在 vtable 上做了很多花哨的处理,但讽刺的是,这些很可能就是导致上述访问冲突的原因。或者可能发生了其他事情……我原本打算在 V2 中使用官方定义来减轻维护负担,但这是一个决定性障碍。
你可以在 examples/ 文件夹中找到更多示例。
ClrOxide 会在当前进程中加载 CLR,解析 mscorlib,重定向 System.Console 的输出,最后加载并运行你的可执行文件,并将其输出作为字符串返回。
目前不支持流式输出,不过我相信用于重定向输出的 CLR 调度魔法,对于任何愿意实现它的人来说都是一个很好的指南。
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);
}
你可以更新上下文以使用自定义应用程序域。如果你想避免使用 DefaultDomain,这会很有用。查看 examples/custom_app_domain.rs 了解更多细节。
...
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)?;
...
mscoree.dll 使用自定义加载器我们需要从 mscoree.dll 加载 CreateInterface 函数来启动 CLR。你可以通过禁用默认特性来提供自定义加载器。
首先,在依赖声明中添加 default-features = false。
clroxide = { version = "1.0.6", default-features = false }
然后在创建 Clr 实例时,提供一个签名为 fn() -> Result<isize, String> 的函数,该函数返回指向 CreateInterface 函数的指针。
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)?;
// ...
}
System.Environment.Exit 使其不退出
你可以使用 ClrOxide 提供的构建块来修补 System.Environment.Exit,如 MDSec 的 Massaging your CLR: Preventing Environment.Exit in In-Process .NET Assemblies 所述。
你可以在 examples/patch_exit.rs 查看参考实现。由于这需要使用 VirtualProtect 或 NtProtectVirtualMemory,我不打算将其作为 ClrOxide 的一个特性添加。