unshell
A fully modular, pluggable framework for building cross-platform endpoint agents that integrate with existing toolsets.
Design Goals
- 100% Modular - Every component is replaceable at runtime. Nothing is hardcoded.
- Tool Integration - Drop in Metasploit payloads, Cobalt Strike beacons, or any external implant
- Cross-Platform - Full Rust cross-compilation support for Windows, Linux, macOS, and embedded targets
- Minimal Footprint - Compile-time obfuscation and size optimization for stealthy payloads
Philosophy
Nothing is fixed. Every part of the system is a plugin:
- Transports - TCP, HTTP, DNS, WebSocket, custom
- Protocols - Encryption, encoding, framing - all swappable
- Payloads - Metasploit, Cobalt Strike, custom - just load and run
- Components - Any Rust struct can be a module
- Communication - Tree-based routing with replaceable backends
Architecture
unshell/
├── src/tree/ # Hierarchical message routing
│ ├── component.rs # Component trait (implement for any module)
│ ├── endpoint.rs # Endpoint manager
│ ├── protocols/ # Pluggable protocol stack
│ └── tcp/ # Example transport implementations
├── ush-obfuscate/ # Compile-time string obfuscation
└── ush-payload/ # Test harness
Core Traits
Everything plugs into these abstractions:
Component - Any Module
use unshell::tree::Component;
use serde_json::Value;
pub trait Component: Send + Sync {
fn name(&self) -> &str;
fn status(&self) -> Value;
fn init(&mut self, config: Value) -> Result<(), String>;
fn shutdown(&mut self) -> Result<(), String>;
}
Protocol - Any Encoding Layer
use unshell::tree::protocols::Protocol;
pub trait Protocol: Send + Sync {
fn name(&self) -> &'static str;
fn encode(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError>;
fn decode(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError>;
}
Transport - Any Connection
// Transports connect to networks - TCP, HTTP, DNS, custom
// Implement send/recv and register with the transport registry
Payload - Any External Implant
// External payloads (Metasploit, Cobalt Strike, etc.) load as components
// They expose the same interface as native components
Module System
use unshell::{ModuleRuntime, Manager};
use unshell::config::RuntimeConfig;
// Define a module
pub struct MyModule;
// Implement runtime lifecycle
impl ModuleRuntime for MyModule {
fn init(&mut self, manager: Arc<Mutex<Manager>>) -> Result<()>;
fn is_running(&self) -> bool;
fn kill(self: Box<Self>);
}
// Export via FFI for dynamic loading
#[unsafe(no_mangle)]
pub fn get_components() -> Vec<NamedComponent> {
vec![NamedComponent { name: "mymodule", ... }]
}
Load compiled .so/.dll modules at runtime using libloading or in-memory via memfd_create.
Protocol Stacking
Layer protocols arbitrarily:
let mut stack = ProtocolStack::new();
stack.push(&ProtocolConfig::Base64(Default::default())).unwrap();
stack.push(&ProtocolConfig::Http(Default::default())).unwrap();
stack.push(&ProtocolConfig::Tcp(Default::default())).unwrap();
Order determines encoding: app → base64 → http → tcp → network
Integration Examples
Load a Metasploit Payload
// Load precompiled Metasploit .so
let module = Module::new("meterpreter.so")?;
// Or load from raw bytes (in-memory execution)
let module = Module::new_bytes(&meterpreter_bytes)?;
Use Cobalt Strike Beacon
// Beacon loads as a component with standard interface
let beacon = CobaltBeacon::new(config);
component_registry.register(Box::new(beacon)).unwrap();
// Communicate via tree messages - same as any other component
Custom Transport
// Implement Protocol trait
pub struct DnsTransport { ... }
impl Protocol for DnsTransport {
fn encode(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
// Encode as DNS TXT records
}
fn decode(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
// Decode DNS responses
}
}
// Register and use
stack.push(&ProtocolConfig::Custom { name: "dns", config: ... });
Cross-Compilation
# Windows x64
rustup target add x86_64-pc-windows-gnu
cargo build --target x86_64-pc-windows-gnu
# ARM64 Linux
rustup target add aarch64-unknown-linux-gnu
cargo build --target aarch64-unknown-linux-gnu
# macOS
rustup target add x86_64-apple-darwin
cargo build --target x86_64-apple-darwin
Building
# Standard build (~500KB)
cargo build
# Size-optimized (~50KB)
cargo build --profile minimize
# With obfuscation
cargo build --features obfuscate
Testing
cd ush-payload
cargo run
Obfuscation
Compile-time string obfuscation to evade static analysis:
use ush_obfuscate::symbol;
const API_KEY: &str = symbol!("SuperSecretKey123");
const C2_URL: &str = symbol!("https://C2Server/endpoint");
Roadmap
- Protocol registry for runtime registration
- Payload loader for common frameworks
- Transport abstraction layer
- Hot-swap components at runtime
Dependencies
libloading- Dynamic library loadingserde_json- Serializationcrossbeam-channel- Message passingbase64- Encodingthiserror- Error handling
License
MIT / Apache-2.0
Implementation Notes
Obfuscation Macros
The ush-obfuscate crate provides two macros for string obfuscation:
sym!()- For static strings that are used at runtime. Uses AES encryption.xor!()- For simple XOR obfuscation.
When the obfuscate feature is enabled, strings are encrypted at compile time. When disabled, they remain as plain strings.
Using Sym Constants
To avoid redundant sym!() calls, define constants in src/tree/symbols.rs:
use crate::obfuscate::sym;
pub const MY_CONSTANT: &'static str = sym!("MyString");
Then use the constant in your code:
use unshell::tree::symbols::*;
json!({ KEY_NAME: "value" })
Module Organization
src/tree/- Core tree system (routing, components, messages)ush-payload/src/- Protocol implementations, TCP client/server, connection management
Protocol-specific and transport-specific code belongs in ush-payload, while the tree system handles hierarchical message routing only.
Building for Size
./build.sh # Builds with minimize profile and strips debug sections
This produces a ~200KB binary (may vary with content).