PicoCTF 2018 - Buffer Overflow 0

Note: This article is part of our PicoCTF 2018 BinExp Guide.

Spot the Bug

This one is relatively easy - since the name is a complete giveaway:

void vuln(char *input){
  char buf[16];
  strcpy(buf, input);
}

// ...
vuln(argv[1]);
// ...

This is a classic buffer overflow. We are defining a variable on the stack of a fixed size (16 bytes), and then we are copying a user-controlled value (the first argument to the program) into that buffer without checking whether it would fit first.

Obviously, the first argument can be any length we want, and it certainly can be larger than 16 chars. Therefore, anything in memory that follows the buf variable could potentially be overwritten (more on that in a bit). There are however some restrictions, namely that strcpy will only copy values until the first null byte (0x00).

Strategy

Now that we found the bug, we need a plan to use that bug in a way that will allow us to capture the flag. Since this is the first challenge, they’ve made it particularly easy for us:

void sigsegv_handler(int sig) {
  fprintf(stderr, "%s\n", flag);
  fflush(stderr);
  exit(1);
}

// ...
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
  printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
  exit(0);
}
fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler);

What this code does is load the content of the flag file (flag.txt) into a global variable named flag. The code then registers a signal handler for the SIGSEGV signal that will print the value of the flag variable to stderr. Getting the flag is just a matter of getting the program itself to segfault, which will call the segfault handler, and then print the flag.

Background Info

How does the vulnerable strcpy help us generate a segfault? If you already know, then go ahead and solve the challenge now, it’s under /problems/buffer-overflow-0_0_6461b382721ccca2318b1d981d363924 on the shell server.

For those who don’t know, the secret is that the memory for the buf variable is allocated on the stack. The stack is a region of memory that can shrink and grow over time, but it is also used for some very specific purposes on x86 architectures (32 bit programs make use of the stack heavily, much more so than 64 bit programs). Whenever you call a function, the address of the next instruction to execute after completing the function call is pushed onto the top of the stack (this is done automatically by the call instruction). If the called function requires any stack itself (and on x86 it almost certainly will), then it will generally preserve the current value of the ebp register by pushing it on the stack. It will then copy the current value of esp (the address of the top of the stack) into the ebp register, and then modify the stack by either pushing values or manually reserving space by modifying the esp register. At the end of the function, it will have to restore the esp and ebp registers, and then call ret, which is functionally equivalent to taking the value at the top of the stack and continuing execution from that address.

Generally, we say the stack grows “up”, which is to say that when we grow the stack, we actually start using memory with consecutively lower and lower addresses. (Which is upward if you were to look at memory vertically starting from low addresses at the top and incrementing to high addresses at the bottom).

Therefore, the layout in memory of the vuln function looks something like this

lower addresses higher addresses
buf[16] < padding > <old ebp> <return address>

The padding exists because compilers will generally have alignment requirements, or preserve space to allow for “off-by-one” errors to not immediately crash to program.

As you can see, if you overwrite the memory past the end of buf, you will eventually overwrite both the old ebp value and the return address, since those values follow the stack variables in memory.

Here’s the corresponding assembly code of the vuln function (I used objdump -M intel -S vuln to look at the assembly)

push   ebp
mov    ebp,esp
sub    esp,0x18
sub    esp,0x8
push   DWORD PTR [ebp+0x8]
lea    eax,[ebp-0x18]
push   eax
call   80484b0 <strcpy@plt>
add    esp,0x10
nop
leave  
ret

You can see here that the code actually reserves 0x18 (24) bytes for the stack buffer, so the “padding” in the above table is 8 bytes.

Exploitation

The easy way to approach this problem is to just execute the program with a sufficiently long argument, almost anything 32 chars or longer is guaranteed to overwrite both the old ebp value AND the return address. If the return address isn’t valid code, or isn’t even mapped into the address space, then the program will immediately issue a segfault, the segfault handler will run, and the flag will be written to stderr.

/problems/buffer-overflow-0_0_6461b382721ccca2318b1d981d363924$ ./vuln 12345678901234567890123456789012
picoCTF{===REDACTED===}

The complications arise if your argument is long enough to overwrite the ebp value on the stack, but not long enough to overwrite the return address. In this case, the program doesn’t immediately segfault, instead it continues but the value of the ebp register is corrupt after returning from the function. Eventually that corrupt ebp value ends up being used to populate the esp register. At this point, the program has a non-functional stack (it likely points to memory that isn’t even mapped into the address space). With a corrupt esp value, it is essentially impossible for code to run, because the x86 is so reliant on the stack. Therefore, even though you may have a SIGSEGV handler registered, it will be impossible for that handler to even get called, and the program will have no choice but to crash.

/problems/buffer-overflow-0_0_6461b382721ccca2318b1d981d363924$ ./vuln 123456781234567812345678
Segmentation fault (core dumped)