For this lab, we are given a program and its corresponding source code:
If we run checksec, we can see that NX, PIE and full RELRO are enabled.
Unfortunately, this is a statically linked executable, so we cannot leverage the partial RELRO to perform a GOT overwrite.
However, the fact that this binary is not compiled with PIE tells us that we do not need an address leak to be able to find valid ROP gadgets from the binary.
This program allows us to create messages which are subsequently xor’d with a randomly generated 32-byte one-time pad.
We can edit these messages, destroy them, and print out their details.
The first vulnerability we can see is a heap overflow introduced in the following length check. For clarity, I’ve removed code that doesn’t concern us.
When a user attempts to create a message, the program asks the user to specify a message length.
This length is then checked by dividing it by the global BLOCK_SIZE, which is 4, and comparing the result to the global MAX_BLOCKS which is 32.
If the result is greater than 32, then the message length is automatically truncated to 128 bytes.
The final message length is then used to specify the number of bytes to read into the 128 byte unsigned int message buffer.
But what happens if we specify the message length to be a value between 128 and 132 bytes? For example, 131 bytes?
In the C programming language, the result is always floored for the division of positive integers. Therefore, if we specify 131 bytes, we will still pass the length check since 131/4 = 32.
This gives us a 3-byte overflow primitive!
And what comes after the unsigned int message buffer?
The message length!
We can use this heap overflow primitive to overwrite our message’s own msg_len member and make it arbitrarily large up to 0xffffff.
We can subsequently use this newly overwritten length later in the edit_message() function to perform another heap overflow and corrupt any data after our initial heap chunk, including the print_msg() function pointer of the next chunk, allowing us to control EIP when we print out the message details of the 2nd chunk.
Here is what 2 allocated messages look like in the heap, before editing the 1st message. Notice the corrupted size of 0x414141 at 0x80f1af4.
And here is what the same chunks look like after we overflow our message data from the 1st chunk to the 2nd chunk using the 1st chunk’s corrupted size to replace its original message with 2000 B’s
Usually at this point we would replace the print_msg() function pointer of the 2nd chunk with a pointer to system(), but that will not work in this case, since we also need “/bin/sh” to be passed in as the argument, and messages[i]->print_msg(messages[i]); uses a function pointer on the stack that we can’t control as its argument to pass into the function pointer.
Additionally, this binary is statically compiled and system() does not exist in it.
Therefore, we need to generate a shell by using ROP. This is where things get interesting.
I found getting from control of EIP to spawning a shell to be the most challenge part of this lab.
When we have control of EIP, this is what our context looks like:
At first glance, it doesn’t seem like we control any data on the stack.
So, in order to ROP, we need to do a stack pivot to the heap, which is where we control data. There we can place our ROP chain gadgets to spawn a shell.
Unfortunately, I could not find any gadgets to successfully do this. Especially since we can only use 1 gadget to pivot to the heap. We would also ideally need to pivot to an address around 0x80f1b00, but not to 0x80f1b00 itself, since that is where our initial stack pivot gadget will be, especially if our pivot involves an xchg.
After some help, I realized that we actually can control some data on the stack, through another vulnerability.
When we print out a message, the programs asks us to specify the index for the message we’d like to print.
However, the code block responsible for checking this user provided input is vulnerable.
Notice how the program first calls fgets() to copy the user provided message index to the 32 byte char numbuf buffer.
strtoul() is then called on the string stored inside the numbuf buffer to convert it to an int.
The resulting int is then compared to MAX_MSG to determine whether or not is is a valid message index.
However, strtoul()’s default behavior allows extra data to be stored after an integer.
For example, calling strtoul("1CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", NULL, 10); will still return 1!
This allows us to pass the message index check while at the same time, placing values that we control onto the stack.
Being able to write extra data on the stack affords us multiple gadgets we can now use to set up our stack pivot, instead of limiting us to only 1 gadget.
From there, it is a simple matter of generating the right gadgets and determining the right offsets within our user input to place them at.
Putting everything together, the following exploit will grant us a shell.
NOTE: After solving this challenge, someone pointed out to me that I could’ve solved it using 2 gadgets instead of 3 gadgets. GADGET 2 is actually not needed.Had I replaced GADGET 2 with GADGET 3, I would’ve returned to GADGET 1, but that is OK, because GADGET 1 moves up the stack anyway, placing us in a good position to begin our ROP chain.