ret2.systems

The past couple of weeks I have been doing the training material offered by ret2.systems for binary exploitation. Having taught people binary exploitation for my Universities CTF team I thought it would be a good idea to try out some premium training on the topic myself. I could not be more happy with what ret2.systems offers in terms of training and platform. While it is expensive ($1000/ 3 months) without a student discount ($150/ 3 months) you'll get your moneys worth and then some.

The way ret2.systems teaches is by first introducing a topic and explaining the fundamentals in an interactive/kinetic way. After you have the fundamentals down you are let loose on three challenge binaries that are vulnerable to the topic you just learned about. In order to proceed to the next topic you must complete 2/3 challenges (I recommend doing all three). The rest of this write-up focuses on the final level of the ROP challenges as it was the first time I hit a brick wall and had to really think outside the box and get creative with my exploit chain. Not to say the previous challenges were not difficult (because all level three binaries are for each topic).

What I will not include is my actual exploit code but hopefully by reading this you will understand what you need to do to get past this binary and move onto the ASLR chapter.

Source Code (optional)

// gcc -g -I ../includes -O0 -o 06_level_3 06_level_3.c -ldl -no-pie#include <stdlib.h>#include <stdio.h>#include <string.h>#include "wargames.h"#include "06_level_3.h"#define STORAGE_SIZE 100// get a number from the user and store itint store_number(uint32_t * data){    uint32_t value = 0;    uint32_t index = 0;    // prompt the user for a number to store    printf(" Number (hex): ");    value = get_hex32();    // prompt the user for an index to store at    printf(" Index: ");    index = get_uint();    // some slots have been reserved for the service owner :-)    if(index % 3 == 1)    {        printf(" ************ ERROR ************\n");        printf("     This index is reserved!    \n");        printf(" ************ ERROR ************\n");        return 1;    }    // save the number to data storage    data[index] = value;    return 0;}// returns the contents of a specified storage indexint read_number(uint32_t * data){    uint32_t index = 0;    // get index to read from */    printf(" Index: ");    index = get_uint();    printf(" Number at data[%u] is 0x%x\n", index, data[index]);    return 0;}void main(){    int res = 0;    char cmd[64] = {};    uint32_t data[STORAGE_SIZE] = {};    init_wargame();    disable_system();    printf("------------------------------------------------------------\n");    printf("--[ DEP & ROP Level #3 - Number Storage v2.0                \n");    printf("------------------------------------------------------------\n");    printf("\n");    printf("  +------------------------------------------------------+\n"\           "  | Welcome to the ~ C L O U D ~ number storage service! |\n"\           "  |           ~~ Sorry, 32bit storage only! ~~           |\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"\           "  | The owner has reserved some storage for themself :-) |\n"\           "  +------------------------------------------------------+\n"\           "\n");    // command handler loop     while(1)    {        res = 1;        // prompt the user for a new command        printf("Input command: ");        fgets(cmd, sizeof(cmd), stdin);        cmd[strcspn(cmd, "\n")] = 0;     // strip newline        // select specified user command        if(!strncmp(cmd, "store", 5))            res = store_number(data);        else if(!strncmp(cmd, "read", 4))            res = read_number(data);        else // Anything else: quit            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));    }}

Information Gathering

checksec

Using checksec against the binary we see stack cookies, partial RELRO, and the NX bit set.

    Arch:      x86_64    RELRO:     Partial RELRO    Stack:     Stack Cookie Detected    NX:        NX Enabled    ASLR:      Disabled    PIE:       No PIE

int store_number(uint32_t * data);

Reviewing the source code it is revealed that the binary has a write-what-where vulnerability in the store_number() function below. The index that the user supplies is never bound checked. Since the data int array is passed by reference on the stack from main() we know that the RIP register the exploit chain will overwrite is main(). Lets a take look at main()'s stack to see what indexes we need to supply in order to overwrite RIP.

