Handle to main w/ dlopen
dlopen is a well known function used to load shared libraries at runtime.
void * dlopen(const char *filename, int flag);
It takes the path to the shared library that you want to load and a flag. You can read more on itsn man page.
Turns out you can pass NULL as path and that will instruct dlopen to return
a handle for the main program.
If path is NULL, then the returned handle is for the main program.
When given to dlsym(3), this handle causes a search for a symbol
in the main program, followed by all shared objects loaded at
program startup, and then all shared objects loaded by dlopen()
with the flag RTLD_GLOBAL.
This means that you can create a program that loads itself! Let’s sketch something quick
use std::ffi::CString;
// these are typically found in libc and libdl and will be resolved
// at runtime (aka dynamically linked).
unsafe extern "C" {
fn dlopen(filename: *const i8, flag: i32) -> *mut std::ffi::c_void;
fn dlsym(handle: *mut std::ffi::c_void, symbol: *const i8) -> *mut std::ffi::c_void;
fn dlerror() -> *const i8;
fn dlclose(handle: *mut std::ffi::c_void) -> i32;
}
const RTLD_NOW: i32 = 0x2;
// exposed
#[unsafe(no_mangle)]
pub extern "C" fn add(a: i32, b: i32) {
println!("{}", a + b)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
unsafe {
let handle = dlopen(std::ptr::null(), RTLD_NOW);
if handle.is_null() {
let err = dlerror();
if !err.is_null() {
let err_str = std::ffi::CStr::from_ptr(err).to_string_lossy();
return Err(format!("dlopen failed: {}", err_str).into());
}
}
println!("lib: {:p}", handle);
let add_ptr = dlsym(handle, CString::new("add").unwrap().as_ptr());
if add_ptr.is_null() {
let err = dlerror();
if !err.is_null() {
let err_str = std::ffi::CStr::from_ptr(err).to_string_lossy();
return Err(format!("error finding symbol: {}", err_str).into());
}
}
println!("add: {:p}", add_ptr);
let add_func: extern "C" fn(i32, i32) = std::mem::transmute(add_ptr);
add_func(5, 7);
dlclose(handle);
Ok(())
}
}
Make sure you instruct the rust compiler to export those symbols in .cargo/config.toml
[build]
rustflags = ["-C", "link-args=-Wl,-export_dynamic"]
The program above should give you a pretty good idea of what’s going on. At the
top we declare all the functions that will be dynamically linked by Rust, since
all of those are in libc we don’t need to do anything special there.
With that, we start by getting a handle of the main program by passing
std::ptr::null() to dlopen. We then use dlsym to lookup symbols in the
loaded library and we specifically search for add which is a function that
will be exported as symbol by rustc since we declared it pub extern "C".
If everything goes smoothly, we should at this point have two pointers, the latter points to a C function that we can call, and that’s exactly what we’re doing eventually.
I’m running the above on an M2 Mac and this is what I’m getting
$ cargo run
lib: 0xfffffffffffffffe
add: 0x100e5a2b8
12