From dae1d524bcb2f810ff1027692ef47728dbaf367d Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:13:06 -0700 Subject: [PATCH] Load ELF from memory using memfd_create --- testproj/Cargo.lock | 7 ++++ testproj/Cargo.toml | 22 +++++++++++ testproj/src/lib.rs | 30 +++++++++++++++ unshell-breakout-module/Cargo.lock | 60 +++++++++++++++++++++++++++++ unshell-breakout-module/src/lib.rs | 1 - unshell-lib/Cargo.lock | 60 +++++++++++++++++++++++++++++ unshell-lib/Cargo.toml | 2 + unshell-lib/src/config/mod.rs | 12 +++++- unshell-lib/src/lib.rs | 2 +- unshell-lib/src/module/mod.rs | 2 + unshell-lib/src/module/module.rs | 34 ++++++++-------- unshell-lib/src/module/proc_load.rs | 56 +++++++++++++++++++++++++++ unshell-payload/Cargo.lock | 60 +++++++++++++++++++++++++++++ unshell-payload/src/main.rs | 13 ++++++- 14 files changed, 340 insertions(+), 21 deletions(-) create mode 100644 testproj/Cargo.lock create mode 100644 testproj/Cargo.toml create mode 100644 testproj/src/lib.rs create mode 100644 unshell-lib/src/module/proc_load.rs diff --git a/testproj/Cargo.lock b/testproj/Cargo.lock new file mode 100644 index 0000000..8362f7b --- /dev/null +++ b/testproj/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "testproj" +version = "0.1.0" diff --git a/testproj/Cargo.toml b/testproj/Cargo.toml new file mode 100644 index 0000000..7173ae5 --- /dev/null +++ b/testproj/Cargo.toml @@ -0,0 +1,22 @@ +cargo-features = ["trim-paths"] + +[package] +name = "testproj" +version = "0.1.0" +edition = "2024" + + +[lib] +crate-type = ["cdylib"] + +[dependencies] + + +[profile.release] +strip = true # Strip symbols from the binary +opt-level = "z" # Optimize for size +lto = true # Link tree optimization +codegen-units = 1 +panic = "abort" +debug = false # Remove debug +trim-paths="all" diff --git a/testproj/src/lib.rs b/testproj/src/lib.rs new file mode 100644 index 0000000..d60ebb1 --- /dev/null +++ b/testproj/src/lib.rs @@ -0,0 +1,30 @@ +#![no_std] +#![crate_type = "cdylib"] + +use core::panic::PanicInfo; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[unsafe(no_mangle)] +fn a() -> i32 { + 1 +} + +#[repr(C)] +pub struct S { + a: u64, + b: u32, + c: u16, + d: u8, +} + +#[unsafe(no_mangle)] +pub extern "C" fn test_identity_struct(x: S) -> S { + x +} + +#[unsafe(no_mangle)] +pub static HELLO: &str = "Hello!"; diff --git a/unshell-breakout-module/Cargo.lock b/unshell-breakout-module/Cargo.lock index 218ae54..cebb7be 100644 --- a/unshell-breakout-module/Cargo.lock +++ b/unshell-breakout-module/Cargo.lock @@ -315,6 +315,15 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -339,6 +348,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.12.2" @@ -490,7 +528,9 @@ version = "0.0.0" dependencies = [ "bincode", "chrono", + "libc", "libloading", + "rand", "serde", "serde_json", "unshell-obfuscate", @@ -642,3 +682,23 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/unshell-breakout-module/src/lib.rs b/unshell-breakout-module/src/lib.rs index 275a317..b5113e3 100644 --- a/unshell-breakout-module/src/lib.rs +++ b/unshell-breakout-module/src/lib.rs @@ -1,5 +1,4 @@ // Behold! The world's most sophisticated shared library! (it has no code) - #![no_main] pub use unshell_lib::get_components; diff --git a/unshell-lib/Cargo.lock b/unshell-lib/Cargo.lock index 24f83b1..643947c 100644 --- a/unshell-lib/Cargo.lock +++ b/unshell-lib/Cargo.lock @@ -315,6 +315,15 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -339,6 +348,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.12.2" @@ -483,7 +521,9 @@ version = "0.0.0" dependencies = [ "bincode", "chrono", + "libc", "libloading", + "rand", "serde", "serde_json", "unshell-obfuscate", @@ -635,3 +675,23 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/unshell-lib/Cargo.toml b/unshell-lib/Cargo.toml index 298bc03..0b01700 100644 --- a/unshell-lib/Cargo.toml +++ b/unshell-lib/Cargo.toml @@ -23,3 +23,5 @@ chrono = "0.4.42" serde = {version = "1.0.228", features=["derive"]} serde_json = "1.0.145" +libc = "0.2.177" +rand = "0.9.2" diff --git a/unshell-lib/src/config/mod.rs b/unshell-lib/src/config/mod.rs index 4edee4c..b283ba5 100644 --- a/unshell-lib/src/config/mod.rs +++ b/unshell-lib/src/config/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Debug}; // use bincode::{Decode, Encode}; // use serde::{Deserialize, Serialize}; @@ -40,6 +40,16 @@ pub struct NamedComponent { ), } +impl Debug for NamedComponent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NamedComponent") + .field("name", &self.name) + // .field("get_interface", &self.get_interface) + // .field("start_runtime", &self.start_runtime) + .finish() + } +} + /// Trait that wraps the get_interface() function inside of components pub trait InterfaceWrapper: Send + Sync { fn get_interface(&self) -> Option diff --git a/unshell-lib/src/lib.rs b/unshell-lib/src/lib.rs index 27742d9..83e798c 100644 --- a/unshell-lib/src/lib.rs +++ b/unshell-lib/src/lib.rs @@ -10,7 +10,7 @@ mod components; pub use components::get_components; mod announcement; -use std::fmt; +use std::fmt::{self, Debug}; pub use announcement::Announcement; diff --git a/unshell-lib/src/module/mod.rs b/unshell-lib/src/module/mod.rs index 8cd0f2c..82e0a03 100644 --- a/unshell-lib/src/module/mod.rs +++ b/unshell-lib/src/module/mod.rs @@ -1,6 +1,8 @@ mod manager; mod module; +mod proc_load; + // use std::any::Any; // pub use logger::setup_logger; diff --git a/unshell-lib/src/module/module.rs b/unshell-lib/src/module/module.rs index 7e8479f..77bfedb 100644 --- a/unshell-lib/src/module/module.rs +++ b/unshell-lib/src/module/module.rs @@ -1,5 +1,6 @@ use libloading::{Library, Symbol}; +use crate::module::proc_load::memfd_create_dlopen; use crate::{ModuleError, logger::SetupLogger, logger::logger}; use crate::*; @@ -22,26 +23,27 @@ impl Module { Ok(this) } + + // TODO: Implement actual reflective ELF loading (possibly even custom format) + // Look at https://github.com/weizhiao/rust-elfloader + pub fn new_bytes(bytes: &[u8]) -> Result { + let lib = + memfd_create_dlopen(bytes).map_err(|e| ModuleError::Error(e.to_string().into()))?; + + let this = Self { lib }; + + if let Ok(setup_logger) = this.get_symbol::(b"setup_logger") { + setup_logger(logger()); + } else { + warn!("setup_logger not found"); + } + + Ok(this) + } pub fn get_symbol(&self, symbol: &[u8]) -> Result, ModuleError> { let symbol = unsafe { self.lib.get::(symbol) } .map_err(|e| ModuleError::LinkError(format!("Failed to load symbol: {}", e)))?; Ok(symbol) } - // pub fn get_id(&self) -> &str { - // self.id - // } - // pub fn get_interface(&self) -> Result { - // if let Ok(interface_function) = self.get_symbol:: T>(b"interface") { - // Ok(interface_function()) - // } else { - // Err(ModuleError::LinkError(format!( - // "Interface function not found!" - // ))) - // } - // } } - -// extern "C" fn test1234() { -// info!("Test1234!"); -// } diff --git a/unshell-lib/src/module/proc_load.rs b/unshell-lib/src/module/proc_load.rs new file mode 100644 index 0000000..2cab96a --- /dev/null +++ b/unshell-lib/src/module/proc_load.rs @@ -0,0 +1,56 @@ +// Load a shared object by saving bytes to a filesystem in /proc + +use std::{error::Error, ffi::CString, io}; // 0.8 + +use libloading::Library; + +use crate::debug; + +// The `memfd_create` syscall flags (MFD_CLOEXEC is common and good practice) +const MFD_CLOEXEC: u32 = 0x0001; +const MFD_ALLOW_SEALING: u32 = 0x0002; + +pub fn memfd_create_dlopen(payload: &[u8]) -> Result> { + use rand::distr::{Alphanumeric, SampleString}; + + let string = Alphanumeric.sample_string(&mut rand::rng(), 16); + + // 1. Create the anonymous in-memory file descriptor using the raw syscall + let c_name = CString::new(string).expect("CString conversion failed"); + + let fd = unsafe { libc::memfd_create(c_name.as_ptr(), MFD_CLOEXEC | MFD_ALLOW_SEALING) }; + + if fd < 0 { + return Err(io::Error::last_os_error().to_string().into()); + } + + // 2. Write the payload bytes to the in-memory file + let bytes_written = + unsafe { libc::write(fd, payload.as_ptr() as *const libc::c_void, payload.len()) }; + + if bytes_written != payload.len() as isize { + // If write fails or is incomplete, clean up the file descriptor + unsafe { + libc::close(fd); + } + return Err("Failed to write full payload to memfd".into()); + } + + // Optional: Seal the file to prevent modification, common for security/integrity + // Note: The MFD_ALLOW_SEALING flag must be set during creation for this to work. + let seals = libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_WRITE; + if unsafe { libc::fcntl(fd, libc::F_ADD_SEALS, seals) } == -1 { + // Log a warning but continue if sealing fails (e.g., due to permissions) + debug!( + "memfd_create_dlopen: Failed to apply seals. Error: {}", + io::Error::last_os_error() + ); + } + + // 3. Construct the virtual path to the in-memory file + // This path is necessary for dlopen to work, as dlopen expects a filesystem path. + let dl_path = format!("/proc/self/fd/{}", fd); + + // 4. Use dlopen (via libloading) on the virtual path + Ok(unsafe { Library::new(&dl_path)? }) +} diff --git a/unshell-payload/Cargo.lock b/unshell-payload/Cargo.lock index 012dedf..bbe0206 100644 --- a/unshell-payload/Cargo.lock +++ b/unshell-payload/Cargo.lock @@ -321,6 +321,15 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -345,6 +354,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.12.2" @@ -489,7 +527,9 @@ version = "0.0.0" dependencies = [ "bincode", "chrono", + "libc", "libloading", + "rand", "serde", "serde_json", "unshell-obfuscate", @@ -650,3 +690,23 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/unshell-payload/src/main.rs b/unshell-payload/src/main.rs index 55213cf..f81bf16 100644 --- a/unshell-payload/src/main.rs +++ b/unshell-payload/src/main.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use std::fs::File; +use std::{collections::HashMap, io::Read}; use lazy_static::lazy_static; use unshell_lib::{ @@ -38,7 +39,15 @@ fn main() { let mut modules = Vec::new(); for arg in args.skip(1) { debug!("Loading module: {}", arg); - modules.push(Module::new(&arg)?) + + let mut file = File::open(arg).map_err(|e| ModuleError::Error(e.to_string().into()))?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer) + .map_err(|e| ModuleError::Error(e.to_string().into()))?; + + modules.push(Module::new_bytes(&buffer)?) + + // modules.push(Module::new(&arg)?) } // Run the manager, this is blocking.