Out-of-bound read-write without integer sign flipping - MBE LAB8B walkthrough - the bonus version without using thisIsASecret() function
This is the continuation of https://hackingiscool.pl/out-of-bounds-write-with-some-integer-sign-flipping-mbe-lab8b-walkthrough-the-basic-version/ - the bonus version not utilizing the
thisIsASecret() function to get the shell.
So, the basic version was in fact very simple after figuring out how to control
EIP. We just overwrote it with a pointer to this function:
Now, since we want to avoid using it to get the bonus points, regardless to what approach we will take (e.g. a full ROP-shell
execve("/bin/sh") shellcode or a call to
system("/bin/sh")), we have to attain some sort of argument control, as an arbitrary
EIP just isn't enough.
How loadFave() really works
As mentioned previously, we can't print arbitrary vectors from the
faves array by calling their own
printFunc functions (like
Even though the target application does contain a function called
printFaves(), I did not find it to be much of a use (neither for code execution, leaking nor for stack-grooming):
The problem with execution control is that this function directly calls the
printVector() function, instead of using the
faves[i]->printFunc pointer - the pointer we can overwrite and break our way into execution control.
Thus, after creating a
v3 vector with arbitrary values and pushing it several times to the
faves array to achieve arbitrary
printFunc pointer values, in order to call any of those pointers first we have to load it to either of the two vectors
v2, explicitly asking the program to call
Now, notice the
memcpy() call's details:
memcpy(v, faves[i], sizeof(v));, NOT
memcpy(v, faves[i], sizeof(struct vector));
It does not copy the entire
fave[i] structure into
v2. Instead, it only overwrites
sizeof(v) - which is a pointer. So the entire
loadFave() operation only overwrites the first 4 bytes of the
vector structure - which happen to be the
Let's illustrate this step by step.
v1 with values of
v2 with values of
2, then sum them up, then add the sum to the faves several times, then load one of the faves back to
v2 and see how it changed.
Full code can be found here: https://github.com/ewilded/MBE-snippets/blob/master/LAB8/LAB8B/init_one_two_sum_load.py
So, after initializing the vectors, summing them up and loading the sum four times to faves, this is what
v2 look like:
Again, this is right BEFORE we load
Note that our
0x00000003 - and its other fields are as well just full of
v2 yet has its original values;
printVector and fields full of
Now, after calling
So, only the
printFunc pointer from the chosen fave is loaded. Everything else stays intact. When attaining execution control, we make the program call
v1.printFunc(v1). Since in the basic version we simply overwrote the
printFunc value with
thisIsASecret() address - which does need nor take any arguments, we simply did not care about them - and honestly I did not even notice this exact
loadFave() behavior until I started poking around a solution that does not involve calling
Controlling more than just EIP
OK fine, so we can make
v1.printFunc, doesn't really matter) an arbitrary value, for instance
system() - even though libc is ASLR-ed, we can leak the layout as already covered in the previous part: https://hackingiscool.pl/out-of-bounds-write-with-some-integer-sign-flipping-mbe-lab8b-walkthrough-the-basic-version/.
v2 as our vector of choice, this means that we would effectively call
system(v1). Now, let's think about it for a while.
system() takes one argument, expecting it to be a pointer to a string of system commands:
And we DO NOT control the pointer being passed to it (we can only chose between
v2) as its only argument:
So, once our arbitrarily chosen (e.g.
system()) function gets called,
v2 pointer is the argument. And again, it looks like this:
So, what happens when
system(0x80003100) is called? Well, it is going to try to execute
\x90\x31\xe6\xb7\x02\0x00 as a string (remember, endianess). So even though we fully control
short int v2.b, as well as further
long int and
long long int fields of the vector, the nullbyte padding the
char v2.a field stands in our way. The string terminates - and although we fully control it, its first four bytes are strictly dictated by the value of EIP we force the program into.
We could possibly get this working if
v2.a was at least two characters, instead of just one. In such case we would make them something like
; is just one of the shell command separators (by the way if you're interested in command and argument separators, see this https://github.com/ewilded/SHELLING), while
a is just another command. We could create a program/script named
/tmp and add
/tmp to our
$PATH before calling the target program. But we can't do this on just one byte.
We could try to add
$PATH and then put our arbitrary commands (like
cat /home/lab8A/.pass) to a script named exactly
\x90\x31\xe6\xb7\x02\, or whatever the current value of
system() would be at the time of executing the target program - after having it leaked (ASLR).
I tried this approach. Did not work due to some of the bytes in this value not fitting into acceptable range of characters allowed in file names.
It became clear I have to try something else. Spoiler alert; stuff described in below Looking for ROP gadgets and stack-pivoting vectors section eventually did not work, although it allowed me to notice a beautiful (only potential as not actually attainable) ROP scenario.
What eventually did work is described in in the section after.
Looking for ROP gadgets and stack-pivoting vectors
So I searched for some stack pivoting scenarios (like the one described here https://hackingiscool.pl/heap-overflow-with-stack-pivoting-format-string-leaking-first-stage-rop-ing-to-shellcode-after-making-it-executable-on-the-heap-on-a-statically-linked-binary-mbe-lab7a/).
None of the functions used in the program turned out useful for stack-grooming in a similar way as
print_index() in LAB7A - again described here https://hackingiscool.pl/heap-overflow-with-stack-pivoting-format-string-leaking-first-stage-rop-ing-to-shellcode-after-making-it-executable-on-the-heap-on-a-statically-linked-binary-mbe-lab7a/).
This is our sample stack at the moment of our execution takeover:
This time we do not seem to have any control over any of the stack values - unless we want to try to stuck our payload somewhere in the input buffer
argv. The problem is that we won't have a gadget that would point our
So I thought "OK we want to make ESP point somewhere at v1/v2/faves integer fields we control and put our ROP shellcode there".
These are the registers at the moment of our execution takeover (
EIP was set to
system() at the time):
EDX points at our v2 structure (its first four bytes,
printFunc, contained the address of
system() when the screenshot was taken).
We want a gadget like
mov edx esp; pop whatever; pop whatever; ret.
mov edx esp would set our stack to the top of
v2. The two following
pop instructions would take out the
v2.b dwords, so
signed short int) would become the top of the stack. Nah that's not good either, we can't control half of that value. Fuck.
I fired up
ropshell.py. I ran
generate /home/levels/lab8B, which generated
lab8.ggt file with gadgets. I loaded it with
load lab8.ggt. Ran the following search:
Fuck, VERY few (only 5
pop; ret;) gadgets. Extremely unlikely to find the one we need.
I checked them all, one by one, looking at different slightly lower starting offsets, to see the instructions above them - making sure they are still what they should be, as depending on the offset we can get different assembly, as instructions do not have fixed lengths and they simply occur one after another. Example below:
0x1676L: pop ebp ;;
Luckily, this can be done in an easier way (
OK, what about libc? I bet there's plenty of gadgets there! So I repeated the steps with
ropshell.py to generate gadgets from
OK, more like it.
By the way,
peda also offers some built-in ROP helpers itself:
So, back to our
mov edx esp:
Nah, not a chance.
Neither for a suitable
pop esp gadget:
Just to make sure the syntax is correct:
Now, this would pivot
And it would return to itself (a recursive ROP), as
printFunc happens to be the address of our gadget (initial
EIP control) and would be laying on the top of the stack once
ESP pointed at
v2 our new stack. The second execution of the gadget would result in popping the
printFunc from the stack, then putting
v2.a+v2.b (which have nullbytes we can't control) into
ESP. Shit, this is getting nowhere. At this point I felt stuck and decided to peek into Corb3nik's solutions (https://github.com/Corb3nik/MBE-Solutions/) - only to find out, to my surprise, that he did not make/publish the bonus version solution.
"Never use this function"
As this thing got under my skin and kept me awake at night, I came up with this while already drifting away to sleep: since
v2.b are standing in our way, let's use our
EIP control and the
v2 argument passed to it on the stack to deliver a new payload to
v2. I thought of
fgets(), only to find out it did not work - only to realize it is expecting three arguments, as I confused it with
gets() - which is exactly what we need here:
So the plan is to first calculate a new sum in such a way that its
system(), add it to
faves under the right offset (
4 is perfect) and we'll be able to load it into to
Then we enter new data into
v2 in such a way that when we sum
v2, we will achieve relevant consecutive
6 is perfect, by the way) pointer to be libc
Then we load it into
v2. Then we ask the program to print it, so
gets() is called, allowing us to overwrite the entire
v2 (and everything that follows it, although we won't need it). This is why it was important to do all the calculations and load the faves before this step - we want
v2 (except for its first bytes - the
printFunc pointer) to stay intact from now on - which is perfectly feasible with the way
loadFave() actually works, as we found out earlier.
gets(v2) is called, we overwrite it with something like
XXXX;/bin/sh. The values of the first four bytes are irrelevant (as long as they are not messing with
gets() input, so we don't want nullbytes or newlines).
We don't care about the first four bytes (e.g.
XXXX), as they will overwrite the current
printFunc pointer (
gets() address at the time).
We don't care about them as we will overwrite them once again in the next step, by calling
faves - so
system(), with the following bytes being
So - at this point we'll just ask the program to
v2 once again, making it call
0xb7e63190 is a sample address of
system() itself. The first command will obviously fail, as it refers to a nonexistent file, the second command should succeed.
So once again, the full algorithm:
v1the same way as so far, with values of
v2in such a way that after calling
i = 4), the same way we did so far with index
4is to avoid the negative signed integer hassle)).
v3to faves five times (because we want
v2in such a way, that when summed up with
v1, will make
v2to faves two more times (make
v2(this will overwrite its
- Ask the program to print
v2, overwriting it with
v2(this will overwrite its
- Ask the program to print
v2and get the shell without using the thisIsSecret() function - the bonus version.
One more tricky thing I spent a while debugging and wondering what was wrong: - it was crucial to use
p.send("2") instead of
p.sendline("2") after issuing
p.sendline("3") - which tells the program to print a vector.
Once receiving our
"3\n" it asks for the vector number by calling
The problem with this is that it is using
getchar() to read the vector number. So, if we send the number followed by a newline character, the number will be read, while the newline character will be pulled from our input as input to
gets(). And since
gets() treats newlines as terminators, it would effectively lead to
gets() writing an empty null-terminated string to our
v2 buffer. So it would basically overwrite the least significant byte of
v2.printFunc pointer with a nullbyte, without placing our shell command payload where we wanted it.
And what can I say - it works:
The full exploit code can be found here