Designed by thousands of monkeys with hundreds of typewriters
Buffer Overflows and You
for 64-bit Linux systems!

Introduction

Welcome to "Buffer Overflows and You." Today we're going to talk about computers. In particular, I'd like to focus on something called a "buffer overflow." What does that mean? Well, let's start by taking a step back and looking at your average computer program.

A modern computer generally runs one "master process," the operating system. The OS is responsible for managing all of the machine's resources, such as memory, disk, access to the video card and other peripherals, etc. The OS also provides an abstraction that makes it easier for developers to write programs to run on the computer without needing intimate knowledge about the machine's architecture.

Virtual memory

In the context provided by the operating system, many other "user" processes are running. Because multiple processes are running at what can be considered essentially the same time*, the OS must provide protection between processes. One protection mechanism that modern operating systems and architectures provide is called virtual memory. Through hardware support and additional code in the operating system, virtual memory allows each user process to act as though it is the only thing running on the computer. It gives each process a completely separate address space.

vm-small

This is facilitated through the use of page tables, as illustrated above. Page tables map the virtual addresses used by the running process into physical addresses that correspond to actual memory. It's good to know this information when looking at a particular process' address space. You'll notice that if you look at the memory map for two different processes there are a lot of common addresses. Let's try it out...

First let's create a really simple program. Copy this code into a file called "sample1.c"

#include <stdio.h>
#include <unistd.h>

int main() {
  char c;

  printf("I am a running instance, or process, of program 1.\n");
  printf("My PID is %d\n", getpid());
  printf("Press enter to exit...\n");
  c = getchar();

  return 0;
}

Now let's run it...

$ gcc -o sample1 sample1.c
$ ./sample1
I am a running instance, or process, of program 1.
My PID is 23814
Press enter to exit...

This creates a single instance of the program, called a process, with the shown PID. You'll probably see a different number for the PID. They're assigned sequentially as programs run. Let's look at this process' memory map in a separate terminal, with the process still running:

$ pmap 23814  # If you don't have pmap installed, use 'cat /proc/23814/maps'
23814:   ./simple1
0000000000400000      4K r-x--  /home/turkstra/cs526/simple1
0000000000600000      4K rw---  /home/turkstra/cs526/simple1
0000003191400000    120K r-x--  /lib64/ld-2.11.1.so
000000319161d000      4K r----  /lib64/ld-2.11.1.so
000000319161e000      4K rw---  /lib64/ld-2.11.1.so
000000319161f000      4K rw---    [ anon ]
0000003191800000   1468K r-x--  /lib64/libc-2.11.1.so
000000319196f000   2048K -----  /lib64/libc-2.11.1.so
0000003191b6f000     16K r----  /lib64/libc-2.11.1.so
0000003191b73000      4K rw---  /lib64/libc-2.11.1.so
0000003191b74000     20K rw---    [ anon ]
00007f09d02ce000     12K rw---    [ anon ]
00007f09d02f2000     12K rw---    [ anon ]
00007fff14cb7000     84K rw---    [ stack ]
00007fff14d00000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total             3812K

Let's look at the memory map for another running program, our shell...

$ pmap $$   # $$ is the pid of the currently running process (your shell)
27379:   -bash
0000000000400000    836K r-x--  /bin/bash
00000000006d0000     40K rw---  /bin/bash
00000000006da000     20K rw---    [ anon ]
0000000001dc4000    396K rw---    [ anon ]
0000003191400000    120K r-x--  /lib64/ld-2.11.1.so
000000319161d000      4K r----  /lib64/ld-2.11.1.so
000000319161e000      4K rw---  /lib64/ld-2.11.1.so
000000319161f000      4K rw---    [ anon ]
0000003191800000   1468K r-x--  /lib64/libc-2.11.1.so
000000319196f000   2048K -----  /lib64/libc-2.11.1.so
0000003191b6f000     16K r----  /lib64/libc-2.11.1.so
0000003191b73000      4K rw---  /lib64/libc-2.11.1.so
0000003191b74000     20K rw---    [ anon ]
0000003192000000      8K r-x--  /lib64/libdl-2.11.1.so
0000003192002000   2048K -----  /lib64/libdl-2.11.1.so
0000003192202000      4K r----  /lib64/libdl-2.11.1.so
0000003192203000      4K rw---  /lib64/libdl-2.11.1.so
00000031a0c00000    116K r-x--  /lib64/libtinfo.so.5.7
00000031a0c1d000   2048K -----  /lib64/libtinfo.so.5.7
00000031a0e1d000     16K rw---  /lib64/libtinfo.so.5.7
00007ffdbed49000     48K r-x--  /lib64/libnss_files-2.11.1.so
00007ffdbed55000   2044K -----  /lib64/libnss_files-2.11.1.so
00007ffdbef54000      4K r----  /lib64/libnss_files-2.11.1.so
00007ffdbef55000      4K rw---  /lib64/libnss_files-2.11.1.so
00007ffdbef56000  96452K r----  /usr/lib/locale/locale-archive
00007ffdc4d87000     12K rw---    [ anon ]
00007ffdc4da4000      8K rw---    [ anon ]
00007ffdc4da6000     28K r--s-  /usr/lib64/gconv/gconv-modules.cache
00007ffdc4dad000      4K rw---    [ anon ]
00007fff28c16000     84K rw---    [ stack ]
00007fff28d5f000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total           107920K

Notice how even though both processes are running at the same time, they both start at the same address - 0x00400000 in this example. This is possible because of virtual memory. They both start at the same virtual address. But each individual virtual address points to a different physical address as mentioned above.

Process memory layout

Okay, great! Let's talk about the general layout of a program in memory.
On your standard Linux system, it looks something like this [1] ...

mmap-small

It is worth noting that this layout is mostly for 64-bit systems. On 32-bit systems the shared libraries are usually found at the lowest address, followed by the text segment, then everything else.

The text, or code, segment contains the actual program and any statically linked libraries. On 64-bit systems it generally starts at 0x400000 (32-bit systems like to place it at 0x8047000).

The data and BSS segments come next. The data segment contains all initialized global variables as well as static strings (eg, those used in printf). The BSS, or "block started by segment" region holds all uninitialized global variables (those which by C convention are initialized automatically to 0).

After that comes the heap, where all memory obtained via malloc() is located. The heap grows upwards as more memory is requested.

Then we have any shared libraries such as the loader, libc, malloc, etc.

Finally we have the stack, which it should be noted grows downward as it expands.

There is no heap in our sample programs because we don't ever call malloc(). If you add a malloc() call to the sample program and run it again, you'll see that memory has also been allocated for a heap.

* Really, there's no need to get into a discussion about context switching, preemption, and multitasking. Really.