Writing Sliver C2 Extensions in Rust

Luke Paris
Paradoxis
Published in
6 min readJan 20, 2024

--

TL;DR

Also suck at C, but want to be a l33t hacker? You can write Sliver extensions in Rust! They’re blazingly fast, require basically no dependencies, and are very resistant to runtime errors due to Rust’s compiler magic. You can find a template project here.

Introduction

Over the past couple of years I’ve been playing around with various C2 frameworks from Metasploit, all the way to Cobalt Strike. While these all had some pretty nice upsides, in my opinion most come with some pretty big downsides (looking at you, Sleep scripting language).

As such, I recently decided to pick up BishopFox’ Sliver C2, as it seemed to be a huge upgrades in terms of developer friendliness when trying to hack around the project itself.

In addition to this, I’m of the opinion that a fancy GUI is completely overrated, as the only real use it serves is seeing inter-connectedness of implants, which rarely exceeds four hops in my day-to-day use. So Sliver was a perfect match made in heaven.

The problem

There’s no beating around the bush with this one, so I’ll just say it as it is. I absolutely suck at C, .NET, and PowerShell (Yes, I know, my beloved colleague. It’s a skill issue, and I’m not afraid to admit it); which also just happen to be three super popular languages the security industry decided to base custom payloads around in C2 frameworks.

While I absolutely admire wizards that have memorized all Win32 API’s by heart and that can perform memory management in their sleep, I find the process of writing BOF’s very painful, and it tends to include hours of trial and error for me, as I try to figure out why the hell my beacon just crashed for the twentieth time (just to find out I mistakenly wrote DWORD instead of PDWORD at line 624 of my over-engineered BOF).

Rewrite it in Rust! ™

After having been voluntarily indoctrinated by No Boilerplate’s Rust series over the past couple of months, I decided to give a stab at the language and fell in love almost instantly.

While fighting the compiler upfront seems annoying, in my experience the tradeoff truly was worth it. Given that I want malware to be as stable as possible, I’ve only really encountered crashes or bugs when I decided to unwisely use unwrap() in places that had no error handling; or, as expected, places where I decided to be a silly goober that used unsafe because GitHub Copilot told me to do so and I was too tired to bother at 2 AM.

After looking around at Dominic Breuker’s amazing Sliver blog posts, I decided to take a stab at converting his C extension to Rust, which turned out to be pretty simple!

All Sliver requires is an entrypoint function, which passes a callback function that accepts a buffer of data. I stripped down the code from this blog post down to it’s bare components, and was left with something like this:

// Minimal Sliver Extension in C
//
// Compile with:
// x86_64-w64-mingw32-gcc extension.c -o extension.x64.dll -shared

#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>

// Sliver's magic
typedef int (*goCallback)(const char *, int);

__declspec(dllexport) int __cdecl entrypoint(
char *argsBuffer,
uint32_t bufferSize,
goCallback callback
);

int entrypoint(char *argsBuffer, uint32_t bufferSize, goCallback callback)
{
char *message = "Hello world!\n";
return callback(message, strlen(message));
}

// Required for compiling your DLL
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}

One cup of coffee later, I was left with this template for a Sliver extension in Rust :

// Minimal Sliver Extension in Rust
//
// Compile with:
// rustup target add x86_64-pc-windows-gnu
// rustc --crate-type cdylib --target x86_64-pc-windows-gnu extension.rs -o extension.x64.dll

use std::ffi::CString;
use std::os::raw::{c_char, c_int, c_void};
use std::str;

type GoCallback = extern "C" fn(*const c_char, c_int) -> c_int;

#[no_mangle]
pub extern "C" fn entrypoint(args_buffer: *mut c_char, buffer_size: u32, cb: GoCallback) -> c_int {

// Parse the arguments
let args: &str = match std::str::from_utf8(unsafe {
std::slice::from_raw_parts(args_buffer as *const u8, buffer_size as usize)
}) {
Ok(v) => v,
Err(_) => return 1,
};

// Return a greeting
let greeting = format!("Hello, {}!", args);
return callback(&greeting, cb);
}

