Stack-canary (ROP), format string leak plus how I learned that nullbyte is not a badchar to scanf("%s",buf) - while socat ignores read on STDIN - MBE LAB8A
This time we are having some fun with a standard null-armored stack canary, as well as an additional custom one (we will extensively cover both scenarios, as there's plenty of subject matter here), plus some peculiarities regarding
The relevant MBE lecture can be found here http://security.cs.rpi.edu/courses/binexp-spring2015/lectures/19/11_lecture.pdf (the last section covers stack canary implementations and possible bypasses, as well as resources on deeper research).
A look at the target app, its vulns and its custom stack cookie protection
As usual, the target app can be found here - https://github.com/RPISEC/MBE/blob/master/src/lab08/lab8A.c.
Here are the compilation flags; static and no PIE - although the latter does not matter much in this case - we will leak the code segment base anyway:
Let's start with the main function:
We have two always functions called from the main function one after another, regardless to any user input;
selectABook() looks like this:
Apart from its (and the entire app's, for that matter) general weirdness, we can see that:
- the function is recurrent (line
29) when user input does not match any of the hardcoded conditions
- it's vulnerable to a stack-based buffer overflow via
scanf("%s",buf_secure)- line 16
- it's also vulnerable to format string (line
readC() are just simple methods printing out static hardcoded strings (Aristote's Metaphysics quotes), nothing useful in the context of exploitation (unless we had
printf() GOT overwritten, but that is not what's going to happen here):
So at this point it already looked like I had what was needed to pwn the app; two bugs to chain together:
- an overflow to overwrite the saved RET on the stack
- a format string to leak the value of stack canary (and stack and code base if neessary) - so we can overwrite the stack canary with its own original value and therefore avoid the stack guard noticing we smashed the stack and therefore avoid the stack guard preventing the program from returning to our arbitrary EIP
Leaking the standard canary with format string
Let's start with identifying how the actual built-in code for handling stack canaries looks like in gcc-produced assembly:
The same holds true for all other functions.
Now, let's see what the stack values look like between runs and how exactly stuff is aligned on the stack. As we want to leak from
selectABook()'s stack - because this is where the format string resides - let's put our breakpoints there:
Let's stop at
selectABook+15 - our current canary will be held in
selectABook+42 - after the
scanf() call - we'll fill the
buf with exactly 512 bytes so we don't overflow anything yet and see the original values on the stack.
OK, now let's continue. Now (we have already been prompted above -
Enter Your Favorite Author's Last Name:), we just paste 512 characters:
OK, we're past the
scanf() call. Let's see the stack now:
... snip ...
The format string we are exploiting is simple
buf_secure is 512 bytes-long. If we apply abuser friendly format string
%p (so the whole dword of choice is printed, as hex) - just like we did 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/) - considering that 512/4 = 128, we would expect our canary at
Nah, something's wrong. Maybe it's because string formats index the`$`-referred arguments starting at
1... Let's see what's under
Nah, it's the
buf_secure address itself.
How about 130?
Yeah more like it.
The value is consistent between function calls (
selectABook() as well as
selectABook()->selectABook() recurrent call - remember, the stack canary value is global to the entire process) and it changes between runs.
Also, in this case the saved EBP should be right next to it, at
Yup. The consecutive values are decreasing by a fixed offset, as recurrent calls of
We will need this value as well while developing the exploit for this.
As a matter of fact at this point I even wrote the first version of the exploit (https://github.com/ewilded/MBE-snippets/blob/master/LAB8/LAB8A/wannabe_initial_exploit.py).
As usual - the exploit failed at the first attempt...
And I was too lazy to actually debug it.
Instead, once I noticed that the saved RET was not overwritten in result of overflowing the buffer, I mistakenly assumed (self-limiting assumptions!) that the nullbyte-armoured stack canary (you probably already noticed that all the canaries so far had nullbyte as their least-significant byte) was the reason I could not - via
scanf("%s",buf_secure) - write beyond the nullbyte. I just thought
scanf() would stop reading after encountering
0x0 on its input, explicitly because of the
%s format string. I was wrong, but this assumption was reinforced by the fact that oftentimes while figuring out solutions to MBE targets I felt like it was all fine and dandy... only to later realize some tiny little obstacle. A tiny little obstacle forcing me to double the overall effort to attain a working exploit. Thus I assumed
selectABook() exploitability was too good (too easy) to be true.
To follow the
selectABook() exploitation route, skip to Building the ROP chain and then to Successfully exploiting selectABook() locally and remotely sections.
Otherwise, read on to explore the remainder of the target app and my exploit dev process.
Analyzing the rest of the code
We have only read half of the source code yet (as mentioned, this is an extensive write up)!
So, to feed our curiosity, instead of getting ahead of ourselves, let's see what's going on in the second function -
The stack-based buffer overflow of the 24-byte
buf buffer with
read(STDIN, buf,2048) at line
75 is quite blatant.
The rest of the code is just super-weird. First, the unused
char lolz, then the entire custom cookie mechanism.
Bypassing the custom canary check
So let's try to figure out what's the deal with it.
global_addr_check are global pointers held in the data segment, declared at the top of the source code, right below the compilation flags comment:
Although their initialization expressions are quite simple, I found them far away from obvious:
global_addr is a pointer to the next value after the
buf (I initially thought it's just the address of the
buf buffer incremented by
1, but I was wrong).
global_addr_check is the
global_addr (whatever it is) decremented by
And then finally there's this check:
The implication is as follows: if we want to exploit the stack-based buffer overflow in
findSomeWords(), we need the function to properly
return, without the
exit(EXIT_FAILURE) nor the standard stack guard interrupting.
So in order to make it return, we need to both:
- overwrite the original stack canary stored on the stack with its own value that we leak earlier via format string in selectABook() (there is just one stack canary value for the entire program, initiated before main() is executed, used by the stack guard for all following function calls)
- make the
((( globaladdr))^((globaladdrcheck))) != ((( globaladdr))^(0xdeadbeef))condition return
exit(EXITFAILURE)is not called
Let's simplify the custom-cookie condition.
We want this:
Which means we want this to be true:
global_addr_check must equal
OK fair enough, does this mean that the custom cookie protection by default makes the program exit with
EXIT_FAILURE error code and
Whoah there! message?
Yes, it does - simply running the app and providing "A" and "HELLO" inputs, respectively, results in this:
Fair enough. Let's bypass this custom canary, forgetting about the format string and overflows for now.
Let's make this app print out
Whew you made it! instead of doing
As my poor understanding of C kept me unsure about the mechanism, I got to the bottom of this by running gdb, disassebmling the
findSomeWords()function, setting up a breakpoint after the
read() call and stepping through it, instruction after instruction.
Debugging step by step.
At this point
0xbffff700 --> 0xc43c9300 - the address of the canary on the local function's stack.
At this point
EAX is still
0xbffff700 --> 0xc43c9300,
0xc43c9300 (canary from the stack). So now we have proof that the
global_addr = (&buf+0x1); instruction makes the
global_addr pointer point at the canary on the stack.
And now we are about to find out what's under
ds:0x80edf24 (the value just gets copied to
0x080481a8... weird. Let's peek the stack and see what's what:
global_addr points at the canary on the stack, while
global_addr_check points at the value two dwords (
-0x8) earlier. But hang on, where did this
0x080481a8 value come from?
The reason is that we did not fill the entire
buf buffer (I only sent 11
Bs at that time). Here's how the
buf overlaps with
This means that:
global_addr points at the stack-stored copy of the canary
global_addr_check points at the before-last byte of the
buf. So the
(&buf+0x1); instruction considered the
buf size, making it point at the next dword on the stack (the canary), while
global_addr_check = global_addr-0x2; made
global_addr_check points two dwords earlier, at the four bytes at
In recap: the stack-stored canary XOR-ed with
0xdeadbeef must equal stack-stored canary XOR-ed with the before-last dword of the
buff. Which simply means we just want the before-last dword of
buff (again, bytes 15-19) to be
So as long as the value we provide to the
read(STDIN,buf,2048) call in
0xdeadbeef at its fifth dword (bytes 15-19), we should bypass the custom stack protection:
Yup, that's exactly it:
OK cool, now we should be able to easily exploit the overflow in
Building the ROP chain
Since we don't have libc dynamically linked in here, we can't do
Fine, we just want to call
execve syscall the usual way:
eax = 0xb
ebx = pointer to "/bin/sh" - or, for that matter,
"/bin/python" or anything other than
"/bin/bash" (because bash is evil and drops the
euid if called from a suid binary - fucking safety features)
ecx = edx = 0
Let's start ROPeme ropshell.py, generate the gadgets from the target binary and search through them.
Spoiler alert: at the late stage of the exploit development process I realized that - when targeting the
scanf("%s") overflow - characters
0xa (newline) and
0xd (carriage-return) have to be avoided - as opposed to
0x0 (yes, really).
Thus, some of the gadgets I initially used had to be replaced due to the fact their addresses contained either
Running ROPeme, generating the gadgets:
Loading the gadgets:
Searching the gadgets (let's start with
xor anything anything):
OK, all the last three look good for starters, we can initiate
By the way, please keep in mind I started building this one with the assumption I could not use nullbytes in the payload, so instead of just putting a
pop eax address followed by a nullbyte, I kept assembling these workarounds - but it was fun and finally worked.
So - as there was no
xor edx edx (effectively
EDX=0) gadget, I followed one of the tips found here (https://trustfoundry.net/basic-rop-techniques-and-tricks/) to use
xchg instead (as we have already put
Just keep in mind now
EAX hold whatever garbage was in
EDX, so we'll have to zero it again, with one of the
xor eax eax gadgets.
Oh fuck, we can't use them. They all contain
Instead, we use the gadget putting
EDX followed by
inc edx to overflow it to
Now, we want
EAX to become
0 at the moment.
So why not to call
inc eax twelve times.
My meticulous effort to keep the chain clean from nullbytes finally collapsed when I had to nullify
ecx. Instead of
pop ecx followed by a nullbyte I did this:
Which looks nicer but still does not change the fact that
p32(0x1) = 0x00000001 - contains three nullbytes.
EBX = address of "/bin/sh" (we will smuggle
/bin/sh string to the stack in user input, then just calculate its address based on the leaked EBP value):
OK, one last thing, the
int 0x80 call.
But wait, it has a nullbyte (I did not want nullbytes!).
OK, so what's the instruction right above it?
NOP. Wonderful. So we can as well use
Successfully exploiting findSomeWords() locally - read(STDIN,buf,2048) not catching up via socat
Having all the bits and pieces I assembled an exploit targeting the
findSomeWords() overflow, with the following algorithm:
1) leak the canary and the saved RET via format string
2) make the
selectABook() function return by providing one of the expected values ("A") to its input
3) overflow the
buf buffer via
read(STDIN, buf, 2048), using the leaked canary as well as the
0xdeadbeef constant properly aligned in the payload, followed by four bytes of garbage to fill the saved EBP and the ROP chain beginning where saved RET was:
And it worked just fine on the target binary
/levels/lab08/lab8A, getting me a shell... The problem was that my privileges were still
lab8A instead of expected
lab8end... So I listed the
/levels/lab08 directory only to find out that this one is NOT a suid binary.
Instead I found this:
This means the target is being run from root like this:
socat TCP-LISTEN:8841,reuseaddr,fork,su=lab8end EXEC:timeout 60 /levels/lab08/lab8A
"Well that's just as well" - I thought. And just changed the
p = process(binary.path,stdin=PTY) line to
p = remote("127.0.0.1", 8841) and ran the thing.
It did not work.
Debugging (this time attaching to the target PID from root, as there was no other way) revealed that the exactly same exploit code did not deliver a single byte to the
So I thought "how come, ffs... Does it mean it completely ignores the user input?".
So I ran it manually to see that was the case:
So yes, I could only interact with the
selectABook() function. Simply typed "A" and pressed
enter, having no further opportunity to interact with the application.
At the moment I still do not know why - please let me know if you have a clue, I am curious.
Successfully exploiting selectABook() locally and remotely
At this point, as usual when I felt despair - I peeked into Corb3nik's solutions (https://github.com/Corb3nik/MBE-Solutions/blob/master/lab8a/solution.py) - not only to see that his exploit did not deal with
findSomeWords() and its custom stack canary at all - but mostly to realize he exploited
selectABook() (which meant
scanf("%s") ... with nullbytes in the payload!
So I fell back on the first exploit I wrote, started debugging it again. I found out the reason it was failing was due
0xd characters in the initial ROP chain. These turned out to be the real bad characters when it comes to
scanf()! Again, as opposed to nullbyte.
Then I found out that the string I was trying to make
EBX point to (
/bin/python) - as I found that string on the stack in the early stage of the exploit development and thought it would be nice to use it instead of delivering
/bin/sh via user input) - was not there when targeting the actual app running under
socat... It must have been a side effect of spawning the process from the python script with pwntools while developing the exploit.
Then it turned out my lengthy ROP chain (overflowing the local
buf_secure of the
selectABook()->selectABook() call ) overwrote the
/bin/sh value I delivered to the stack right after the initial format-string payload (the first call of
So I ended up adding additional 200 characters (
H) between the format string and
/bin/sh and increasing the value subtracted from the leaked EBP in the
binsh_addr =EBP_value-338 expression accordingly.
1) Attacking the first
selectABook() call to leak the canary and the saved
EBP via format string while also stuffing
/bin/sh on the stack - with 200
H-s between as this buffer will get overwritten by the ROP chain when we overflow the buffer in the second (recurrent) call
2) Attacking the second call
selectABook()->selectABook() by overflowing the
buf_secure with 512
B-s followed by the original leaked canary value, the original saved
EBP value (although this value does not matter here as long as it is not a bad char) and the
0xd-free ROP chain replacing the saved
3) Making the third
selectABook()->selectABook()->selectABook() call return (instead of continuing the recurrence) by providing one of the expected values -
The final code can be found here: