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

Modern defenses

Everything has been pretty cool up to this point. Sadly, it's time for reality. The attacks that we just talked about can no longer happen on modern systems. There are three reasons for this...

NX and Exec Shield

Modern architectures provide a "No eXecute" bit, which allows you to mark certain regions of memory as non-executable. If the stack is marked in this way, it is impossible to run shellcode that has been injected into a buffer on the stack. That said, you may be able to get around this by overflowing a heap buffer (but heaps are almost always non-executable now too) or by using a return-to-libc-style attack.

On older architectures that do not provide the NX bit, there is something called "exec shield," found on many Red Hat systems. It emulates the NX bit on systems that do not have hardware support for it. Other systems accomplish the same thing, just with a different name. Even newer versions of windoze have support for software emulation of the NX bit, called "Data Execution Prevention" (DEP).

If you recall from earlier, we actually turned this off for our programs. This can be accomplished by setting a flag in the actual binary (we can also shut it off system-wide, but there's really no reason to). You can use "execstack" to set the flag on existing binaries, or, if you're compiling a new binary you can pass the "-z execstack" flag on to gcc.

gcc StackGuard

gcc by default also adds extra code to programs to protect against buffer overflows in general. This extra code adds "special" values called canaries before and after the return address and checks to make sure they haven't been overwritten before proceeding to execute a return. We disable it by including the "-fno-stack-protection" when compiling.

Address space layout randomization (ASLR)

Let's take our sample program from the introduction, sample1, and run it a couple of times while looking at each memory map...

$ pmap 12662
12662:   ./sample1
0000000000400000      4K r-x--  /home/turkstra/src/cs526/sample1
0000000000600000      4K rw---  /home/turkstra/src/cs526/sample1
00000032e7000000    120K r-x--  /lib64/ld-2.11.1.so
00000032e721d000      4K r----  /lib64/ld-2.11.1.so
00000032e721e000      4K rw---  /lib64/ld-2.11.1.so
00000032e721f000      4K rw---    [ anon ]
00000032e7400000   1468K r-x--  /lib64/libc-2.11.1.so
00000032e756f000   2048K -----  /lib64/libc-2.11.1.so
00000032e776f000     16K r----  /lib64/libc-2.11.1.so
00000032e7773000      4K rw---  /lib64/libc-2.11.1.so
00000032e7774000     20K rw---    [ anon ]
00007f314ab04000     12K rw---    [ anon ]
00007f314ab28000     12K rw---    [ anon ]
00007fff06c3d000     84K rw---    [ stack ]
00007fff06d19000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total             3812K
$ pmap 12666
12666:   ./sample1
0000000000400000      4K r-x--  /home/turkstra/src/cs526/sample1
0000000000600000      4K rw---  /home/turkstra/src/cs526/sample1
00000032e7000000    120K r-x--  /lib64/ld-2.11.1.so
00000032e721d000      4K r----  /lib64/ld-2.11.1.so
00000032e721e000      4K rw---  /lib64/ld-2.11.1.so
00000032e721f000      4K rw---    [ anon ]
00000032e7400000   1468K r-x--  /lib64/libc-2.11.1.so
00000032e756f000   2048K -----  /lib64/libc-2.11.1.so
00000032e776f000     16K r----  /lib64/libc-2.11.1.so
00000032e7773000      4K rw---  /lib64/libc-2.11.1.so
00000032e7774000     20K rw---    [ anon ]
00007fbbe2954000     12K rw---    [ anon ]
00007fbbe2978000     12K rw---    [ anon ]
00007fff261f5000     84K rw---    [ stack ]
00007fff26282000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total             3812K

Notice how the address of the stack keeps changing? Well this is the final nail in the coffin. If the stack is no longer executable, we must rely on return-to-libc style attacks, and those almost always rely on knowing where to find a particular function (like system()) as well as a string to pass that function (eg, "/bin/sh"). With the stack's location randomized, the location of that string changes every time the program executes. This makes attacks much more difficult, particularly if the randomization is being done properly.

It's worth noting that ASLR only works in the context of a non-executable stack. If the stack is executable it's usually possible (as we did in the previous example) to develop a payload that does not strictly rely on any fixed addresses.