
CLR을 호스팅하고 dotnet 바이너리를 실행할 수 있는 Rust 라이브러리입니다.
ClrOxide는 CLR을 호스팅하고 dotnet 바이너리를 동적으로 실행할 수 있게 해주는 rust 라이브러리입니다.
특별한 이유 없이 Kepler라고 부르고 싶었지만, cargo에 이미 kepler라는 패키지가 있습니다. :(
지난 2년 동안 rust로 CLR 호스팅 작업을 틈틈이 해왔는데, 마침내 2주 전에 뭔가가 맞물리기 시작했습니다!
이 라이브러리는 다음 프로젝트들이 없었다면 불가능했을 것입니다:
winim/clr이 Console.Write의 출력 버퍼를 덮어쓰고 출력을 가져오는 방식의 우아함! 그 동일한 우아함을 추구한 것이 이 라이브러리가 2년이나 걸린 유일한 이유입니다.
Cas가 이것을 재현할 수 없다면 어떻게 그를 설득해서 rust를 만져보게 할 수 있겠습니까!? NimPlant용 rust 임플란트 작업도 제가 이 토끼굴에 빠지게 된 계기였습니다.go-clr의 어떤 부분 덕분에 모든 것이 맞물려 3일 만에 완성된 결과물입니다!go-clr과 유사하게 Kurosh의 dinvoke_rs 프로젝트 역시 일부 rust/win32의 복잡한 부분을 명확히 해주었고 프로젝트를 진행할 수 있게 해주었습니다.ClrOxide는 x86_64-pc-windows-gnu 또는 x86_64-pc-windows-msvc로 컴파일된 경우에만 동작합니다.
i686-pc-windows-gnu로 컴파일하면 rust 패닉 언와인딩(panic unwinding)의 알려진 문제로 인해 실패합니다. i686-pc-windows-msvc에서는 동작할 수도 있지만, 직접 시도해보지는 않았습니다.
제가 직접 이 문제를 겪은 것은 아니지만, 어셈블리를 Any CPU 대신 x64로 특별히 컴파일해야 하는 경우가 있을 수 있습니다.
windows 크레이트는 몇 주 전까지만 해도 mscoree.dll에 대한 타입 정의가 없었습니다. mscoree.dll에 대한 정의가 0.48.0 버전의 windows 크레이트에 포함된 것으로 보입니다. 그러나 이러한 정의는 제대로 작동하지 않는 것 같습니다. 예를 들어, CLR 스레드 내의 함수(예: 주소 0x7ffef16821a0)를 가리켜야 하는 vtable 항목이 범위를 크게 벗어난 주소(0x750003cac9053b48)를 반환합니다.
windows 크레이트는 안전성을 위해 vtable에 많은 멋진 작업을 수행하지만, 아이러니하게도 이것이 위의 액세스 위반(access violation)을 일으키는 것 같습니다. 아니면 다른 무언가가 일어나고 있는 것일 수도 있습니다... V2에서는 유지보수 부담을 덜기 위해 공식 정의를 사용하려고 했지만, 이것은 결정적인 문제입니다.
더 많은 예제는 examples/ 폴더에서 찾을 수 있습니다.
ClrOxide는 현재 프로세스에 CLR을 로드하고, mscorlib를 해석(resolve)하며, 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용 사용자 지정 로더 사용CLR을 시작하려면 mscoree.dll에서 CreateInterface 함수를 로드해야 합니다. 기본 기능(default features)을 비활성화하여 사용자 지정 로더를 제공할 수 있습니다.
먼저 종속성 선언에 default-features = false를 추가하세요.
clroxide = { version = "1.0.6", default-features = false }
그런 다음 Clr 인스턴스를 생성할 때 CreateInterface 함수에 대한 포인터를 반환하는 fn() -> Result<isize, String> 시그니처의 함수를 제공하세요.
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 패치하여 종료되지 않게 하기
MDSec의 Massaging your CLR: Preventing Environment.Exit in In-Process .NET Assemblies에 설명된 대로 ClrOxide가 제공하는 구성 요소를 사용하여 System.Environment.Exit를 패치할 수 있습니다.
참조 구현은 examples/patch_exit.rs에서 확인할 수 있습니다. 이 작업에는 VirtualProtect 또는 NtProtectVirtualMemory 사용이 필요하므로 ClrOxide에 이 기능을 추가할 의도는 없습니다.