What I learned writing a bof in rust
To preface I won’t be explaining how BOF/COFFs work. Instead I will just show you the process and headache of attempting to write a bof in rust.
Writing the entry point in rust
This seemed trivial. Most coffloaders will default to go
for the entry point. And to prevent the rust compiler from renaming the entry point we use #[no_mangle]
. Exposing one of the Beacon API functions, using the same method, with prefix __imp_
.
#[no_mangle]
pub unsafe extern "C" fn go(args: *const c_char, alen: i32) {
...
}
#[no_mangle]
unsafe extern "C" {
fn __imp_BeaconOutput(_: c_int, _: *const c_char, _: c_int);
}
compiling with rustc
RUSTFLAGS="-C target-cpu=x86-64 -C target-feature=+crt-static -C link-arg=-nostartfiles -C link-arg=-nodefaultlibs -C link-arg=-Wl,--gc-sections" \
rustc --target x86_64-pc-windows-gnu \
-C opt-level=z \
-C panic=abort \
-C debuginfo=0 \
-C strip=symbols \
-C codegen-units=1 \
-C embed-bitcode=no \
--emit=obj \
src/lib.rs -o objects/rust_part.o
Looking at the object file, this should work.
0x0000000000000000 __imp_BeaconOutput
0x0000000000000001 go
Running it inside of a Coffer loader. It fails to run the go
fn. For some reason this doesn’t work like ever. When I say I’ve tried so many things, I’ve tried writing the go
func using global_asm!
macro, using asm!
to align the stack, all of which confused me more and didn’t work.
I have tried and tried and still can’t figure it out. So what now.
Trying something else ??
My first idea was to keep the entry point in c and pass the beacon api functions to rust with FFI. Like this
//entry.c
// Extern Rust initialize fn
extern void initialize(
void (*beacon_output)(int, const char *, int),
void (*beacon_printf)(int, const char * fmt, ...),
char* args,
int alen
);
void go(char* args, int alen) {
// Pass the fn pointers to the rust wrapper
initialize(
BeaconOutput,
BeaconPrintf,
args,
alen
);
}
This ended up working pretty well. On the rust side
//lib.rs
pub type BeaconOutputFn = extern "C" fn(i32, *const c_char, i32);
pub type BeaconPrintfFn = extern "C" fn(i32, *const c_char, *mut c_char);
#[unsafe(no_mangle)]
pub unsafe extern "C" fn initialize(
beacon_output: BeaconOutputFn,
beacon_printf: BeaconPrintfFn,
// Arguments from Beacon
args: *mut c_char,
alen: c_int,
) {
let mut beacon = Beacon::new(beacon_output, beacon_printf, args, alen);
rust_bof(beacon);
}
//rust_bof.rs
pub fn rust_bof(mut beacon: Beacon) {
beacon.output("HELLOOOOOOOO");
}
Anndd this worked flawlessly, what in the actual fuck.
If anyone can explain to me why this works but the first method failed please hit me up on twitter.
As a Proof of concept this was fantastic as it proved I could actually write bofs this way. As the project grew I realized I wanted a more modular approach.
extern void rust_bof(char* args, int alen);
void go(char* args, int alen) {
rust_bof(args, alen);
}
extern void starOutput(int type, char* args, int alen) {
BeaconOutput(0, args, alen);
}
Now in rust
unsafe extern "C" {
fn starOutput(r#type: c_int, data: *const c_char, len: c_int);
...
}
//Create a wrapper fn
pub fn output(bytes: &str) {
unsafe {
(starOutput)(
0 as c_int,
bytes.as_ptr() as *const c_char,
bytes.len() as i32,
);
//Add null byte
(starOutput)(0 as c_int, core::ptr::null_mut() as *const c_char, 0 as i32);
}
}
Now use the wrapper fn
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_bof(args: *mut c_char, alen: c_int) {
output("HELOOOOOO");
}
Now this might seem redundant and over engineered but trust me this is the smallest implementation I have to date. Rust might not have been a great choice. But this idea was fun to build out. Once the functions are built out though, it’s easy to write reliable code to be used in a BOF.