1

This code is supposed to create a memfd (anonymous file), copy shellcode as a Vec<u8>, then finally execute using fexecve().

// A method that takes a u8 vector and copies it to a memfd_create file, then executes using fexecve()

use std::ffi::{CStr, CString};
use nix::sys::memfd::{memfd_create, MemFdCreateFlag};
use nix::unistd::fexecve;
use nix::unistd::write;

fn fileless_exec(code: Vec<u8>) {
    
    // Name using CStr 
    let name = CStr::from_bytes_with_nul(b"memfd\0").unwrap();

    // Create a new memfd file.
    let fd = memfd_create(&name, MemFdCreateFlag::MFD_CLOEXEC).unwrap();

    // Write to the file
    let _nbytes = write(fd, &code);

    // args for fexecve
    let arg1 = CStr::from_bytes_with_nul(b"memfd\0").unwrap();

    // enviroment variables
    let env = CString::new("").unwrap();

    // fexecve
    let _ = match fexecve(fd, &[&arg1], &[&env]) {
        Ok(_) => {
            println!("Success!");
        },
        Err(e) => {
            println!("Error: {}", e);
        }
    };
}

fn main() {

    // Read the file `hello_world` into a vector of bytes.
    let code = std::fs::read("/tmp/hello_world").unwrap();
    fileless_exec(code);
}

(hello_world is just a simple C hello world example).

The binary executes and writes to stdout normally. How would I capture the output as, say, a String in Rust? I've seen this example do it in C which is ultimately what I'm trying to achieve here.

The whole point here is to execute a file using its fd and capture its output. The input could be coming from anywhere (not always from disk as with the hello_world executable): from a web endpoint, other processes, etc.

I'm aware this code isn't that "Rust"-y.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
aminu_moh
  • 21
  • 1
  • It should be pretty much the same as you capture output from any other process. Remember that it's not mandatory to use `fexecve`; you can pass `/dev/fd/NNN` to APIs that expect a string filename. – o11c Nov 29 '21 at 05:29
  • You can do the same thing that the C example does. The `nix` crate (which you already use) should provide safe or mostly-safe wrappers for all the primitives you need, such as `pipe()` and `fork()`. – user4815162342 Nov 29 '21 at 08:36
  • BTW whenever I see `let _nbytes = write(fd, &code)`, I die a little on the inside. `write()` returns how much it has actually written, and it can be fewer than requested! A `write()` that is not part of a loop is almost certainly a bug waiting to happen. – user4815162342 Nov 29 '21 at 08:38

2 Answers2

0

So following some very bad practices I was able to make this:

// A method that takes a u8 vector and copies it to a memfd_create file.

use std::ffi::{CStr, CString};
use nix::sys::memfd::{memfd_create, MemFdCreateFlag};
use nix::unistd::{read, write, fexecve, dup2, close, fork};


fn fileless_exec(code: Vec<u8>, fd_name: &[u8], stdout: &mut String) {
    
    // Name using CStr 
    let name = CStr::from_bytes_with_nul(fd_name).unwrap();

    // Create a new memfd file.
    let fd = memfd_create(&name, MemFdCreateFlag::MFD_CLOEXEC).unwrap();

    // Write to the file
    let _nbytes = write(fd, &code);

    // args for fexecve
    let arg1 = CStr::from_bytes_with_nul(fd_name).unwrap();

    // enviroment variables
    let env = CString::new("").unwrap();

    // to capture the output we need to use a pipe
    let pipe = nix::unistd::pipe().unwrap();

    unsafe {
        let mut output = [0u8; 1024];
        
        // fork and exec
        let pid = fork().unwrap();

        if pid.is_child() {
            
            // dup the read end of the pipe to stdout
            dup2(pipe.1, nix::libc::STDOUT_FILENO).unwrap();
            
            // close the write end of the pipe
            close(pipe.0).unwrap();

            // close the read end of the pipe
            close(pipe.1).unwrap();

            // fexecve
            fexecve(fd, &[&arg1], &[&env]).unwrap();
        } else {

            // close the read end of the pipe
            close(pipe.1).unwrap();

            // write to the pipe
            let _nbytes = read(pipe.0, &mut output);

            // close the write end of the pipe
            close(pipe.0).unwrap();

            // convert output to a string
            *stdout = String::from_utf8(output.to_vec()).unwrap();
        }
    }
}

fn main() {

    // Read the file `/bin/ls` into a vector of bytes.
    let code = std::fs::read("/bin/ls").unwrap();
    let mut output = String::new();
    

    fileless_exec(code, b"anonymous\0", &mut output);

    print!("File output: {}", output);
}

This works for now... thanks for answers

aminu_moh
  • 21
  • 1
  • I see no bad practices (other than not checking the result of `write()`, already mentioned). There's a bug though: you have a single `read()`, which won't work for programs that write output in multiple writes, or those whose output is larger than the pipe buffer. You can use `File::from_raw_fd()` to create a `File` from `pipe.0` and then read it to the end using `File::read_to_end()`. – user4815162342 Nov 29 '21 at 08:51
0

Somewhat old question, but I couldn't find a better answer anywhere. If you want to do this, there is now a crate memfd-exec to do exactly this!

For example (from the docs) we can download an execute a program without ever writing it to disk:

use memfd_exec::{MemFdExecutable, Stdio};
use reqwest::blocking::get;

const URL: &str = "https://novafacing.github.io/assets/qemu-x86_64";
let resp = get(URL).unwrap();

// The `MemFdExecutable` struct is at near feature-parity with `std::process::Command`,
// so you can use it in the same way. The only difference is that you must provide the
// executable contents as a `Vec<u8>` as well as telling it the argv[0] to use.
let qemu = MemFdExecutable::new("qemu-x86_64", resp.bytes().unwrap().to_vec())
    // We'll just get the version here, but you can do anything you want with the
    // args.
    .arg("-version")
    // We'll capture the stdout of the process, so we need to set up a pipe.
    .stdout(Stdio::piped())
    // Spawn the process as a forked child
    .spawn()
    .unwrap();

// Get the output and status code of the process (this will block until the process
// exits)
let output = qemu.wait_with_output().unwrap();
assert!(output.status.into_raw() == 0);
// Print out the version we got!
println!("{}", String::from_utf8_lossy(&output.stdout));

I am the author of memfd-exec.

novafacing
  • 86
  • 1
  • 2