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.

What's vulnerable

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 main() function

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 setup_account() function.

We don't care about the print_listing() function, we are not going to use it, neither anything else not mentioned so far.

Now, the setup_account() function. This is where our neat buffer overflow resides:

The vulnerability is sitting in the expression being the first argument to memcpy().

As 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 print_name():

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 print_name() function):

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 9e0 become be2.

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!):

A sneak peak of the exploit code

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 system() method.

But how are we going to know what it is? Well, we can obviously calculate the offset between system() and printf():

So, in our libc printf()'s address is 0xd0f0 above 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 make_listing().

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 data, code and 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 system().

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:

  1. Trigger the overflow for the second time, this time to overwrite the sfunc value 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 merchant object (name buffer), so it becomes the argument to the sunc(&merchant) call.
  2. Leak the printf() libc address by calling the newly overwritten sfunc.
  3. Trigger the overflow for the third time, this time to overwrite the sfunc value with the address of system(), while putting the arbitrary command in front of the merchant object (name buffer).
  4. 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->user and 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 memcpy:

the 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 read() and 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).

And since 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 exploit

The full exploit code can be found here:

https://github.com/ewilded/MBE-snippets/blob/master/LAB6A/exploit.py

No one really gives a shit about cookies and neither do I