fn callback(msg: &str, cb: GoCallback) -> c_int {
let msg = CString::new(msg.to_string()).unwrap();
cb(msg.as_ptr(), msg.to_bytes().len() as c_int)
}

#[no_mangle]
pub extern "C" fn DllMain(
_h_module: *mut c_void,
_ul_reason_for_call: u32,
_lp_reserved: *mut c_void,
) -> bool {
true
}

While I know that using unsafe is, well, unsafe, in this case it’s required given that we’re forced to accept input from another source we don’t have control over. Just pray to the gods Sliver passes your user input properly.

Note: This code is limited to Windows, but nothing is stopping you from porting it to Linux, the compiler will happily do so! The only current issue with Linux extensions, is that as of writing, Linux & MacOS support is yet to be added in Sliver (related issue).

Combine this with a fancy Makefile :

TARGETS := x86_64-unknown-linux-gnu x86_64-pc-windows-gnu


all: release

debug:
$(foreach target,$(TARGETS),cargo build --lib --target $(target);)
cp target/x86_64-pc-windows-gnu/debug/hello.dll hello.dll

release:
$(foreach target,$(TARGETS),cargo build --release --lib --target $(target);)
cp target/x86_64-pc-windows-gnu/release/hello.dll hello.dll

And of course an extension config file:

{
"name": "hello",
"command_name": "hello",
"version": "0.0.1",
"extension_author": "Paradoxis",
"original_author": "Paradoxis",
"repo_url": "https://github.com/Paradoxis/Sliver-Rust-Extension-Template",
"help": "Bare minimum extension template for Sliver, written in Rust.",
"entrypoint": "entrypoint",
"files": [
{
"os": "windows",
"arch": "amd64",
"path": "hello.dll"
}
]
}

Run make, load the resulting extension, and you now have yourself a Sliver extension, completely written in Rust!

Important note: using Sliver’s argument system like used in BOF’s does not work, anything you type after running your command is directly passed to the DLL itself. Not all hope is lost though, you can use clap!

It works!

Tips, Tricks, and Dragons

Shooting yourself in the foot

Of course, no code is perfect. Try to avoid using unwrap on unhandled errors, as well as using panic where possible. If you’re using third-party libraries (for instance, like Clap for argument parsing) make sure you use functions that do not panic unexpectedly, like Clap’s parsedoes).

Background tasks!

Fun fact! You can also use this to create long-lived background tasks! While this (yet again) does require the usage of unsafe due to the fact we’ll probably have to touch static variables (using lazy_static will not work as whatever you initialized will be overwritten). Just fire up a new thread, and keep track of your state somewhere, like so:

// Word of warning:
// This code has not been tested thoroughly

static mut GLOBAL_THING: Option<Arc<RwLock<Vec<i32>>>> = None;

#[no_mangle]
pub extern "C" fn entrypoint(args: *mut c_char, size: u32, cb: GoCallback) -> c_int {

// Ensure thing is set
unsafe {
if GLOBAL_THING.is_none() {
GLOBAL_THING = Some(Arc::new(RwLock::new(Vec::new())));
}
}

// here you can either read or write to the variable
// don't forget the closure as you might cause deadlocks
{
let thing = unsafe { &GLOBAL_THING };
// get / set values
}

// Or just spawn a thread and make a worker periodically write
// to your buffer. Just make sure you don't run out of memory :)
std::thread::spawn(...);
}

fn background_task() {
let thing = unsafe { &GLOBAL_THING };
// do something with thing
}

What about the size?

It’s no secret that Rust binaries can be big, but changing a few settings in your cargo.toml should help drasticly reduce your payload size. While Rust DLL’s will probably never reach the same size of BOF’s, my DLL’s usually end up being around 300KB using the following config:

[profile.release]
strip = "debuginfo"
opt-level = "z"
lto = true

--

--

Dutch cyber security specialist with a passion for software & penetration testing, my weapons of choice are Python and Linux.