int store_number(uint32_t * data){    uint32_t value = 0;    uint32_t index = 0;    // prompt the user for a number to store    printf(" Number (hex): ");    value = get_hex32();    // prompt the user for an index to store at    printf(" Index: ");    index = get_uint();    // some slots have been reserved for the service owner :-)    if(index % 3 == 1)    {        printf(" ************ ERROR ************\n");        printf("     This index is reserved!    \n");        printf(" ************ ERROR ************\n");        return 1;    }    // save the number to data storage    data[index] = value;    return 0;}

main()'s stack

Using the built-in debugger and setting a break point in main() we can see our stack is 496 bytes long. Meaning our RIP offset is 504 bytes from RSP. However, this is not accurate enough since main() has two arrays on the stack: char64 and int100. The binary creator did us a solid and specified the int size for each index at 32bits/4bytes. Which leads us to now knowing that 464 of the 496 main() stack size is used up by two arrays. The remaining stack size is used for the 'int res' variable (4 bytes) and values that do not matter to the exploit (stack frame prologue/epilogue).

Breakpoint 0: 0x400fa6, main+319wdb> x/gx $rsp0x7fffffffebe0: 0x0000000000000000wdb> x/gx $rbp0x7fffffffedd0: 0x0000000000401050wdb> x/gx cmd0x7fffffffed80: 0x00000065726f7400wdb> x/gx data0x7fffffffebf0: 0x0000000000000001

The char64 (cmd) string starts at address 0x7fffffffed80 and int100 (data) starts at 0x7fffffffebf0. With this information we know what the main() stack looks like and what index's we need to use to overwrite the RIP register.

RSP --->|------------| 0x7fffffffebe0        |            |        --------------        |   int res  |        --------------        |  data[100] | 0x7fffffffebf0        --------------        |   cmd[64]  | 0x7fffffffed80        --------------        |            |        --------------        |   cookie   |RBP --->|------------| 0x7fffffffedd0        |   0x401050 |        --------------        |     RIP    | 0x7fffffffedd8        --------------        

RIP Write-What-Where Index

0x7fffffffedd8 - 0x7fffffffebf0 = 488 / 4 = 122 - 1 = 121.

Subtracting the starting address of RIP from the starting address of the int data100 array we get the difference of 488 bytes. Now we divide 488 by 4 because that is how many bytes each index is, and we get 122 indexes we need to index before reaching the start of the RIP address. Subtract 1 from 122 because we start counting at 0 in computer science, and we get the index number 121 for the least significant byte (LSB) of RIP and 122 for the most significant byte (MSB). The LSB being at a lower index than the MSB is due to the endianness being set to little.

RBP Write-What-Where Index

Following the same algorithm above we know that the RBP index is 119 (LSB) and 120 (MSB) respectively.

The Issue's

From the information gathering stage we have everything we need to start writing a ROP chain and get the flag. However, there are two issues: system() is disabled and any index that has a reminder of one with modulus 3 (index % 3) is reserved by the program in store_number(). Meaning, a ROP chain can not be stored in the data array, and we can not index the MSB of RBP.

RBP MSB Issue Discussion

If this exploit did not require a rop chain this would not be an issue. However, after looking at the ROP gadgets available we have to pivot the stack pointer with a "leave; ret" gadget as it is the only one that will allow complete control of the RSP register. The way the leave instruction works is by moving the current contents of RBP to RSP. Meaning, if we want to pivot to a memory region that has an address of 5 or more bytes we need to control the MSB of RBP. Failing to do so will result in a segmentation fault. Unfortunately, for us our stack address is using 6 bytes of data.

The Solution

We will tackle the bigger issue of the reserved indexes; system() being disabled is not that serious as we can make a system call to execve to execute a shell.

Overwriting RBP

Not being able to use the write-what-where vulnerability to the fullest extent forces us to become creative. Let's review real quick what we know about the binary and our exploit constraints:

  • Index 121 and 122 for RIP overwrite.
  • If index modulus 3 equals 1 we can not use that index.
  • 'leave; ret' is the only gadget we can use to control RSP correctly.
  • 'leave; ret' requires us to have complete control of RBP (as of now we do not).
  • NX bit is set
  • The char cmd64 array is way too big which makes it suspicious.
    • To big because the largest it should be is 6 to account for a null char for the string.

Okay so we have a binary that allows us to control the RIP register but not supply a proper ROP chain in a ROP challenge. What is going on? It's time to think outside the box and figure out how we can use the cmd string to hold our ROP chain then pivot to it.

gets()

After a coffee break and a walk I came back to tackle this hurdle. While on my walk it occurred to me that if I can only index RIP fully then I should only focus on that and leverage it to create another vulnerability and exploit that. This is where gets() comes into play; gets() is a vulnerable function that will read in any amount of bytes until a line feed or end of file is reached. Meaning, I can supply gets() address to RIP and then insert my ROP chain to syscall execve "/bin/sh". Since ASLR is disabled gets() will be at the same address every time the binary is executed. However, gets() needs a string supplied to it as it's one and only argument luckily RDI is already pointing to the cmd string from the previous user input.

Final Thoughts

After exploiting the initial write-what-where vulnerability by supplying RIP with the address of gets() the only thing left to do is overflow the cmd string with our ROP chain and pivot the stack to the starting address of the ROP chain. The ROP chain must be used to call execve since system() is disabled.

flag-obtained