MBE is fun - lab6A walkthrough
I'll try to keep this one short.
What we are going to cover
We are not going to overwrite the saved RET on the stack (we're gonna have a different pointer available, without touching the stack protector). We are also going to:
- beat ASLR with an initial tiny little taste of brute force combined with a partial overwrite
- do some leaking
- do some offset-based calculations
- do some more overflowing and overwriting
- do some more leaking
- again some overflowing with overwriting
- then we'll call up our shell.
Looking at the source code of the target app (https://raw.githubusercontent.com/RPISEC/MBE/master/src/lab06/lab6A.c) I felt a bit confused, seeing how much code it has - comparing to previous apps.
Noticing a good deal of unused code reaffirmed my feeling that this app was either intended to be solvable in multiple ways or was expected to be solved in a very painful way, requiring multiple steps and gadgets to be used (which would mean that the originally intended solution was slightly more complicated than what I came up with).
For the sake of brevity, I am only going to bring up parts of the code I found relevant for getting arbitrary code execution.
First, there's a simple structure definition, holding two buffers and an integer:
OK, now the
main() function (this is where the
uinfo structure is instantiated, by the way):
From all of the above, we are in fact only interested in:
1) line 75: an instance of the
uinfo structure gets declared as a local variable, which means it's on the local stack of the
2) line 91: the address of the
print_listing() function is assigned to the
merchant.sfunc integer value
3) line 113: if we type '3', we call the function from the
merchant.sfunc address, passing the address of the
merchant structure as an argument.
4) line 107: if we type '1', we call the
We don't care about the
print_listing() function, we are not going to use it, neither anything else not mentioned so far.
setup_account() function. This is where our neat buffer overflow resides:
The vulnerability is sitting in the expression being the first argument to
temp is 128 bytes long,
user->name can be up to 32 bytes and the fixed
" is a " string is 6 bytes long, we are able to overflow the
user->desc buffer by 38 bytes.
If we look at the
uinfo structure definition again, we can see that the
sfunc pointer resides right after the
desc buffer, so it becomes clear how we are going to achieve execution control. We are actually going to exploit this three times to execute arbitrary code.
What's useful - a few gadgets
On line 69, there's a nice and very simple function
It's not called anywhere in the code, but it's definitely a good gadget for leaking. Will print any buffer pointed by the argument, until a nullbyte is encountered.
Also, on line 29, there's a definition of a strange function. This function does not get called anywhere from the rest of the program, clearly suggesting it being intended to be used as a gadget.
It simply writes 8 bytes of the buffer pointed by the value pointed by its argument (a pointer to a pointer) to the standard output:
In fact I found it quite handy using it as a gadget in leaking information needed for properly constructing the final code execution payload.
Leaking the address space layout
So, we want to overwrite the
sfunc integer with an address of the
print_name() function, as it appears to be the best (simplest) way to leak some memory.
This is how the stack looks like when the
setup_account() is called (with 31 'A' characters + newline as username, plus 90 'B' characters to fill the desc (32+90 = 128), to stop exactly before touching the original
sfunc value (the address of the
Let's see what offsets our functions have (output from gdb on a binary that was not run before, hence all bases are 0x00000 and only offsets are visible):
OK, so we can do a partial overwrite (by using 130 bytes instead of 132), only overwrite two least significant bytes of the pointer, leaving the base value (which we won't know at the time of exploitation) alone. This is a common ASLR bypass technique. We want to
The problem is that we can't simply overwrite half-bytes, only whole bytes. This means we have keep trying (brute force) with some arbitrary value of the first half-byte we do not know (because it's part of the base provided by ASLR), until we hit an instance of the program when in fact that half-byte will be equal to it, so overwriting it with our arbitrary value won't mess up the address.
'b' is the value I chose, as I saw it appearing in an actual address in gdb (see the screenshot above).
Hence I decided to try doing this partial overwrite to
print_name with an arbitrary value of
0xbbe2, whereas the first
be2 is the known offset of the `print_name` function while the preceding 'b' half-byte is a guess. First two most significant bytes are left intact (it's important to avoid sending out the trailing newline, as it will overwrite the third least significant byte with 0x0a and we definitely don't want that!):
To automate this a bit, the routine was put into a loop:
This does not need many attempts as there are only 16 possible values a half-byte can have.
If the address is incorrect, the program will crash right after calling the 'View info' option by sending '3'. If it does not crash,
print_name(&merchant) was successfully called, with the entire
merchant (name + desc + print_name_addr) content being printed out up until the nearest nullbyte down the stack.
And this is how it looks like:
This way we have leaked the entire base of the code segment, after guessing its least-significant half-byte. Now we can do calculations, so we know exactly the value we'll overwrite the
sfunc pointer next (we will NOT restart the program from now, but keep overflowing and calling from now on - no more bruteforce!), to achieve arbitrary code execution.
Again, the exploit snippet:
Calculating libc system() - the hard way
So, I obviously thought of the simplest
system("sh") similar to ret2libc. Let's just overwrite the
sfunc with the address of the libc
But how are we going to know what it is? Well, we can obviously calculate the offset between
So, in our libc
printf()'s address is
system()'s. Hence, all we'll need to do to achieve
system()'s address will be a subtraction of this value. Then another overwrite with
setup_account() and we should get our shell.
OK, where do we get that (
printf()'s address) value from? It should be in our address space (GOT, in the data segment), because
printf() is being used by our target program so it is definitely linked and already resolved in GOT by the PLT routine (the PLT routine is in the code segment, by the way).
A quick search showed that this is the case (the program was broken on a breakpoint at
setup_account(), so GOT was resolved already (0xb7707000 0xb7708000 was the range of the data segment in that instant):
The above also showed that the relevant GOT entry (0xb7707010) was located at offset 0x10 of the current base address of the data segment (0xb7707010 - 0xb7707000 = 0x10).
But then I thought: but how do we leak the data segment address?
I started looking at the code to notice that it is being passed on the stack, e.g. for the
ulisting-operating functions like
I could read that from the stack. But how do I leak the stack address first?
Oh fuck no, it looks like I am going to have to redirect the execution to that
make_note() function first and exploit it first? Nah, this is madness. There has to be an easier way!
Calculating libc system() from here - the easy way
So, below is a sample full output of the
vmmap command (this time addresses are slightly different than the ones earlier, this is due ASLR, nonetheless the same rules apply):
Notice something? The three consecutive segments marked red, are, respectively:
- the code segment
- the read only data segment
- the data segment
And they create a continuous range of addresses, which suggests they are aligned at fixed offsets from each other. Let's run the program several times and check if this is the case:
Yup. We can clearly see that
rodata share the same base. Awesome, looks like we found a shortcut.
rodata is is
0x2000 bytes greater than code, data is
0x1000 greater than rodata.
So, once we have the base for the code segment, we simply add
0x3000 to it and get the base for the data segment. Then we add the known offset and we know the address of
printf()'s GOT entry. So we know where to read from the libc
printf() address. Then we can calculate the address of
The exploitation algorithm from here
The first overflow allowed us to leak the base of the
code segment and calculate everything else we need for exploitation. Now we want to:
- Trigger the overflow for the second time, this time to overwrite the
sfuncvalue with the address of the
write_wrap()function (which is perfectly suited to leak the GOT after being provided its address, because the GOT itself is a pointer). With the GOT address put in front of the
namebuffer), so it becomes the argument to the
- Leak the
printf()libc address by calling the newly overwritten
- Trigger the overflow for the third time, this time to overwrite the
sfuncvalue with the address of
system(), while putting the arbitrary command in front of the
- Cll it!
How the second overflow unexpectedly failed and why. read() and strncpy() to the rescue.
So, the last surprise here was that the second overflow failed. Instead of leaking the GOT, the whole buffer was printed again. This meant that the
sfunc was not overwritten this second time and that in result of "pressing" '3',
print_name() was called once again.
After looking at the code I figured out why. The
merchant->desc buffers are initialized with nullbytes only before the
while loop and never again.
This means that after filling both buffers with non-null values, the next time
setup_account() calls this
strlen(user->desc) expression is going to return much more than 32 (as it did in the first call), because after the first overflow at least 128 bytes of the
user->desc buffer already contain non-null bytes. This will effectively make this second overflow go much further, starting overwriting beyond the pointer we want to overwrite.
Just before that
memcpy() happens, this is how
user->desc is impacted:
So if we need that
strlen(user->desc) to return less, this time we have to inject a nullbyte into the
user->name buffer (via the
read() call on line 60) and let it be propagated to
user->desc by the following
strncpy() call. Luckily both
strncpy() support this :D
After that - depending on which character we put the nullbyte at, the
strlen() call will return no more than 32, making the
sfunc pointer again within the reach of our overwrite. We just need to properly calculate how many bytes will there be to fill between the beginning of the
user->desc buffer and the
sfunc variable (the sum will always be 128).
merchant is the argument to the
sfunc() call, we put our arbitrary command (argument for
system()) in the beginning of the
merchant->name buffer, as it's the first field of the structure anyway):
And here we see the final action:
The full exploit code can be found here: